O PDFium Component cria gradientes PDF axiais e radiais no Delphi através do TPdf.SaveAsShadingCreated. Descreve cada gradiente num registo TPdfShadingCreate (tipo, espaço de cor, coordenadas, cores ou paragens, página de destino), e o método escreve uma cópia verificada em que cada preenchimento é um verdadeiro dicionário de shading ISO 32000-1, registado no dicionário /Resources /Shading da página e pintado pelo operador sh, pelo que o PDFium o renderiza nativamente
A necessidade aparece em geradores de relatórios e certificados. Um designer entrega uma maqueta com uma faixa de cabeçalho azul-suave-a-branco ou um brilho radial atrás de um selo, e a API de objetos de página que cobre texto, caminhos e imagens não tem nada para isso. Pode fingir um gradiente com cem retângulos finos em cores ligeiramente diferentes, mas o resultado mostra bandas visivelmente ao ampliar, incha o fluxo de conteúdo e imprime mal. O PDF tem shadings suaves desde o 1.3; o trabalho está em escrevê-los de forma a que um renderizador os desenhe de facto
Por que é que o PDFium Component escreve gradientes numa gravação separada?
O TPdf.SaveAsShadingCreated é uma operação de gravação em vez de uma chamada de objeto de página porque o ABI público do PDFium não tem função nenhuma que crie um dicionário de shading ou emita um operador sh. A camada de objetos de página usada ao criar PDFs do zero com o PDFium Component constrói caminhos, texto e imagens, e é aí que para. Os shadings vivem um nível abaixo, no grafo de objetos e na sintaxe do fluxo de conteúdo
Por isso o método trabalha em etapas. O PDFium primeiro grava o documento aberto nativamente, o que mantém todas as edições que fez através da API normal. O PDFium Component depois reescreve essa saída preparada: aloca um objeto indireto por shading, acrescenta a entrada de recurso a cada página de destino, anexa um novo fluxo de conteúdo ao /Contents da página, e escreve uma tabela de referências cruzadas fresca. Antes de qualquer coisa chegar ao seu fluxo ou ficheiro, o resultado é recarregado com uma verificação estrita de referências cruzadas, e o método devolve False com uma mensagem no relatório se essa recarga falhar. O overload de ficheiro grava primeiro num ficheiro temporário, pelo que uma gravação falhada não esmaga um destino existente
Construir um gradiente axial de duas cores
Um shading axial (ISO 32000-1 §8.7.4.5.3, Type 2) mistura cores ao longo de uma reta de x0 y0 a x1 y1, pelo que o Coords recebe quatro valores. Um shading radial (§8.7.4.5.4, Type 3) mistura entre dois círculos e recebe seis: x0 y0 r0 x1 y1 r1. Com StartColor e EndColor simples, o PDFium Component emite uma função exponencial Type 2 (§7.10.3) com /N 1, que é uma interpolação linear direta, e define /Extend [true true] para que as cores finais continuem para lá de ambas as pontas do eixo
uses
System.SysUtils, PDFium, FPdfCompress;
procedure AddHeaderBand(const InFile, OutFile: string);
var
Pdf: TPdf;
Fills: TPdfShadingCreateShadings;
Report: TPdfShadingCreateReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InFile;
Pdf.Active := True;
SetLength(Fills, 1);
Fills[0] := Default(TPdfShadingCreate);
Fills[0].ShadingType := pstAxial;
Fills[0].ColorSpace := pscDeviceRGB;
Fills[0].Coords := [0, 792, 0, 640]; // x0 y0 x1 y1, de cima para baixo
Fills[0].StartColor := [0.12, 0.35, 0.75]; // azul escuro na aresta de cima
Fills[0].EndColor := [1, 1, 1]; // branco mais abaixo
Fills[0].PageNumber := 1; // base 1
Fills[0].ResourceName := 'HeaderBand'; // opcional, a predefinição é Sh1, Sh2...
if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
A contagem de componentes de cada cor tem de casar com o espaço de cor: um para pscDeviceGray e pscCalGray, três para pscDeviceRGB, pscCalRGB e pscLab, quatro para pscDeviceCMYK. Para pscICCBased a contagem vem dos bytes de perfil que passar em IccProfile, que devem expor 1, 3 ou 4 componentes; o perfil é incorporado como o seu próprio fluxo ICC com o /N correspondente. Espaços calibrados aceitam CalWhitePoint opcional (três valores, Y positivo), CalGamma (um valor para CalGray, três para CalRGB) e, para Lab, um LabRange de seis valores. Cada coordenada e componente é verificado contra NaN e infinito antes de um byte ser escrito
Note que as coordenadas estão no espaço de utilizador predefinido, com a origem no canto inferior esquerdo da página. Lembre-se de que o eixo só define onde a mistura acontece: a área pintada é a que o recorte permitir, o que importa mais do que parece, como as secções seguintes mostram
Gradientes multiparagem compilam para uma função stitching Type 3
O PDFium Component transforma uma matriz Stops na função que um leitor PDF espera: duas paragens tornam-se uma única função Type 2, e 3 a 256 paragens tornam-se uma função stitching Type 3 (ISO 32000-1 §7.10.4) feita de um segmento Type 2 linear por par adjacente. Cada TPdfShadingCreateStop transporta um Position e um Color. Quando o Stops não está vazio substitui completamente o StartColor e o EndColor
A função stitching precisa de três matrizes que são fáceis de errar subtilmente à mão. O /Functions guarda os segmentos, o /Bounds guarda só as posições interiores de paragem (k segmentos precisam de k-1 limites), e o /Encode mapeia cada subdomínio de volta para [0 1] para que cada segmento veja o seu intervalo de entrada completo. O componente deriva os três das suas paragens, razão pela qual é estrito quanto à entrada: as posições têm de ser finitas, estritamente crescentes, começar em 0 e acabar em 1 (dentro de 1e-6). Uma lista de paragens que comece em 0.1 deixaria parte do domínio sem função definida, e o método rejeita-a em vez de adivinhar
function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
I: Integer;
begin
Result.Position := Position;
SetLength(Result.Color, Length(Rgb));
for I := 0 to High(Rgb) do
Result.Color[I] := Rgb[I];
end;
procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
Fill := Default(TPdfShadingCreate);
Fill.ShadingType := pstRadial;
Fill.ColorSpace := pscDeviceRGB;
// x0 y0 r0 x1 y1 r1: um ponto no centro do selo a crescer até um círculo de 90 pt
Fill.Coords := [450, 160, 0, 450, 160, 90];
Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
Stop(0.4, [0.95, 0.75, 0.30]),
Stop(1.0, [1.00, 1.00, 1.00])];
Fill.PageNumber := 1;
end;
Para um gradiente de três paragens isso produz um /Functions com dois segmentos Type 2, /Bounds [0.4] e /Encode [0 1 0 1]. As fronteiras de segmento são junções de cores duras, pelo que uma lista de paragens não suaviza através delas; se uma junção parecer demasiado acentuada, acrescente paragens em vez de esperar que o leitor interpole curvatura
Por que é que o PDFium desenha silenciosamente nada?
O PDFium renderiza um shading recém-criado só quando dois detalhes estão certos: o /ShadingType tem de ser um inteiro, e a chamada sh tem de estar dentro de um caminho de recorte. Erre qualquer um e o ficheiro abre, valida estruturalmente, e mostra uma página intocada. Sem erro, sem aviso, sem linha de log
Armadilha um: o /ShadingType é um número, não um nome
A Tabela 78 da ISO 32000-1 define o /ShadingType como um inteiro: 2 para axial, 3 para radial. A primeira versão deste escritor emitia /ShadingType /Axial porque se lia melhor num hex dump. O PDFium lê a chave com um getter de inteiro, recebe 0 para um nome, classifica o shading como inválido, falha ao carregá-lo, e salta o operador sh sem queixa. A página renderizava em branco e nada no PDFium dizia porquê. A causa só ficou visível depois de construir um PDF mínimo escrito à mão e abri-lo num segundo renderizador independente, que reportava um tipo de shading desconhecido de 0. A lição generaliza a todos os escritores PDF: quando a especificação tipa uma enumeração como inteiro, escreva o inteiro, por mais tentadora que pareça um nome autodescritivo
Armadilha dois: um fluxo de conteúdo que só contém sh não tem caixa delimitadora
O operador sh (ISO 32000-1 §8.7.4.2) pinta o shading sobre a região de recorte atual, pelo que a extensão da própria página o delimita no papel. O PDFium, porém, constrói um objeto de página de shading cujo retângulo vem ou do caminho de recorte ativo ou da caixa delimitadora acumulada por operadores de desenho anteriores no mesmo fluxo, e o renderizador abate objetos com retângulo vazio. Um fluxo recém-anexado que não contenha nada além de /Sh1 sh não tem nenhum dos dois, pelo que o gradiente é descartado antes da rasterização. O TPdf.SaveAsShadingCreated por isso lê a caixa de recorte de cada página (regradando para a media box, depois para o tamanho da página) e embrulha cada invocação num recorte explícito re W n. Numa página US Letter com a origem em zero, o fluxo anexado tem este aspeto:
q
0 0 612 792 re W n
/HeaderBand sh
Q
O par q/Q isola o recorte e qualquer transformação do conteúdo existente da página, pelo que nada desenhado mais tarde na página os herda. Se escrever invocações de shading você mesmo noutra ferramenta, copie esta forma: um recorte explícito no mesmo fluxo é barato e remove a dependência do que veio antes
Como interage uma matriz de gradiente personalizada com o recorte?
Uma matriz de gradiente no PDFium Component é aplicada como um operador cm colocado depois do recorte e antes do sh, pelo que o recorte continua de página inteira enquanto só a geometria do gradiente é transformada. Defina HasMatrix como True e preencha MatrixA a MatrixF, os mesmos seis números de qualquer matriz de transformação PDF. Os valores têm de ser finitos e o determinante não nulo, porque uma matriz singular colapsaria o shading numa reta. O componente também regista os seis valores como /Matrix no dicionário de shading, mas para uma chamada sh direta é o cm no fluxo de conteúdo que define o sistema de coordenadas em que o PDFium pinta
A ordem importa aqui, e é a terceira armadilha. Os operandos re são interpretados na matriz de transformação atual. Ponha o cm primeiro e o retângulo de recorte do tamanho da página é escalado, rodado ou enviesado juntamente com o gradiente, pelo que uma matriz que achata uma elipse também achata a área visível e partes da página perdem o preenchimento. Recortar primeiro no espaço de utilizador não transformado, depois concatenar a matriz, dá o resultado esperado: um gradiente transformado a cobrir a página inteira. O guia de carimbos Form XObject e a sua matriz de seis números percorre o que cada componente da matriz faz se precisar de construir uma
procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
BuildSealGlow(Fill); // preenchimento radial centrado em 450,160
Fill.Coords := [0, 0, 0, 0, 0, 90]; // círculos na origem
Fill.HasMatrix := True;
Fill.MatrixA := 1.6; // estica horizontalmente
Fill.MatrixB := 0;
Fill.MatrixC := 0;
Fill.MatrixD := 0.8; // comprime verticalmente
Fill.MatrixE := 450; // depois move para o centro do selo
Fill.MatrixF := 160;
end;
Ler o relatório e conhecer os limites
O TPdfShadingCreateReport diz-lhe o que foi escrito: SourceByteCount e OutputByteCount, CreatedShadingObjectNumbers (um número de objeto por entrada de entrada, pela ordem), AttachedShadingCount, Succeeded e ErrorMessage. Erros de validação nomeiam a entrada e o campo, por exemplo uma lista de paragens que não é estritamente crescente, por isso valem a pena ser registados à letra
if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
Writeln(Format('%d shading(s) attached, %d -> %d bytes',
[Report.AttachedShadingCount, Report.SourceByteCount,
Report.OutputByteCount]))
else
Writeln('Rejected: ' + Report.ErrorMessage);
O perfil é deliberadamente delimitado. Cria só shadings axiais e radiais; shadings baseados em função e em malha (Types 1 e 4 a 7) e espaços de cor de padrão são rejeitados. Uma chamada aceita 1 a 4096 preenchimentos, os números de página têm de existir, e os nomes de recursos são limitados a 127 caracteres de letras, dígitos, sublinhado, menos e ponto. Entrada encriptada é recusada na fase de trailer. Um documento assinado precisa de AllowSignedDocument := True, e como a saída é uma reescrita completa em vez de uma atualização incremental, as assinaturas existentes deixarão de validar, por isso assine depois de acrescentar gradientes. Cada preenchimento é pintado no fim do conteúdo da página, por cima do que já lá está; para um efeito de fundo, crie o gradiente primeiro e acrescente texto e imagens depois, ou use conteúdo opcional como descrito no artigo sobre controlar caminhos de conteúdo opcional PDF no Delphi quando o preenchimento deva poder ser ligado e desligado
FAQ
O SaveAsShadingCreated suporta gradientes multiparagem?
Sim. Passe 2 a 256 entradas em Stops com posições estritamente crescentes de 0 a 1. Duas paragens produzem uma função Type 2 e três ou mais produzem uma função stitching Type 3 com matrizes /Bounds e /Encode derivadas
Por que o meu gradiente é invisível no PDFium quando o ficheiro parece válido?
Verifique que o /ShadingType é o inteiro 2 ou 3 em vez de um nome, e que o operador sh corre dentro de um recorte re W n no mesmo fluxo de conteúdo. O PDFium salta silenciosamente tipos de shading inválidos e abate objetos de shading com retângulo vazio
Que espaços de cor pode usar um shading criado?
O TPdfShadingCreate aceita DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab e ICCBased com um perfil de 1, 3 ou 4 componentes. Espaços de cor Pattern e Indexed não fazem parte do perfil de criação
Criação de gradientes, conteúdo opcional e o resto dos escritores preparados viajam com o PDFium Component for Delphi and C++Builder, que embrulha o PDFium para projetos Delphi, C++Builder e Lazarus