Artigo Técnico

Gradientes PDF axial e radial em Delphi com PDFium

O PDFium Component cria gradientes PDF axial e radial em Delphi por meio do TPdf.SaveAsShadingCreated. Você descreve cada gradiente em um registro TPdfShadingCreate (tipo, espaço de cor, coordenadas, cores ou stops, página alvo), e o método grava uma cópia verificada em que todo fill é um dicionário de shading real da ISO 32000-1, registrado no dicionário /Resources /Shading da página e pintado pelo operador sh, então o PDFium o renderiza nativamente

A necessidade aparece em geradores de relatórios e certificados. Um designer entrega um mock-up com uma faixa de cabeçalho suave de azul para branco ou um brilho radial atrás de um selo, e a API de page-objects que cobre texto, paths e imagens não tem nada para isso. Você pode fingir um gradiente com cem retângulos finos em cores levemente diferentes, mas o resultado mostra bandas visivelmente quando ampliado, incha o content stream e imprime mal. O PDF tem shadings suaves desde o 1.3; o trabalho está em gravá-los de forma que um renderer realmente os desenhe

Por que o PDFium Component grava gradientes como um salvar separado?

O TPdf.SaveAsShadingCreated é uma operação de salvar em vez de uma chamada de page-object porque a ABI pública do PDFium não tem função que crie um dicionário de shading ou emita um operador sh. A camada de page-objects usada ao criar PDFs do zero com o PDFium Component constrói paths, texto e imagens, e é ali que ela para. Shadings vivem um nível abaixo, no grafo de objetos e na sintaxe de content stream

Então o método trabalha em estágios. O PDFium primeiro salva o documento aberto nativamente, o que mantém toda edição que você fez pela API normal. O PDFium Component então regrava essa saída preparada: aloca um objeto indireto por shading, acrescenta a entrada de recurso a cada página alvo, anexa um novo content stream ao /Contents da página, e grava uma tabela de cross-reference fresca. Antes de qualquer coisa chegar ao seu stream ou arquivo, o resultado é recarregado com uma checagem estrita de cross-reference, e o método devolve False com uma mensagem no relatório se essa recarga falhar. A sobrecarga de arquivo grava primeiro em um arquivo temporário, então um salvar que falha não destrói um alvo existente

Construindo 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, então 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 reta, e define /Extend [true true] para as cores finais continuarem além das duas pontas do eixo

Geometria de shading axial e radial do PDFium Component para Delphi: um gradiente Type 2 mistura StartColor a EndColor ao longo da reta de x0 y0 a x1 y1 com /Extend true true continuando além das duas pontas, enquanto um gradiente Type 3 mistura entre um círculo inicial de raio r0 e um círculo final de raio r1
O PDFium pinta o shading sobre o que o clip permitir: Coords Type 2 posicionam o eixo de mistura de x0 y0 a x1 y1, Coords Type 3 posicionam os dois círculos, e /Extend [true true] carrega as duas cores finais além da geometria
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 borda superior
    Fills[0].EndColor := [1, 1, 1];             // branco mais abaixo
    Fills[0].PageNumber := 1;                   // base 1
    Fills[0].ResourceName := 'HeaderBand';      // opcional, o padrã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 toda cor precisa 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 você passar em IccProfile, que precisa expor 1, 3 ou 4 componentes; o perfil é embutido como seu próprio stream 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. Toda coordenada e componente é checada contra NaN e infinito antes de um byte ser gravado

Note que as coordenadas estão no espaço de usuário padrão, com a origem no canto inferior esquerdo da página. Lembre que o eixo só define onde a mistura acontece: a área pintada é o que o clip permitir, o que importa mais do que parece, como as próximas seções mostram

Gradientes multi-stop compilam para uma função stitching Type 3

