Artigo Técnico

Templates de Blocos PDF no Delphi: Texto, Imagem e PDF

O PDFlibPas (losLab PDF Library) preenche templates de blocos PDF a partir do Delphi: um bloco é um retângulo nomeado numa página de template, tipado como texto, imagem ou PDF, e o FillTextBlock, o FillImageBlock e o FillPDFBlock desenham dados variáveis nele. Os blocos são encontrados como anotações de página PDFlibBlock:Text, PDFlibBlock:Image e PDFlibBlock:PDF, ou num dicionário page-piece sob /PieceInfo

O caso de uso é do tipo aborrecido e de grande volume. Um designer maqueta um certificado, um bilhete de evento ou uma carta de cobrança uma única vez, e o programa carimba um nome, um número de lugar, uma imagem de assinatura ou uma página de código de barras pré-renderizada em espaços fixos para dez mil registos. Os campos de formulário conseguem fazer parte disso, mas um campo AcroForm é um objeto interativo com o seu próprio fluxo de aparência e as suas próprias regras, e normalmente acaba-se por o achatar de qualquer forma. Um bloco é mais simples: um retângulo mais um punhado de dicas de estilo, consumido uma vez e depois deitado fora

O que é exatamente um bloco PDF no PDFlibPas?

Um bloco do PDFlibPas é uma anotação de página cujo /Subtype é PDFlibBlock:Text, PDFlibBlock:Image ou PDFlibBlock:PDF, cujo /Rect é a área do marcador, e cuja string /Contents guarda name;key=value;key=value. O primeiro segmento é o nome do bloco, tudo o que se segue é uma lista de propriedades. O TPDFlib.AddBlock escreve a anotação com /F 4 (Print) e sem fluxo de aparência, pelo que um bloco por preencher não ocupa qualquer píxel no ecrã ou no papel

Anatomia de um template de blocos do PDFlibPas para dados variáveis no Delphi: uma anotação de página com /Subtype PDFlibBlock:Text, Image ou PDF, um marcador /Rect, uma string /Contents de nome e propriedades key=value, /F 4 Print e sem fluxo de aparência, com o dicionário page-piece /PieceInfo /PDFlib /Blocks como segunda fonte
Os blocos enumeram-se de base 1 por página com as anotações primeiro e os blocos page-piece depois; as entradas de matriz tornam-se Block1 e Block2, as chaves /Custom achatam em propriedades, e tudo o que não seja Image, PDF ou Page é tratado como texto

O lado da leitura tem uma segunda fonte. Depois de enumerar as anotações de bloco, o PDFlibPas recorre ao caminho do dicionário de página /PieceInfo << /PDFlib << /Blocks ... >> >> e à sua variante /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>. Os dicionários page-piece são PDF padrão, definidos na ISO 32000-1 §14.5 como o lugar onde uma aplicação guarda dados privados anexados a uma página, e é aí que vivem normalmente as definições de blocos de ferramentas externas de autoria de templates. O contentor /Blocks pode ser um dicionário indexado pelo nome do bloco ou uma matriz simples; na forma de matriz o PDFlibPas nomeia as entradas Block1, Block2 e assim por diante, a menos que um bloco transporte o seu próprio /Name

Dentro de um bloco page-piece, o valor /Subtype (ou /Type) Image mapeia para um bloco de imagem, PDF ou Page mapeia para um bloco PDF, e tudo o resto é tratado como texto. Todas as outras chaves exceto /Type, /Subtype, /Name, /Rect e /Custom tornam-se propriedades, e as chaves de um dicionário /Custom aninhado são achatadas para dentro também, pelo que uma entrada /Custom << /department (Finance) >> é legível como GetBlockProperty(Page, Index, 'department'). Os índices de bloco são de base 1 por página, blocos de anotação primeiro, blocos page-piece depois

Criar um template de blocos com o AddBlock

O TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) acrescenta um bloco à página atualmente selecionada e devolve 1 em caso de sucesso. Devolve 0 para um nome vazio, uma largura ou altura zero ou negativa, ou um BlockType diferente de 'text', 'image' ou 'pdf' (sem distinguir maiúsculas). O Properties é anexado ao nome com um ponto e vírgula, pelo que usa a mesma sintaxe key=value;key=value da string guardada

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // arte estática: moldura, logótipo, texto fixo ...
    Lib.DrawText(72, 200, 'This certifies that');

    Lib.AddBlock('text', 'recipient', 72, 230, 450, 44,
      'fontname=Times-Bold;fontsize=30;fillcolor={cmyk 0 0.55 1 0.1};alignment=center');
    Lib.AddBlock('text', 'course', 72, 290, 450, 24,
      'fontname=Helvetica;fontsize=14;alignment=center;fitmethod=word');
    Lib.AddBlock('image', 'signature', 360, 600, 160, 60, '');
    Lib.AddBlock('pdf', 'seal', 72, 580, 100, 100, '');

    if Lib.GetBlockCount(1) <> 4 then
      raise Exception.Create('Template blocks missing');
    Lib.SaveToFile('certificate-template.pdf');
  finally
    Lib.Free;
  end;
end;

O analisador de propriedades é deliberadamente frouxo. As chaves são comparadas sem distinguir maiúsculas de minúsculas, os valores são aparados mas de resto devolvidos à letra (um valor escrito como {top left} regressa com as suas chavetas), e uma chave em falta devolve uma string vazia. O único limite duro é o separador: um valor não pode conter um ponto e vírgula, porque o ponto e vírgula termina o segmento

Como aplica o FillTextBlock fonte, cor e alinhamento?

O TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) lê as propriedades do bloco, aplica-as ao estado de desenho, ajusta o texto ao retângulo do bloco com o FitTextBox e depois restaura a fonte, o tamanho do texto, a cor do texto e o alinhamento do chamador num bloco finally. Essa restauração importa num ciclo de merge: um bloco de título vermelho de 30 pt não pode deixar o seu estilo escapar para a próxima chamada DrawText que fizer na mesma página

  • fontname (ou font): primeiro reutiliza uma fonte já carregada no documento com esse nome, depois mapeia os nomes Standard 14 como Helvetica-Bold, Times-Roman ou Courier, e por fim tenta o AddTrueTypeFont com o nome
  • fontsize (ou size): o tamanho do texto passado ao ajustador, que o encolhe se o texto não couber
  • fillcolor (ou textcolor, color): #RRGGBB, {rgb r g b}, {cmyk c m y k}, {gray g}, listas simples de 3 ou 4 números, ou os nomes black, white, red, green, blue e gray; componentes RGB acima de 1 são lidos como 0–255
  • alignment, align e position são lidos em conjunto: left, right, center (ou centre) e justify definem o alinhamento horizontal, top e bottom definem a colocação vertical, e a predefinição é centrado verticalmente
  • fitmethod: word ou nofitsplit impede que palavras compridas sejam partidas, scale ou expand deixa o tamanho crescer para encher a caixa

O caminho CMYK é a razão pela qual as gráficas se importam. O {cmyk 0 0.55 1 0.1} chama o SetTextColorCMYK, pelo que o texto variável é emitido em DeviceCMYK e acerta na cor da marca na impressão sem uma ida e volta RGB. O FillTextBlock devolve 1 quando o texto foi desenhado; devolve 0 para um Text vazio, um bloco em falta, ou um bloco que não seja de texto, e em todos esses casos o bloco fica no lugar

Ordem de processamento do FillTextBlock no PDFlibPas para Delphi: as propriedades do bloco fontname, fontsize, fillcolor e alignment são aplicadas ao estado de desenho, o valor CMYK passa pelo SetTextColorCMYK, o texto é ajustado com o FitTextBox, e a fonte, tamanho, cor e alinhamento do chamador regressam num bloco finally
O estilo do bloco é aplicado uma vez e revertido, pelo que um título vermelho de 30 pt não pode escapar para o próximo DrawText; a chamada devolve 1 quando o texto foi desenhado e 0 com o bloco no lugar para texto vazio ou bloco não textual

Preencher blocos de imagem e PDF, e os bits de Options

O FillImageBlock(Page, BlockIndex, ImageID, Options) desenha uma imagem previamente adicionada, e o FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importa uma página de outro documento carregado como um Form XObject e desenha-a, o que mantém conteúdo vetorial como um selo, uma página de código QR ou um timbrado inteiramente vetorial. Ambos preservam o rácio de aspeto da origem e centram o resultado dentro do retângulo do bloco. As três funções de preenchimento partilham os mesmos bits de Options: o bit 1 (valor 1) remove o bloco após um preenchimento bem-sucedido, apagando a anotação ou removendo a entrada do contentor /Blocks, e o bit 2 (valor 2) estica o conteúdo de imagem e PDF até ao retângulo exato em vez de o ajustar

