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
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(oufont): primeiro reutiliza uma fonte já carregada no documento com esse nome, depois mapeia os nomes Standard 14 comoHelvetica-Bold,Times-RomanouCourier, e por fim tenta oAddTrueTypeFontcom o nomefontsize(ousize): o tamanho do texto passado ao ajustador, que o encolhe se o texto não couberfillcolor(outextcolor,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–255alignment,alignepositionsã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 verticalmentefitmethod:wordounofitsplitimpede que palavras compridas sejam partidas,scaleouexpanddeixa 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
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
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