Aplicar uma marca de água ou um logótipo em todas as páginas de um documento assemelha-se a uma tarefa de cinco minutos até abrir o resultado num inspetor de tamanho de ficheiro. A abordagem óbvia consiste em percorrer as páginas e, em cada uma delas, construir novamente os mesmos objetos de texto ou de imagem. Isso funciona a nível visual, mas representa um desperdício de recursos cumulativo. Uma marca de água "RASCUNHO" na diagonal desenhada diretamente num relatório de cem páginas traduz-se em cem cópias dos mesmos dados de caminho e texto nos fluxos de conteúdo, fazendo com que o ficheiro guardado carregue todas elas.
Um Form XObject é a estrutura fornecida pelo PDF para evitar precisamente este problema. Envolve uma secção de conteúdo reutilizável, uma página inteira ou um pequeno modelo, num único objeto nomeado que pode ser desenhado várias vezes em diversas posições. O conteúdo reside no ficheiro uma única vez. Cada página que requeira o carimbo contém uma instrução curta que indica: "desenhar o XObject N aqui, com esta transformação." Uma marca de água em cem páginas adiciona assim um único objeto de conteúdo ao ficheiro em vez de cem, sendo essa a diferença entre um documento cujo tamanho cresce linearmente com a contagem de páginas e um que não o faz. Marcas de água, carimbos de logótipos, modelos de paginação e selos representam todos o mesmo tipo de problema, e o Form XObject é a ferramenta indicada para cada um deles.
Por que razão um objeto guardado supera cem desenhos diretos
A poupança é de ordem estrutural, e não apenas estética. Uma página PDF é renderizada executando o seu fluxo de conteúdo, que consiste numa sequência de operadores de desenho. Quando volta a desenhar um carimbo por página, está a anexar toda a sequência de operadores desse carimbo ao fluxo de cada página, duplicando os bytes tantas vezes quantas as páginas existentes. Um Form XObject move esses operadores para um fluxo único guardado uma vez no documento. A referência mantida por cada página especial é reduzida: introduz uma matriz de transformação, invoca o XObject e repõe o estado. A contagem de páginas deixa de multiplicar o custo do elemento gráfico.
Isto assume particular importância quando o carimbo é pesado. Um selo vetorial com centenas de segmentos de caminho, ou um bitmap de logótipo, acarreta custos elevados de armazenamento. Ao guardá-lo uma vez e referenciá-lo, o elemento pesado é pago uma única vez, passando o acréscimo por página a resumir-se a escassos bytes de invocação. O resultado visual na página é idêntico ao de um desenho direto, o que cumpre o objetivo. O leitor não nota a diferença; o tamanho do ficheiro, pelo contrário, nota-a perfeitamente.
Capturar uma página num XObject
O PDFium cria o objeto reutilizável a partir de uma página existente. A origem pode ser uma página de um documento que tenha aberto, um pequeno PDF de uma página que contenha apenas a arte da sua marca de água, ou uma página específica de um ficheiro maior. O método CreateXObjectFromPage captura o conteúdo dessa página de origem num identificador (handle) reutilizável pertencente ao documento de destino que está a carimbar.
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 devolve nil em caso de falha em vez de gerar uma exceção, pelo que a validação explícita efetuada acima é obrigatória. O identificador devolvido é um TPdfXObject de que é proprietário, e as duas restrições de tempo de vida a ele associadas constituem a etapa deste processo que costuma induzir em erro, pelo que dispõem de uma secção própria abaixo.
Posicionar o carimbo numa página
Um XObject capturado não executa nada por si só. Para o tornar visível, insere uma cópia do mesmo na página atual do documento com InsertFormObjectFromXObject. Essa chamada devolve o objeto de página subjacente, um FPDF_PAGEOBJECT, sendo o identificador devolvido o elemento utilizado para controlar o posicionamento. Sem uma transformação, o carimbo posiciona-se na origem das coordenadas da própria página de origem, o que raramente coincide com a posição pretendida.
Como o InsertFormObjectFromXObject insere uma cópia por chamada e disponibiliza um objeto de página novo de cada vez, pode desenhar o mesmo XObject múltiplas vezes numa página com transformações diferentes, mantendo-se o conteúdo guardado contabilizado apenas uma vez no ficheiro. Um logótipo num canto e uma marca de água suave em toda a página podem derivar 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;
Um detalhe de propriedade garante a segurança da limpeza. Após ser inserido, o objeto de página pertence à página e não ao XObject. Libertar o XObject mais tarde não invalida os posicionamentos que já efetuou. É isto que permite o funcionamento da sequência criar-posicionar-libertar descrita abaixo.
A regra de tempo de vida do identificador que gera erros
Duas restrições controlam o identificador do XObject, e ignorar qualquer uma delas gera uma falha cuja causa aparenta não ter relação. Primeiro, o documento de origem deve estar ativo no momento em que chama CreateXObjectFromPage. A captura lê o conteúdo da página de origem a partir do documento de origem ativo, pelo que esse documento e a respetiva página devem estar abertos e válidos quando o identificador é criado. Segundo, e este é o aspeto que costuma surpreender, o identificador tem de ser libertado antes de fechar a página de origem e, na prática, antes de fechar ou libertar o documento de origem de onde proveio.
A razão prende-se com o facto de o XObject constituir uma referência a uma estrutura de que o documento de origem ainda é proprietário. Não se trata de uma cópia autónoma e desvinculada que possa transportar após a eliminação da origem. Se fechar a origem primeiro, o identificador fica a apontar para conteúdo que já foi destruído, pelo que libertá-lo mais tarde, ou qualquer outra utilização, operará sobre memória que já não é válida. O sintoma é o clássico de um identificador pendente (dangling handle): uma violação de acesso (access violation) no encerramento ou corrupção intermitente que varia consoante a ordem de alocação, com uma pilha (stack) que aponta para o código de limpeza e não para a linha que de facto causou o problema. A solução reside na ordenação e não em programação defensiva. Crie o XObject, insira-o em todas as páginas que dele necessitem, liberte o XObject e, apenas depois, feche o documento de origem. O destrutor do TPdfXObject liberta o identificador PDFium subjacente por si, pelo que libertar o invólucro (wrapper) no momento correto constitui toda a sua responsabilidade.
A matriz e o significado dos seus seis números
O posicionamento traduz-se numa transformação afim 2D, a mesma que o PDF utiliza em toda a parte para posicionar conteúdos (ISO 32000-1, secção 8.3.4). É composta por seis números, escritos como a, b, c, d, e, f, e o PDFium expõe-nos sob a forma de registo FS_MATRIX. Mapeiam um ponto do espaço próprio do 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)
Pode definir esses seis valores manualmente, mas compô-los à mão é o ponto onde a rotação costuma falhar, porque esta mistura os quatro valores a, b, c, d em simultâneo. O wrapper TPdfMatrix compõe as operações comuns por si e aplica a pós-multiplicação à medida que avança, pelo que Translate, Scale e Rotate encadeiam-se na ordem em que os chama. Uma marca de água na diagonal consiste numa rotação seguida de uma translação para recenteamento; um logótipo no canto consiste numa escala seguida de uma translação. Quando a matriz estiver concluída, passe o seu valor direto para FPDFPageObj_SetMatrix(PageObj, M.Handle), onde M.Handle representa a estrutura FS_MATRIX subjacente. A função de nível inferior FPDFPageObj_Transform, que aceita os seis valores diretamente como doubles, está disponível caso prefira passar números em vez de construir um wrapper.
Carimbar todas as páginas na ordem correta
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;
O desenho dos blocos try executa o verdadeiro trabalho estrutural. O finally interno liberta o XObject antes que o fluxo de execução alcance o finally externo que liberta o Stamp, garantindo que o identificador é sempre libertado enquanto a sua origem está ativa, mesmo que surja uma exceção a meio do ciclo. Configure esse aninhamento corretamente e a regra do tempo de vida resolver-se-á por si própria. (Utilize o seletor de página atual fornecido pela sua versão; o corpo do ciclo mantém-se idêntico.)
O carimbo é um aspeto de um conjunto mais amplo de ferramentas para criar e editar conteúdos de páginas. Se o seu carimbo for uma imagem em si, em vez de uma página capturada, converter imagens em documentos PDF com o PDFium descreve como introduzir esse bitmap num documento em primeiro lugar. E quando o elemento que deseja associar é um ficheiro e não tinta na página, o artigo trabalhar com anexos PDF em Delphi descreve a incorporação de ficheiros. Tudo isto é disponibilizado com o PDFium Component para Delphi e C++Builder, a par das APIs de renderização, edição e documentos abordadas noutros locais deste blogue.