function FillCertificate(const TemplateFile, SealFile, SignatureFile,
  OutFile: WideString; Values: TStrings): Boolean;
var
  Lib: TPDFlib;
  SealID, SignatureID, I: Integer;
  BlockName, BlockType: WideString;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SealFile, '') <> 1 then Exit;
    SealID := Lib.SelectedDocument;          // o ID do documento real
    if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
    // o template é agora o documento selecionado
    SignatureID := Lib.AddImageFromFile(SignatureFile, 0);

    // caminhar para trás: o bit 1 remove cada bloco, o que renumera os seguintes
    for I := Lib.GetBlockCount(1) downto 1 do
    begin
      BlockName := Lib.GetBlockName(1, I);
      BlockType := Lib.GetBlockType(1, I);
      if BlockType = 'text' then
        Lib.FillTextBlock(1, I, Values.Values[BlockName], 1)
      else if (BlockType = 'image') and (SignatureID <> 0) then
        Lib.FillImageBlock(1, I, SignatureID, 1)
      else if BlockType = 'pdf' then
        Lib.FillPDFBlock(1, I, SealID, 1, 1);
    end;
    Result := Lib.SaveToFile(OutFile) = 1;
  finally
    Lib.Free;
  end;
end;

Dois detalhes nesse ciclo são fáceis de errar. O ciclo corre do índice mais alto para baixo porque remover o bloco 2 transforma o antigo bloco 3 no bloco 2; contar para cima com o bit 1 ativo salta blocos alternados. E o documento de origem para o FillPDFBlock é carregado primeiro e o seu ID capturado antes de o template ser carregado, porque os IDs de imagem pertencem ao documento selecionado, pelo que o AddImageFromFile tem de correr enquanto o template está selecionado. Para execuções em lote em que o selo ou o timbrado muda por registo, o costurar de páginas Form XObject e templates de marca de água componíveis no PDFlibPas usa o mesmo mecanismo de capturar e desenhar em páginas inteiras

Por que é que as coordenadas dos blocos saem invertidas?

Os retângulos dos blocos saem invertidos quando o código mistura os dois sistemas de coordenadas com que o PDFlibPas lida. Um /Rect de anotação é guardado no espaço de utilizador PDF, medido a partir do fundo da página (ISO 32000-1 §12.5.2), enquanto a API de desenho TPDFlib mede a partir do topo. O AddBlock recebe entrada baseada no topo e guarda a aresta inferior como PageHeight - Top - Height; o GetBlockRect e as três funções de preenchimento leem o retângulo de volta e convertem-no com PageHeight - URy

Na prática isso significa: use o GetBlockRect(Page, BlockIndex, Left, Top, Width, Height) quando precisar da geometria do espaço, por exemplo para desenhar uma moldura de depuração ou posicionar uma legenda ao lado de um bloco, e não leia o /Rect através de uma chamada genérica de anotação para o alimentar diretamente ao DrawText. Um bloco page-piece com /Rect [40 700 240 730] numa página letter de 792 pt reporta Top = 62, não 700. O bug que isto previne é o clássico em que todos os valores preenchidos aterram espelhados perto da margem inferior e nada dá erro

Inversão de coordenadas do retângulo de bloco no PDFlibPas para Delphi: os valores /Rect vivem no espaço de utilizador PDF medido a partir do fundo enquanto o AddBlock recebe entrada medida do topo e guarda PageHeight menos Top menos Height, pelo que /Rect 40 700 240 730 numa página letter de 792 pt lê-se de volta como Top 62 através do GetBlockRect e da sua conversão PageHeight menos URy
Misturar os dois sistemas de coordenadas espelha todos os valores preenchidos para a margem inferior sem um único erro; leia a geometria do espaço através do GetBlockRect em vez de alimentar o /Rect diretamente ao DrawText

Armadilhas de que vale a pena aprender

O bug mais instrutivo no código dos blocos foi um deslize de um só carácter. As anotações de bloco são reconhecidas por um teste de prefixo no subtipo, e 'PDFlibBlock:' tem 12 caracteres. Uma versão inicial comparava Copy(Subtype, 1, 13) com esse literal de 12 caracteres, que nunca pode ser igual, pelo que o GetBlockCount devolvia 0 para todos os templates e todos os preenchimentos reportavam "no such block". Nada rebentava e nada ficava registado. A lição generaliza a qualquer motor de templates: afirme a contagem de blocos de um template conhecido num teste, porque uma enumeração silenciosamente sem correspondências parece exatamente uma página vazia