O PDFium Component transforma um array Stops na função que um leitor PDF espera: dois stops viram uma única função Type 2, e 3 a 256 stops viram uma função stitching Type 3 (ISO 32000-1 §7.10.4) feita de um segmento Type 2 linear por par adjacente. Cada TPdfShadingCreateStop carrega um Position e um Color. Quando o Stops não está vazio ele substitui inteiramente o StartColor e o EndColor

A função stitching precisa de três arrays que é fácil errar sutilmente à mão. O /Functions guarda os segmentos, o /Bounds guarda apenas as posições internas de stop (k segmentos precisam de k-1 bounds), e o /Encode mapeia cada subdomínio de volta a [0 1] para todo segmento ver sua faixa de entrada completa. O componente deriva os três dos seus stops, e é por isso que ele é rigoroso com a entrada: as posições precisam ser finitas, estritamente crescentes, começar em 0 e terminar em 1 (dentro de 1e-6). Uma lista de stops que começa em 0.1 deixaria parte do domínio sem função definida, e o método a rejeita em vez de chutar

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 crescendo 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 stops 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 cor duras, então uma lista de stops não suaviza através delas; se uma junção parecer forte demais, acrescente stops em vez de esperar que o leitor interpole curvatura

Compilação de gradiente multi-stop do PDFium Component em Delphi: três Stops nas posições 0.0, 0.4 e 1.0 viram uma função stitching Type 3 com um segmento Type 2 por par de stops adjacente, /Bounds [0.4] para a posição interna e /Encode [0 1 0 1] mapeando cada subdomínio de volta à faixa completa
Cada par de stops adjacentes vira um segmento Type 2 dentro de uma função Type 3; os arrays /Bounds e /Encode derivados são o que funções stitching escritas à mão costumam errar

Por que o PDFium silenciosamente não desenha nada?

O PDFium renderiza um shading recém-criado apenas quando dois detalhes estão certos: o /ShadingType precisa ser um inteiro, e a chamada de sh precisa sentar dentro de um clipping path. Errar qualquer um dos dois e o arquivo abre, valida estruturalmente, e mostra uma página intocada. Sem erro, sem aviso, sem linha de log

Armadilha um: /ShadingType é um número, não um nome

A ISO 32000-1 Tabela 78 define o /ShadingType como um inteiro: 2 para axial, 3 para radial. A primeira versão deste writer emitia /ShadingType /Axial porque lia melhor em um hex dump. O PDFium lê a chave com um getter inteiro, recebe 0 para um nome, classifica o shading como inválido, falha ao carregá-lo, e pula o operador sh sem reclamar. A página renderizava em branco e nada no PDFium dizia por quê. A causa só ficou visível depois de construir um PDF mínimo escrito à mão e abri-lo em um segundo renderer independente, que reportou um tipo de shading desconhecido igual a 0. A lição se generaliza a todo writer de PDF: quando a especificação tipa uma enumeração como inteiro, grave o inteiro, por mais tentadora que uma autodescritiva pareça

Armadilha dois: um content stream com apenas sh não tem bounding box

O operador sh (ISO 32000-1 §8.7.4.2) pinta o shading sobre a região de clipping atual, então a própria extensão da página o limita no papel. O PDFium, porém, constrói um objeto de página de shading cujo retângulo vem ou do clip path ativo ou da bounding box acumulada por operadores de desenho anteriores no mesmo stream, e o renderer descarta objetos com retângulo vazio. Um stream recém-anexado que contém nada além de /Sh1 sh não tem nenhum dos dois, então o gradiente é descartado antes da rasterização. O TPdf.SaveAsShadingCreated portanto lê o crop box de cada página (recuando para o media box, depois para o tamanho da página) e envolve cada invocação em um clip explícito re W n. Em uma página US Letter com origem em zero, o stream anexado fica assim:

q
0 0 612 792 re W n
/HeaderBand sh
Q

O par q/Q isola o clip e qualquer transformação do conteúdo existente da página, então nada desenhado depois na página os herda. Se você escrever invocações de shading você mesmo em outra ferramenta, copie esta forma: um clip explícito no mesmo stream é barato e remove a dependência do que veio antes

