Inserir uma marca d'água ou logotipo em cada página de um documento parece uma tarefa de cinco minutos, até você abrir o resultado em um inspetor de tamanho de arquivo. A abordagem óbvia é percorrer as páginas e, em cada uma, construir os mesmos objetos de texto ou imagem novamente. Isso funciona visualmente, mas é ineficiente de uma forma cumulativa. Uma marca d'água "DRAFT" diagonal desenhada diretamente em um relatório de cem páginas representa cem cópias dos mesmos dados de caminho e texto nos fluxos de conteúdo, e o arquivo salvo carrega cada uma delas.
Um Form XObject é a estrutura que o PDF fornece para evitar exatamente isso. Ele agrupa um conteúdo reutilizável, uma página inteira ou um pequeno modelo, em um único objeto nomeado que pode ser desenhado várias vezes em várias posições. O conteúdo reside no arquivo apenas uma vez. Cada página que precisa do carimbo contém uma instrução curta que diz "desenhe o XObject N aqui, com esta transformação." Uma marca d'água em cem páginas adiciona um único objeto de conteúdo ao arquivo em vez de cem, e essa é a diferença entre um documento que cresce linearmente com a contagem de páginas e um que não cresce. Marcas d'água, carimbos de logotipo, modelos de numeração de página e selos representam o mesmo tipo de problema, e o Form XObject é a ferramenta correta para cada um deles.
Por que um único objeto armazenado supera cem redesenhos
A economia é estrutural, não cosmética. Uma página em PDF é renderizada executando seu fluxo de conteúdo, uma sequência de operadores de desenho. Quando você redesenha um carimbo por página, você adiciona a sequência completa de operadores daquele carimbo ao fluxo de cada página, e os bytes são duplicados tantas vezes quantas forem as páginas. Um Form XObject move esses operadores para um fluxo armazenado uma única vez no documento. A referência que uma página individual mantém é pequena: ela define uma matriz de transformação, invoca o XObject e restaura o estado. A contagem de páginas não multiplica mais o custo dos elementos visuais.
Isso é ainda mais importante quando o carimbo é pesado. Um selo vetorial com centenas de segmentos de caminho, ou um bitmap de logotipo, é caro para armazenar. Armazenado uma vez e referenciado, a parte pesada é paga uma única vez e o custo por página resume-se a alguns bytes de invocação. O resultado visual na página é idêntico a um redesenho direto, que é o objetivo. O leitor não percebe a diferença; o tamanho do arquivo sim.
Capturando uma página em um XObject
O PDFium constrói o objeto reutilizável a partir de uma página existente. A origem é uma página de algum documento que você tem aberto, um pequeno PDF de uma página que contém apenas sua arte de marca d'água ou uma página específica de um arquivo maior. CreateXObjectFromPage captura o conteúdo dessa página de origem em um manipulador reutilizável que pertence ao documento de destino, aquele que você está carimbando.
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
A assinatura é CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. O método retorna nil em caso de falha em vez de gerar uma exceção, portanto, a verificação explícita acima não é opcional. O manipulador retornado é um TPdfXObject de sua propriedade, e as duas restrições de tempo de vida associadas a ele são a parte de todo esse processo que costuma causar erros, por isso têm uma seção própria abaixo.
Posicionando o carimbo em uma página
Um XObject capturado não faz nada por si só. Para fazê-lo aparecer, você insere uma cópia dele na página atual do documento com InsertFormObjectFromXObject. Essa chamada retorna o objeto de página subjacente, um FPDF_PAGEOBJECT, e o manipulador retornado é como você define o posicionamento. Sem uma transformação, o carimbo é colocado na origem das coordenadas da própria página de origem, o que raramente é o desejado.
Como InsertFormObjectFromXObject insere uma cópia por chamada e retorna um novo objeto de página a cada vez, você pode desenhar o mesmo XObject várias vezes em uma página com diferentes transformações, e o conteúdo armazenado ainda será contado apenas uma vez no arquivo. Um logotipo de canto e uma marca d'água suave em tela cheia podem vir do mesmo objeto capturado.
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
A regra de tempo de vida de manipulador que costuma causar problemas
Duas restrições regem o manipulador do XObject, e ignorar qualquer uma delas produz uma falha que parece não ter relação com a causa. Primeiro, o documento de origem deve estar ativo no momento em que você chama CreateXObjectFromPage. A captura lê o conteúdo da página de origem a partir do documento de origem ativo, portanto, esse documento e sua página devem estar abertos e válidos quando o manipulador for construído. Segundo, e esta é a parte que surpreende as pessoas, o manipulador deve ser liberado antes que a página de origem seja fechada e, na prática, antes de você fechar ou liberar o documento de origem de onde ele veio.
O motivo é que o XObject é uma referência à estrutura que o documento de origem ainda possui. Ele não é uma cópia independente e isolada que você pode carregar depois que a origem for removida. Feche a origem primeiro e o manipulador ficará apontando para um conteúdo que já foi destruído, de modo que liberá-lo mais tarde, ou qualquer outro uso, operará em memória não mais válida. O sintoma é o clássico de um manipulador pendente: uma violação de acesso no encerramento ou corrupção intermitente que se desloca dependendo da ordem de alocação, com uma pilha que aponta para o código de limpeza em vez de apontar para a linha que realmente causou o problema. A correção é a ordenação, e não codificação defensiva. Construa o XObject, insira-o em cada página que precisar dele, libere o XObject e somente então feche o documento de origem. O destrutor do TPdfXObject libera o manipulador do PDFium subjacente para você, de modo que liberar o wrapper no momento correto é toda a sua responsabilidade.
A matriz, e o que significam seus seis números
O posicionamento é uma transformação afim 2D, a mesma que o PDF usa em todos os lugares para posicionar conteúdo (ISO 32000-1, seção 8.3.4). São seis números, escritos como a, b, c, d, e, f, e o PDFium os expõe como o registro FS_MATRIX. Eles mapeiam um ponto do espaço do próprio objeto para o espaço da página:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
Você pode preencher esses seis valores manualmente, mas compô-los à mão é onde a rotação costuma dar errado, porque ela mistura os quatro valores a, b, c, d. O wrapper TPdfMatrix compõe as operações comuns para você e faz a multiplicação posterior conforme prossegue, de modo que Translate, Scale e Rotate se encadeiam na ordem em que você os chama. Uma marca d'água diagonal é uma rotação seguida por uma translação para centralizar; um logotipo de canto é uma escala seguida por uma translação. Quando a matriz estiver pronta, passe seu valor bruto para FPDFPageObj_SetMatrix(PageObj, M.Handle), onde M.Handle é a FS_MATRIX subjacente. A função de nível inferior FPDFPageObj_Transform, que recebe os seis valores diretamente como doubles, está disponível quando você preferir passar números em vez de criar um wrapper.
Carimbando cada página, na ordem correta
O padrão completo reúne as peças com a ordenação exigida pela regra de tempo de vida. Abra ambos os documentos, capture o carimbo uma vez, percorra as páginas de destino selecionando cada uma por vez e inserindo mais posicionando uma cópia, depois libere o XObject, salve e deixe o documento de origem fechar por último.
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
A estrutura dos blocos try faz o trabalho real. O finally interno libera o XObject antes que a execução possa alcançar o finally externo que libera Stamp, de modo que o manipulador é sempre liberado enquanto sua origem ainda está ativa, mesmo se uma exceção ocorrer no meio do loop. Ajuste esse aninhamento e a regra de tempo de vida se resolverá por si só. (Use o seletor de página atual que sua compilação expõe; o corpo do loop é o mesmo de qualquer maneira.)
O carimbo é apenas uma parte de um conjunto maior de ferramentas para construir e editar conteúdo de página. Se o seu carimbo for uma imagem em si, em vez de uma página capturada, o artigo sobre como converter imagens em documentos PDF com PDFium aborda como inserir esse bitmap em um documento primeiro. E quando o que você deseja carregar ao lado do carimbo visível for um arquivo em vez de tinta na página, o texto sobre como trabalhar com anexos em PDF no Delphi mostra o lado dos arquivos anexados. Tudo isso é fornecido com o PDFium Component para Delphi e C++Builder, junto com as APIs de renderização, edição e documentos abordadas em outras partes deste blog.