Mover um bloco de campos de formulário do modelo do ano passado para o layout deste ano é o ponto em que as idas e voltas de FDF e XFDF deixam de ser suficientes: os valores chegam, mas as streams de aparência, as ações de cálculo e os recursos predefinidos não. O PDFiumPas responde a esse caso com GraftPdfAcroForm, que clona todo o grafo de objetos de campos de um PDF e grava-o noutro
A razão pela qual uma exportação ao nível dos dados não consegue fazer isto é estrutural. Um campo não é um registo, é um subgrafo. A ISO 32000-1 §12.7 define o dicionário de formulário interativo que contém /Fields, /CO, /DR e /DA, a §12.7.3 define os dicionários de campo pendurados por baixo dele, e a §12.5.6.19 define as anotações widget que dão a esses campos uma caixa visível numa página. O XFDF transporta as folhas dessa estrutura. A enxertia transporta a própria estrutura
Porque copiar o array /Fields nunca chega
Copiar o array /Fields de um documento para outro produz um formulário que se parte de todas as formas interessantes, porque o array contém apenas referências indiretas e mais nada. A ISO 32000-1 §7.3.10 torna um objeto indireto endereçável por número de objeto mais geração, e esses números só têm significado dentro do ficheiro de onde vieram. Colar o array noutro lado faz com que cada referência nele fique pendente ou, pior, resolva silenciosamente para um objeto não relacionado que por acaso ocupa essa posição no destino. Por baixo de cada referência está um grafo ao mesmo tempo partilhado e cíclico. Um dicionário de campo aponta para os seus filhos, cada filho aponta de volta para o seu /Parent, um widget aponta para as suas streams de aparência e, através de /P, para a página que o transporta, as streams de aparência apontam para tipos de letra no dicionário de recursos predefinidos do formulário, e os dicionários de ações adicionais sob /AA apontam para mais objetos. Dois widgets em páginas diferentes partilham frequentemente um tipo de letra e um XObject de aparência. Assim, um enxerto correto tem de percorrer esse grafo, clonar cada objeto alcançável exatamente uma vez, redirecionar o /P de cada widget para a página de destino mapeada e adicionar o widget clonado ao array /Annots dessa página — caso contrário o campo existe no formulário e é invisível na página. Se já perseguiu a diferença entre um campo, o seu widget e a anotação de página que o apresenta, a nossa nota sobre índice de widget versus índice de anotação cobre exatamente essa divisão
O que é que o GraftPdfAcroForm precisa de si?
Precisa de três streams distintas e de um mapeamento de páginas explícito. O GraftPdfAcroForm recebe Source, Destination e Output como instâncias separadas de TStream, um array TPdfGraftPageMappings, um record TPdfAcroFormGraftOptions, um TPdfCrossDocumentGraftMap opcional e um parâmetro out TPdfAcroFormGraftReport. Devolve Boolean em vez de lançar exceções, e em caso de falha o relatório transporta o motivo em ErrorMessage. O mapeamento de páginas é baseado em um de ambos os lados e não é inferido: cada página de origem que transporte um widget que pretenda enxertar tem de lá figurar. Passar nil para o mapa de enxerto é legítimo — a função cria e liberta um mapa privado pela duração da chamada — e TPdfAcroFormGraftOptions.Default fornece CollisionPolicy definido como pagcpReject, RenamePrefix definido como Imported_, MaxObjects de 100000, MaxDepth de 128 e AllowSignedDestination definido como False. Esses três últimos são orçamentos, e existem porque o grafo de objetos que está prestes a percorrer veio de um ficheiro que não escreveu
uses
Classes, SysUtils, FPdfCompress;
var
Source, Destination, Output: TMemoryStream;
Options: TPdfAcroFormGraftOptions;
Mappings: TPdfGraftPageMappings;
Report: TPdfAcroFormGraftReport;
begin
Source := TMemoryStream.Create;
Destination := TMemoryStream.Create;
Output := TMemoryStream.Create;
try
Source.LoadFromFile('claim-template-2025.pdf');
Destination.LoadFromFile('claim-layout-2026.pdf');
Source.Position := 0;
Destination.Position := 0;
Options := TPdfAcroFormGraftOptions.Default;
SetLength(Mappings, 2);
Mappings[0].SourcePageNumber := 1;
Mappings[0].DestinationPageNumber := 1;
Mappings[1].SourcePageNumber := 2;
Mappings[1].DestinationPageNumber := 3;
if GraftPdfAcroForm(Source, Destination, Output, Mappings,
Options, nil, Report) then
Output.SaveToFile('claim-2026-with-fields.pdf')
else
raise Exception.Create(Report.ErrorMessage);
finally
Output.Free;
Destination.Free;
Source.Free;
end;
end;
Como é que o mapa de enxerto evita clonar duas vezes um tipo de letra partilhado?
O TPdfCrossDocumentGraftMap mantém uma tabela de referências de origem para destino cujas chaves transportam tanto o número de objeto como a geração, e o clonador recursivo consulta-a antes de descer. A ordem das operações é o que torna os ciclos seguros: o clonador aloca o número de objeto de destino e regista o mapeamento primeiro, depois percorre as referências filhas do objeto de origem. Um pai que alcance um filho que aponte de volta para ele encontra o pai já registado e devolve a referência de destino existente em vez de continuar a recursão. A mesma consulta é o que faz com que um tipo de letra, uma stream de aparência ou uma ação partilhados por seis widgets sejam clonados uma vez e referenciados seis vezes. O mapa é vinculado ao documento de origem por um hash SHA-256 dos bytes de origem, exposto como SourceIdentity. Se entregar ao GraftPdfAcroForm um mapa cuja identidade não corresponde à origem que passou, ele recusa a chamada em vez de reutilizar referências que nunca foram válidas para este ficheiro. Os mapeamentos de páginas são semeados no mesmo mapa antes de a clonagem começar, e é precisamente assim que o /P de um widget acaba por apontar para a página de destino: o objeto de página de origem já resolve para o objeto de página de destino mapeado, portanto a passagem comum de reescrita de referências trata disso sem caso especial
uses
Classes, SysUtils, FPdfCompress, FPdfSha256;
var
GraftMap: TPdfCrossDocumentGraftMap;
SourceBytes: TBytes;
EntriesBefore: Integer;
begin
SetLength(SourceBytes, Source.Size);
Source.Position := 0;
if Length(SourceBytes) > 0 then
Source.ReadBuffer(SourceBytes[0], Length(SourceBytes));
GraftMap := TPdfCrossDocumentGraftMap.Create(
AnsiString(SHA256Hex(SHA256Bytes(SourceBytes))));
try
EntriesBefore := GraftMap.Count;
Source.Position := 0;
if not GraftPdfAcroForm(Source, Destination, Output, Mappings,
Options, GraftMap, Report) then
begin
// As entradas adicionadas por esta chamada foram revertidas;
// tudo o que foi registado antes dela permanece intacto.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
Essa reversão é o motivo de possuir o mapa por conta própria. O PDFiumPas trata um mapa fornecido pelo chamador de forma transacional: um enxerto falhado descarta as entradas que essa chamada adicionou e mantém todos os mapeamentos que já existiam, pelo que uma recusa nunca deixa para trás uma cache de referências para objetos que nunca foram gravados. Mantenha um mapa por documento de destino, no entanto — o lado de destino de cada entrada é um número de objeto nesse ficheiro específico, e não significa nada noutro
Colisões de nomes de campos: rejeitar ou renomear
Os nomes de campos totalmente qualificados têm de permanecer únicos dentro de um formulário, e o PDFiumPas não vai adivinhar o que quis dizer quando colidem. TPdfAcroFormCollisionPolicy oferece exatamente duas respostas. Sob pagcpReject, a predefinição, o primeiro campo de origem cujo título já exista no destino aborta todo o enxerto com um erro e deixa a stream de saída vazia. Sob pagcpRename, o campo de origem em colisão é renomeado com o prefixo RenamePrefix e o enxerto continua, com Report.RenamedFieldCount a indicar quantas vezes isso aconteceu
Options := TPdfAcroFormGraftOptions.Default;
Options.CollisionPolicy := pagcpRename;
Options.RenamePrefix := 'Y2025_';
Options.MaxObjects := 20000;
Options.MaxDepth := 64;
if GraftPdfAcroForm(Source, Destination, Output, Mappings,
Options, nil, Report) then
begin
WriteLn('source fields : ', Report.SourceFieldCount);
WriteLn('existing fields: ', Report.DestinationFieldCount);
WriteLn('grafted fields : ', Report.GraftedFieldCount);
WriteLn('renamed fields : ', Report.RenamedFieldCount);
WriteLn('cloned objects : ', Report.GraftedObjectCount);
WriteLn('reused objects : ', Report.ReusedObjectCount);
WriteLn('mapped pages : ', Report.MappedPageCount);
WriteLn('output bytes : ', Report.OutputByteCount);
end
else
WriteLn('graft refused : ', Report.ErrorMessage);
Renomear não é gratuito, e deve decidê-lo deliberadamente em vez de recorrer a isso para fazer um erro desaparecer. Um campo renomeado é um campo diferente: qualquer JavaScript no destino que o enderece pelo nome, qualquer entrada de cálculo em /CO que alguém escreveu contra o nome antigo, e qualquer consumidor a jusante que se baseie no nome do campo precisará de saber do prefixo. Se os dois documentos descrevem genuinamente o mesmo campo, a correção honesta costuma ser reconciliar os nomes a montante, não no momento do enxerto. Assim que o enxerto aterra, percorrer o formulário combinado para confirmar o que realmente obteve é o passo natural seguinte, e a navegação de campos de formulário no PDFiumPas cobre essa travessia
Onde o enxerto falha deliberadamente de forma segura
Cada condição ambígua é um erro, nunca um resultado de melhor esforço, e essa é uma decisão de desenho que vale a pena compreender antes de o surpreender em produção. O GraftPdfAcroForm devolve False, repõe a stream de saída e comunica o motivo quando se depara com qualquer uma destas situações
- O formulário de origem transporta uma entrada
/XFA— os pacotes XFA são um modelo de formulário paralelo e não podem ser reduzidos a dicionários de campos AcroForm - Um widget vive numa página de origem sem entrada no mapeamento de páginas, o que de outra forma descartaria silenciosamente o campo ou anexá-lo-ia à página errada
- Os mapeamentos de páginas estão fora do intervalo, ou dois mapeamentos reutilizam a mesma página de origem ou de destino
- Ambos os formulários definem um dicionário de recursos predefinidos
/DR, porque fundir dois espaços de nomes de recursos arriscaria reapontar um nome existente para um tipo de letra diferente - O grafo de objetos excede
MaxObjectsou a recursão excedeMaxDepth - O destino contém uma assinatura e
AllowSignedDestinationéFalse - O mapa de enxerto fornecido pertence a um documento de origem diferente, ou uma referência de origem está pendente
O caminho de escrita é igualmente conservador. O PDFiumPas emite o resultado como uma revisão incremental dispersa anexada ao destino, depois rematerializa a saída escrita e relê o seu formulário: se a contagem de campos do resultado não for igual à contagem original de campos do destino mais a da origem, todo o enxerto é rejeitado e a saída é limpa. Nunca fica com um ficheiro parcialmente enxertado. O custo dessa política é real — uma colisão de /DR ou um destino assinado detém-no por completo, e tem de resolver isso por si em vez de aceitar uma aproximação combinada — mas a alternativa é um formulário que abre bem e calcula mal
Quando enxertar é a ferramenta errada
A enxertia move estrutura, por isso use-a quando a estrutura é o que lhe falta. Se ambos os documentos já transportam o mesmo conjunto de campos e só precisa de mover valores e anotações entre eles, o caminho de exportação e importação no artigo sobre dados de formulário XFDF é mais leve, baseado num padrão e reversível. Recorra ao GraftPdfAcroForm quando o destino não tem campos nenhum, ou tem um conjunto diferente, e precisa de que os widgets, as streams de aparência, as ações e a ordem de cálculo cheguem intactos. Uma última nota prática sobre identidade: como o mapa de enxerto se indexa pelo número de objeto mais geração e está vinculado a um SHA-256 dos bytes de origem, voltar a gravar ou otimizar a origem entre execuções produz uma identidade diferente e um mapa que já não se aplica. Faça um instantâneo da origem de onde enxerta e mantenha-a estável durante o lote; trate-a como um artefato de entrada, não como algo que um trabalho noturno pode reescrever à vontade
O GraftPdfAcroForm, o TPdfCrossDocumentGraftMap e o toolkit PDF ao nível de streams que os rodeia acompanham o PDFiumPas Delphi PDFium Component para Delphi, C++Builder e Lazarus, onde a página do produto traz a referência completa da API para as opções de enxerto, os campos do relatório e o resto da superfície de edição de documentos