A segunda armadilha são os IDs de documento. O TPDFlib.LoadFromFile devolve um código de estado, 1 para sucesso e 0 para falha, não o ID do documento que acabou de carregar. Passar esse 1 como SourceDocumentID ao FillPDFBlock falha porque 1 não é um ID de documento válido. Chame o SelectedDocument imediatamente após um carregamento bem-sucedido para obter o ID real, e o SelectDocument para voltar ao template se carregou a origem depois

Um menor: o FillTextBlock com uma string vazia não faz nada e mantém o bloco. Para um mail merge com campos opcionais, como uma segunda linha de endereço em falta, isso é normalmente o que se quer, mas a anotação por preencher fica depois no ficheiro de saída. Remova-a explicitamente com o DeleteAnnotation ou preencha um único espaço com o bit 1 se a saída tiver de ficar limpa. Todas as chamadas de bloco guardam e restauram a página selecionada, pelo que um preenchimento na página 3 não muda para onde vai a sua próxima chamada de desenho

Quais são os limites de interoperabilidade das anotações de bloco?

As anotações de bloco são uma convenção privada, não uma funcionalidade padrão do PDF. O PDFlibBlock:Text e os seus irmãos não estão entre os tipos de anotação listados na ISO 32000-1 §12.5.6.1, pelo que um visualizador os trata como anotações desconhecidas. Como a flag Invisible está desligada e não há fluxo de aparência, visualizadores conformes não desenham nada para elas, mas alguns visualizadores podem ainda listá-las num painel de comentários, e outras ferramentas PDF não compreenderão nem preservarão a sua semântica

  • Preencha com o bit 1 ativo tudo o que sai do seu sistema, para que o ficheiro final contenha apenas conteúdo de página ordinário
  • O PDF/A não permite tipos de anotação fora da especificação PDF (ISO 19005-1 §6.5.2), pelo que uma saída arquivada não pode transportar anotações de bloco por preencher
  • Os blocos page-piece são mais seguros para manter num template, já que o /PieceInfo é padrão e os consumidores ignoram dados que não lhes pertencem
  • Os blocos não são campos de formulário: não há interatividade, nenhum valor de campo no ficheiro e nenhuma exportação para FDF ou XFDF; para dados que têm de permanecer editáveis veja a troca de dados de formulário FDF, XFDF e XFA no PDFlibPas

Se o template tiver de declarar os seus campos variáveis de forma normalizada e verificável por validadores num fluxo de impressão, os blocos são a ferramenta errada. É para isso que serve o PDF/VCR-1, e o manifesto de marcadores de template PDF/VCR-1 no PDFlibPas cobre como esses marcadores vivem na árvore de estrutura em vez de em anotações

FAQ

O PDFlibPas consegue preencher templates de blocos criados por outras ferramentas?

Sim, quando a ferramenta guarda blocos sob /PieceInfo /PDFlib /Blocks ou /PieceInfo /PDFlib /Private /Blocks, em forma de dicionário ou matriz, com um /Rect de quatro números. O GetBlockCount conta esses depois de quaisquer anotações PDFlibBlock:*, e as chaves personalizadas são expostas através do GetBlockProperty

O FillTextBlock muda a fonte que selecionei antes da chamada?

Não. O FillTextBlock guarda a fonte, o tamanho do texto, a cor do texto e o alinhamento selecionados, aplica as propriedades fontname, fontsize, fillcolor e alignment do bloco, e restaura o estado original depois, mesmo que o ajuste falhe

Como coloco um logótipo vetorial ou uma página de código de barras num bloco?

Carregue o PDF de origem, guarde o SelectedDocument como o seu ID, selecione de novo o documento template se necessário, e chame o FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1). A página é importada como um Form XObject, pelo que permanece vetorial e é ajustada ao bloco com o rácio de aspeto preservado, a menos que acrescente o bit 2

Templates de blocos, importação de páginas Form XObject e o resto do kit de dados variáveis viajam no PDFlibPas, a losLab PDF Library para Delphi, que cobre criação, edição e produção de impressão PDF para projetos Delphi e C++Builder