Por que um shading PDF de aparência válida não desenha nada no PDFium: um /ShadingType gravado como o nome Axial é lido por um getter inteiro como 0 e o operador sh é pulado silenciosamente, e um content stream com apenas sh não tem bounding box até um clip explícito re W n dar-lhe um retângulo
Ambas as falhas de shading invisível são silenciosas no PDFium, então o SaveAsShadingCreated grava o ShadingType inteiro ele mesmo e envolve toda chamada de sh em um clip re W n do crop box

Como uma matriz de gradiente customizada interage com o clip?

Uma matriz de gradiente no PDFium Component é aplicada como um operador cm colocado depois do clip e antes do sh, então o clip permanece 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 precisam ser finitos e o determinante não nulo, porque uma matriz singular colapsaria o shading sobre uma reta. O componente também registra os seis valores como /Matrix no dicionário de shading, mas para uma chamada direta de sh é o cm no content stream que define o sistema de coordenadas em que o PDFium pinta

A ordem importa aqui, e é a terceira armadilha. Os operandos do re são interpretados na matriz de transformação atual. Ponha o cm primeiro e o retângulo de clip do tamanho da página é escalado, rotacionado ou enviesado junto com o gradiente, então uma matriz que achata uma elipse também achata a área visível e partes da página perdem o fill. Recortar primeiro no espaço de usuário não transformado, então concatenar a matriz, dá o resultado esperado: um gradiente transformado cobrindo a página inteira. O guia de stamps de Form XObject e sua matriz de seis números percorre o que cada componente de matriz faz caso você precise montar uma

procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
  BuildSealGlow(Fill);       // fill 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;       // então move para o centro do selo
  Fill.MatrixF := 160;
end;

Lendo o relatório e conhecendo os limites

O TPdfShadingCreateReport diz o que foi gravado: SourceByteCount e OutputByteCount, CreatedShadingObjectNumbers (um número de objeto por entrada de entrada, em ordem), AttachedShadingCount, Succeeded e ErrorMessage. Erros de validação nomeiam a entrada e o campo, por exemplo uma lista de stops que não é estritamente crescente, então valem ser logados verbatim

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 limitado. Ele cria apenas shadings axial e radial; shadings baseados em função e em malha (Types 1 e 4 a 7) e espaços de cor pattern são rejeitados. Uma chamada aceita 1 a 4096 fills, os números de página precisam existir, e nomes de recurso são limitados a 127 caracteres de letras, dígitos, underscore, menos e ponto. Entrada criptografada é recusada no estágio de trailer. Um documento assinado precisa de AllowSignedDocument := True, e como a saída é uma regravação completa em vez de uma atualização incremental, assinaturas existentes deixarão de validar, então assine depois de acrescentar gradientes. Cada fill é pintado no fim do conteúdo da página, por cima do que já está lá; para um efeito de fundo, crie o gradiente primeiro e acrescente texto e imagens depois, ou use optional content como descrito no artigo sobre controlar camadas de optional content PDF em Delphi quando o fill deve ser chaveável

FAQ

O SaveAsShadingCreated suporta gradientes multi-stop?

Sim. Passe 2 a 256 entradas em Stops com posições estritamente crescentes de 0 a 1. Dois stops produzem uma função Type 2 e três ou mais produzem uma função stitching Type 3 com arrays derivados de /Bounds e /Encode

Por que meu gradiente fica invisível no PDFium quando o arquivo parece válido?

Confira que o /ShadingType é o inteiro 2 ou 3 em vez de um nome, e que o operador sh roda dentro de um clip re W n no mesmo content stream. O PDFium pula silenciosamente tipos de shading inválidos e descarta objetos de shading com retângulo vazio

Que espaços de cor um shading criado pode usar?

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, optional content e o resto dos writers em estágios vêm com o PDFium Component for Delphi and C++Builder, que embrulha o PDFium para projetos Delphi, C++Builder e Lazarus