O PDFlibPas (losLab PDF Library) preenche templates de blocos PDF a partir do Delphi: um bloco é um retângulo nomeado em uma página de template, tipado como texto, imagem ou PDF, e FillTextBlock, FillImageBlock e FillPDFBlock desenham dados variáveis dentro dele. Os blocos são encontrados como anotações de página PDFlibBlock:Text, PDFlibBlock:Image e PDFlibBlock:PDF, ou em um dicionário page-piece sob /PieceInfo
O caso de uso é do tipo chato e de alto volume. Um designer monta um certificado, um ingresso de evento ou uma carta de cobrança uma única vez, e o programa carimba um nome, um número de assento, uma imagem de assinatura ou uma página de barcode pré-renderizada em slots fixos para dez mil registros. Form fields fazem parte disso, mas um campo AcroForm é um objeto interativo com seu próprio appearance stream e suas próprias regras, e você normalmente acaba achatando-o de qualquer forma. Um bloco é mais simples: um retângulo mais um punhado de dicas de estilo, consumido uma vez e depois jogado 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 de placeholder, e cuja string /Contents guarda name;key=value;key=value. O primeiro segmento é o nome do bloco, tudo depois dele é uma lista de propriedades. O TPDFlib.AddBlock grava a anotação com /F 4 (Print) e sem appearance stream, então um bloco não preenchido não ocupa nenhum pixel na tela 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 ... >> >> >>. 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 é ali que as definições de blocos de ferramentas externas de autoria de templates costumam viver. O contêiner /Blocks pode ser um dicionário indexado por nome de bloco ou um array simples; na forma de array o PDFlibPas nomeia as entradas Block1, Block2 e assim por diante, a menos que um bloco carregue seu próprio /Name
Dentro de um bloco page-piece, o valor de /Subtype (ou /Type) Image mapeia para um bloco de imagem, PDF ou Page mapeia para um bloco PDF, e qualquer outra coisa é tratada como texto. Toda outra chave exceto /Type, /Subtype, /Name, /Rect e /Custom vira uma propriedade, e as chaves de um dicionário /Custom aninhado também são achatadas, então uma entrada /Custom << /department (Finance) >> pode ser lida como GetBlockProperty(Page, Index, 'department'). Os índices de bloco são base 1 por página, blocos de anotação primeiro, blocos page-piece depois
Criando um template de blocos com AddBlock
TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) adiciona um bloco à página atualmente selecionada e devolve 1 em caso de sucesso. Devolve 0 para nome vazio, largura ou altura zero ou negativa, ou um BlockType diferente de 'text', 'image' ou 'pdf' (sem diferenciar maiúsculas). Properties é anexado ao nome com ponto e vírgula, então usa a mesma sintaxe key=value;key=value da string armazenada
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
// arte estática: borda, logo, textos fixos ...
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 parser de propriedades é propositalmente frouxo. Chaves são comparadas sem diferenciar maiúsculas, valores são aparados mas devolvidos verbatim (um valor escrito como {top left} volta com as chaves), e uma chave ausente devolve string vazia. O único limite duro é o separador: um valor não pode conter ponto e vírgula, porque o ponto e vírgula encerra o segmento
Como o FillTextBlock aplica 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 FitTextBox e então restaura a fonte, o tamanho de texto, a cor de texto e o alinhamento do chamador em um bloco finally. Essa restauração importa em um loop de mala direta: um bloco de título vermelho de 30 pt não pode vazar seu estilo para a próxima chamada de DrawText que você fizer na mesma página
fontname(oufont): primeiro reusa uma fonte já carregada no documento com esse nome, depois mapeia os nomes Standard 14 comoHelvetica-Bold,Times-RomanouCourier, e por fim tentaAddTrueTypeFontcom o nomefontsize(ousize): o tamanho de 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 juntos: left, right, center (ou centre) e justify definem o alinhamento horizontal, top e bottom definem o posicionamento vertical, e o padrão é centralizado verticalmentefitmethod:wordounofitsplitimpede que palavras longas sejam quebradas,scaleouexpanddeixa o tamanho crescer para preencher a caixa
O caminho CMYK é a razão pela qual gráficas se importam. {cmyk 0 0.55 1 0.1} chama SetTextColorCMYK, então o texto variável é emitido em DeviceCMYK e acerta a cor da marca na impressão sem uma ida e volta por RGB. O FillTextBlock devolve 1 quando o texto foi desenhado; devolve 0 para um Text vazio, um bloco ausente ou um bloco que não é de texto, e em todos esses casos o bloco fica no lugar
Preenchendo blocos de imagem e PDF, e os bits de Options
FillImageBlock(Page, BlockIndex, ImageID, Options) desenha uma imagem adicionada anteriormente, e FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importa uma página de outro documento carregado como um Form XObject e a desenha, o que mantém conteúdo vetorial como um selo, uma página de QR code ou um timbrado integralmente vetorial. Ambos preservam a proporção da origem e centralizam o resultado dentro do retângulo do bloco. As três funções de preenchimento compartilham os mesmos bits de Options: o bit 1 (valor 1) remove o bloco após um preenchimento bem-sucedido, seja apagando a anotação ou removendo a entrada do contêiner /Blocks, e o bit 2 (valor 2) estica o conteúdo de imagem e PDF até o retângulo exato em vez de ajustá-lo
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 real do documento
if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
// o template agora é o documento selecionado
SignatureID := Lib.AddImageFromFile(SignatureFile, 0);
// ande para trás: o bit 1 remove cada bloco, o que renumera os posteriores
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 desse loop são fáceis de errar. O loop roda 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 ligado pula um bloco sim, outro não. E o documento de origem do FillPDFBlock é carregado primeiro e tem seu ID capturado antes de o template ser carregado, porque os IDs de imagem pertencem ao documento selecionado, então o AddImageFromFile precisa rodar enquanto o template está selecionado. Para lotes em que o selo ou o timbrado muda por registro, o emendamento de páginas com Form XObject e templates de watermark compostáveis no PDFlibPas usa o mesmo mecanismo de capturar e desenhar em páginas inteiras
Por que as coordenadas dos blocos saem invertidas?
Retângulos de bloco saem invertidos quando o código mistura os dois sistemas de coordenadas com que o PDFlibPas lida. Um /Rect de anotação é armazenado no espaço de usuário do PDF, medido da base da página (ISO 32000-1 §12.5.2), enquanto a API de desenho do TPDFlib mede do topo. O AddBlock recebe entrada baseada no topo e armazena a borda inferior como PageHeight - Top - Height; o GetBlockRect e as três funções de preenchimento leem o retângulo de volta e o convertem com PageHeight - URy
Na prática isso significa: use GetBlockRect(Page, BlockIndex, Left, Top, Width, Height) quando precisar da geometria do slot, por exemplo para desenhar um quadro de debug ou posicionar uma legenda ao lado de um bloco, e não leia o /Rect por uma chamada genérica de anotação para alimentá-lo direto no DrawText. Um bloco page-piece com /Rect [40 700 240 730] em uma página letter de 792 pt reporta Top = 62, não 700. O bug que isso previne é o clássico em que todo valor preenchido cai espelhado perto da margem inferior e nada dá erro
Armadilhas das quais vale aprender
O bug mais instrutivo do código de blocos foi um deslize de um caractere. As anotações de bloco são reconhecidas por um teste de prefixo no subtype, e 'PDFlibBlock:' tem 12 caracteres. Uma versão inicial comparava Copy(Subtype, 1, 13) com aquele literal de 12 caracteres, que nunca pode ser igual, então o GetBlockCount devolvia 0 para todo template e todo preenchimento reportava "no such block". Nada quebrou e nada foi logado. A lição se generaliza para qualquer engine de template: afirme a contagem de blocos de um template conhecido em um teste, porque uma enumeração silenciosa de zero resultados se parece exatamente com uma página vazia
A segunda armadilha são os IDs de documento. O TPDFlib.LoadFromFile devolve um código de status, 1 para sucesso e 0 para falha, não o ID do documento que acabou de carregar. Passar esse 1 como SourceDocumentID para o FillPDFBlock falha porque 1 não é um ID de documento válido. Chame SelectedDocument logo após um carregamento bem-sucedido para obter o ID real, e o SelectDocument para voltar ao template se você carregou a origem depois
Uma menor: o FillTextBlock com string vazia não faz nada e mantém o bloco. Para uma mala direta com campos opcionais, como uma segunda linha de endereço ausente, normalmente é isso que você quer, mas a anotação não preenchida então fica no arquivo de saída. Remova-a explicitamente com DeleteAnnotation ou preencha um único espaço com o bit 1 se a saída precisa ficar limpa. Todas as chamadas de bloco salvam e restauram a página selecionada, então um preenchimento na página 3 não muda para onde vai sua próxima chamada de desenho
Quais são os limites de interoperabilidade das anotações de bloco?
Anotações de bloco são uma convenção privada, não um recurso padrão do PDF. PDFlibBlock:Text e seus irmãos não estão entre os tipos de anotação listados na ISO 32000-1 §12.5.6.1, então um viewer as trata como anotações desconhecidas. Como a flag Invisible está limpa e não há appearance stream, viewers conformes não desenham nada para elas, mas alguns viewers podem ainda listá-las em um painel de comentários, e outras ferramentas PDF não entenderão nem preservarão sua semântica
- Preencha com o bit 1 ligado para qualquer coisa que saia do seu sistema, para o arquivo final conter apenas conteúdo de página comum
- PDF/A não permite tipos de anotação fora da especificação PDF (ISO 19005-1 §6.5.2), então uma saída arquivada não deve carregar anotações de bloco não preenchidas
- Blocos page-piece são mais seguros de manter em um template, já que
/PieceInfoé padrão e consumidores ignoram dados que não lhes pertencem - Blocos não são form fields: não há interatividade, nenhum valor de campo no arquivo e nenhuma exportação para FDF ou XFDF; para dados que precisam continuar editáveis veja intercâmbio de dados de formulário FDF, XFDF e XFA no PDFlibPas
Se o template precisa declarar seus campos variáveis de forma padronizada e verificável por validador para um fluxo de impressão, blocos são a ferramenta errada. É para isso que existe o PDF/VCR-1, e o manifesto de placeholders de template PDF/VCR-1 no PDFlibPas cobre como esses placeholders vivem na structure tree em vez de em anotações
FAQ
O PDFlibPas preenche templates de blocos criados por outras ferramentas?
Sim, quando a ferramenta armazena os blocos sob /PieceInfo /PDFlib /Blocks ou /PieceInfo /PDFlib /Private /Blocks, em forma de dicionário ou array, com um /Rect de quatro números. O GetBlockCount conta esses depois de quaisquer anotações PDFlibBlock:*, e chaves customizadas são expostas pelo GetBlockProperty
O FillTextBlock muda a fonte que eu selecionei antes da chamada?
Não. O FillTextBlock salva a fonte, o tamanho de texto, a cor de texto e o alinhamento selecionados, aplica as propriedades fontname, fontsize, fillcolor e alignment do bloco, e restaura o estado original depois, mesmo se o ajuste falhar
Como colocar um logo vetorial ou uma página de código de barras em um bloco?
Carregue o PDF de origem, guarde o SelectedDocument como seu ID, selecione o documento do template de novo se necessário, e chame FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1). A página é importada como um Form XObject, então permanece vetorial e é ajustada ao bloco com a proporção preservada, a menos que você adicione o bit 2
Templates de blocos, importação de páginas como Form XObject e o resto do toolkit de dados variáveis vêm no PDFlibPas, a losLab PDF Library for Delphi, que cobre criação, edição e produção de impressão de PDF para projetos Delphi e C++Builder