Carimbar uma marca d'água ou um logotipo em todas as páginas de um documento parece ser um trabalho de cinco minutos, até que você abra o resultado em um inspetor de tamanho de arquivo. A abordagem óbvia é percorrer as páginas e, em cada uma delas, construir novamente os mesmos objetos de texto ou imagem. Isso funciona visualmente e é um desperdício de uma forma cumulativa. Uma marca d'água "RASCUNHO" na diagonal desenhada diretamente em um relatório de cem páginas significa cem cópias dos mesmos dados de caminho e texto assentados nos fluxos de conteúdo, e o arquivo salvo carrega todos eles
Um Form XObject é a estrutura que o PDF fornece para evitar exatamente isso. Ele agrupa um pedaço de conteúdo reutilizável, uma página inteira ou um pequeno modelo, em um único objeto nomeado que pode ser pintado diversas vezes em diversas posições. O conteúdo reside no arquivo apenas uma vez. Cada página que deseja o carimbo guarda uma instrução curta que diz "pinte o XObject N aqui, com esta transformação." Uma marca d'água em cem páginas adiciona então apenas um objeto de conteúdo ao arquivo em vez de cem, e essa é a diferença entre um documento que cresce linearmente com sua contagem de páginas e um que não cresce. Marcas d'água, carimbos de logotipo, modelos de números de página e selos representam todos o mesmo tipo de problema, e o Form XObject é a ferramenta certa para cada um deles
Por que um objeto armazenado supera cem redesenhos
A economia é estrutural, não estética. Uma página em PDF é renderizada pela execução do seu fluxo de conteúdo, uma sequência de operadores de desenho. Quando você redesenha um carimbo por página, você está anexando a sequência completa de operadores daquele carimbo ao fluxo de cada página, e os bytes são duplicados tantas vezes quanto você tiver páginas. Um Form XObject move esses operadores para um fluxo armazenado apenas uma vez no documento. A referência que uma página individual mantém é pequena: ela envia uma matriz de transformação, invoca o XObject e restaura o estado. A contagem de páginas já não multiplica o custo da arte
Isso importa mais quando o carimbo é pesado. Um selo vetorial com centenas de segmentos de caminho ou um bitmap de logotipo é caro para armazenar. Armazenada uma vez e referenciada, a parte pesada é paga apenas uma vez e a sobrecarga por página são alguns bytes de invocação. O resultado visual na página é idêntico a um redesenho direto, que é o ponto principal. O leitor não consegue perceber 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 em algum documento que você abriu, um pequeno PDF de uma página que não contém nada além da 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 num identificador reutilizável que pertence ao documento de destino, aquele que você está carimbando
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := 'Report.pdf';
Dest.Active := True;
Stamp.FileName := 'Watermark.pdf'; // one page of artwork
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// 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 levanta uma exceção se o documento de origem não estiver Active, e retorna nil em vez de levantar exceção quando o PDFium não consegue construir o objeto, portanto, a verificação explícita acima não é opcional. O identificador que volta é um TPdfXObject que você possui, e as duas restrições de vida útil a ele vinculadas são a parte de todo esse exercício que surpreende as pessoas, então elas recebem sua própria seção abaixo
Colocando o carimbo em uma página
Um XObject capturado não faz nada por conta própria. Para fazê-lo aparecer, você insere uma cópia dele na página atual do documento, a página selecionada pela propriedade PageNumber (baseada em 1), usando InsertFormObjectFromXObject. Essa chamada retorna o objeto de página subjacente, um FPDF_PAGEOBJECT, e o identificador retornado é como você posiciona a colocação. Sem uma transformação, o carimbo aterrissa na origem, nas próprias coordenadas da página de origem, o que raramente é onde você o deseja
Como o InsertFormObjectFromXObject insere uma cópia por chamada e devolve um novo objeto de página a cada vez, você pode pintar o mesmo XObject várias vezes em uma página sob diferentes transformações, e o conteúdo armazenado ainda é contado uma vez no arquivo. Um logotipo de canto e uma marca d'água fraca de página inteira podem vir do mesmo objeto capturado
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
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);
RawM := M.Handle;
if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
raise Exception.Create('Cannot assign the stamp matrix');
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits to its content stream
// if not Dest.SaveAs(...) then ... when every page is done.
end;
Dois detalhes de manutenção tornam isso seguro. Primeiro, uma vez inserido, o objeto de página pertence à página e não ao XObject. Liberar o XObject posteriormente não invalida os posicionamentos que você já fez. Isso é o que permite o funcionamento da ordem "criar-posicionar-liberar" descrita abaixo. Segundo, inserir e posicionar muda a lista de objetos da página apenas na memória; UpdatePage é o que serializa essa lista de volta para o fluxo de conteúdo da página, portanto, se você editar uma página e não chamar isso, ela será salva como se o carimbo nunca tivesse sido colocado
A regra de vida útil do identificador que engana as pessoas
Duas restrições controlam o identificador do XObject, e ignorar qualquer uma delas produz uma falha que parece não ter relação com a sua 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 a partir do documento de origem ativo, de forma que o documento e sua página precisam estar abertos e válidos quando o identificador é construído. Segundo — e esta é a que surpreende as pessoas —, o identificador deve ser liberado antes de a página de origem ser fechada e, na prática, antes de você fechar ou liberar o documento de origem de onde ela veio
A razão é que o XObject é uma referência para a estrutura que o documento de origem ainda possui. Não é uma cópia separada e independente que você possa carregar por aí depois que a origem tiver desaparecido. Fechar a origem primeiro deixa o identificador apontando para um conteúdo que foi destruído; portanto, liberá-lo depois, ou qualquer outro uso, opera em memória que não é mais válida. O sintoma é o clássico de um identificador perdido: uma violação de acesso no desligamento (shutdown), ou uma corrupção intermitente que se altera dependendo da ordem de alocação, com uma pilha que aponta para o código de limpeza e não para a linha que realmente causou o problema. A solução é a ordem, e não código defensivo. 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 de TPdfXObject libera o identificador subjacente do PDFium por você, de modo que liberar o invólucro (wrapper) no momento correto é toda a sua responsabilidade
A matriz e o que os seus seis números significam
A colocação é uma transformação afim em 2D, a mesma que o PDF usa em qualquer lugar para posicionar o 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 desde o próprio espaço do objeto até 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 manualmente é onde a rotação dá errado, porque a rotação mistura os quatro valores de a, b, c, d. O invólucro TPdfMatrix, a partir da unidade FPdfMatrix, compõe as operações comuns para você e pós-multiplica à medida que prossegue, para que Translate, Scale e Rotate se encadeiem na ordem que você as invoca. Uma marca d'água diagonal é uma rotação seguida por uma translação para recentralizá-la; um logotipo de canto é uma escala seguida por uma translação. Quando a matriz estiver pronta, copie seu valor bruto, a propriedade Handle do tipo FS_MATRIX, em uma variável local e passe-a para FPDFPageObj_SetMatrix; a importação declara a matriz como um parâmetro var, então uma propriedade não pode ser passada diretamente a ela, e seu resultado é 0 em caso de falha. A rotina de mais baixo nível, FPDFPageObj_Transform, que recebe os seis valores diretamente como doubles, está disponível quando você preferir passar números em vez de construir um wrapper
Carimbando todas as páginas, na ordem certa
O padrão completo reúne as peças com a ordenação que a regra de vida útil exige. Abra ambos os documentos, capture o carimbo uma vez, percorra as páginas de destino definindo, a cada turno, o PageNumber (baseado em 1) e inserindo e posicionando uma cópia; submeta cada página com UpdatePage, em seguida, libere o XObject e por fim salve com SaveAs, deixando que o documento de origem seja o último a se fechar
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
I: Integer;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := ASource;
Dest.Active := True;
Stamp.FileName := AStamp;
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// 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. PageNumber is 1-based.
for I := 1 to Dest.PageCount do
begin
Dest.PageNumber := 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
RawM := M.Handle;
FPDFPageObj_SetMatrix(PageObj, RawM);
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
if not Dest.SaveAs(AOutput) then
raise Exception.Create('Could not save ' + AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
O formato dos blocos try faz o verdadeiro trabalho. O finally mais interno libera o XObject antes de o controle alcançar o finally exterior que libera o Stamp, de modo que o identificador é sempre liberado enquanto sua fonte ainda está viva, mesmo que uma exceção seja disparada no meio do loop. Acerte esse aninhamento e a regra de vida útil cuidará de si mesma
A estampagem (stamping) é uma parte de um conjunto de ferramentas muito mais amplo para construir e editar conteúdo de página. Se o seu carimbo for em si mesmo uma imagem e não uma página capturada, converter imagens para documentos PDF com o PDFium aborda como obter esse bitmap para dentro de um documento primeiramente. E quando o que você deseja carregar junto ao carimbo visível for um arquivo, em vez de tinta na página, trabalhar com anexos em PDF no Delphi mostra a parte do arquivo incorporado. Tudo isso é fornecido com o Componente PDFium para Delphi e C++Builder, junto às APIs de renderização, edição e documentos abordadas noutros locais deste blog