Mover um bloco de form fields do template do ano passado para o layout deste ano é onde os round-trips de FDF e XFDF deixam de ser suficientes: os valores chegam, mas os appearance streams, as ações de cálculo e os recursos padrão não. O PDFiumPas responde a esse caso com o GraftPdfAcroForm, que clona todo o grafo de objetos de field de um PDF e o escreve em outro
A razão pela qual uma exportação em nível de dados não consegue fazer isso é estrutural. Um field não é um record, é um subgrafo. A ISO 32000-1 §12.7 define o dicionário de formulário interativo que guarda /Fields, /CO, /DR e /DA, a §12.7.3 define os dicionários de field pendurados abaixo dele, e a §12.5.6.19 define as widget annotations que dão a esses fields uma caixa visível em uma página. O XFDF carrega as folhas dessa estrutura. Enxertar carrega a estrutura em si
Por que copiar o array /Fields nunca é suficiente
Copiar /Fields de um documento para outro produz um formulário quebrado de todas as maneiras interessantes, porque o array guarda apenas referências indiretas e nada mais. 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ó são significativos dentro do arquivo de onde vieram. Cole o array do outro lado e cada referência nele ou fica pendurada ou, pior, silenciosamente resolve para um objeto não relacionado que por acaso ocupa aquele slot no destino. Abaixo de cada referência fica um grafo que é ao mesmo tempo compartilhado e cíclico. Um dicionário de field aponta para seus kids, cada kid aponta de volta para seu /Parent, um widget aponta para seus appearance streams e para a página que o carrega por meio de /P, appearance streams apontam para fonts no dicionário de recursos padrão do formulário, e dicionários de additional-action sob /AA apontam para ainda mais objetos. Dois widgets em páginas diferentes rotineiramente compartilham uma font e um appearance XObject. Então um enxerto correto precisa 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 field existe no formulário e é invisível na página. Se você já perseguiu a diferença entre um field, seu widget e a page annotation que o exibe, nossa nota sobre índice de widget versus índice de annotation cobre exatamente essa divisão
O que o GraftPdfAcroForm precisa de você?
Ele precisa de três streams distintos e um mapeamento de páginas explícito. GraftPdfAcroForm recebe Source, Destination e Output como instâncias separadas de TStream, um array TPdfGraftPageMappings, um record TPdfAcroFormGraftOptions, um TPdfCrossDocumentGraftMap opcional, e um out TPdfAcroFormGraftReport. Ele retorna Boolean em vez de levantar exceção, e em caso de falha o report carrega o motivo em ErrorMessage. O mapeamento de páginas é base 1 nos dois lados e não é inferido: toda página de origem que carrega um widget que você pretende enxertar precisa aparecer nele. Passar nil para o graft map é legítimo — a função então cria e libera um privado pela duração da chamada — e TPdfAcroFormGraftOptions.Default lhe dá 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 você está prestes a percorrer veio de um arquivo que você 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 o graft map evita clonar uma font compartilhada duas vezes?
O TPdfCrossDocumentGraftMap guarda uma tabela de referência de origem para destino cujas chaves carregam número de objeto e geração, e o cloner recursivo a consulta antes de descer. A ordem das operações é o que torna os ciclos seguros: o cloner aloca o número de objeto de destino e registra o mapeamento primeiro, depois percorre as referências filhas do objeto de origem. Um parent que alcança um kid que aponta de volta para seu parent encontra o parent já registrado e retorna a referência de destino existente em vez de recorrer. A mesma consulta é o que faz uma font, um appearance stream ou uma action compartilhada por seis widgets ser clonada uma vez e referenciada seis vezes. O map é vinculado ao documento de origem por um hash SHA-256 dos bytes de origem, exposto como SourceIdentity. Se você entregar ao GraftPdfAcroForm um map 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 arquivo. Os page mappings são semeados no mesmo map antes do cloning começar, e é precisamente assim que o /P de um widget acaba apontando para a página de destino: o objeto de página de origem já resolve para o objeto de página de destino mapeado, então o passo comum de reescrita de referências cuida 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
// Entradas adicionadas por esta chamada foram revertidas;
// qualquer coisa registrada antes dela permanece intacta.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
Esse rollback é o motivo de possuir o map você mesmo. O PDFiumPas trata um map fornecido pelo chamador transacionalmente: um graft que falha descarta as entradas que aquela chamada adicionou e mantém todo mapeamento que existia antes, então uma recusa nunca deixa para trás um cache de referências a objetos que nunca foram escritos. Mantenha um map por documento de destino, porém — o lado de destino de cada entrada é um número de objeto naquele arquivo específico, e não significa nada em um diferente
Colisões de nomes de fields: rejeitar ou renomear
Nomes de fields totalmente qualificados precisam permanecer únicos dentro de um formulário, e o PDFiumPas não vai adivinhar o que você queria dizer quando eles colidem. TPdfAcroFormCollisionPolicy oferece exatamente duas respostas. Sob pagcpReject, o padrão, o primeiro field de origem cujo nome já existe no destino aborta o graft inteiro com um erro e deixa o stream de saída vazio. Sob pagcpRename, o field de origem em colisão é renomeado prefixando RenamePrefix e o graft continua, com Report.RenamedFieldCount dizendo a você com que frequência 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 é de graça, e você deve decidir isso deliberadamente em vez de recorrer a isso para fazer um erro sumir. Um field renomeado é um field diferente: qualquer JavaScript no destino que o enderece pelo nome, qualquer entrada de cálculo em /CO que uma pessoa escreveu contra o nome antigo, e qualquer consumidor a jusante que chaveia no nome do field vai precisar saber do prefixo. Se os dois documentos genuinamente descrevem o mesmo field, a correção honesta geralmente é reconciliar os nomes a montante, não na hora do graft. Uma vez que o graft pousa, percorrer o formulário mesclado para confirmar o que você realmente recebeu é o próximo passo natural, e navegação de form fields no PDFiumPas cobre essa travessia
Onde o graft deliberadamente falha fechado
Toda condição ambígua é um erro, nunca um resultado de melhor esforço, e essa é uma decisão de design que vale entender antes que ela o surpreenda em produção. GraftPdfAcroForm retorna False, reinicia o stream de saída e relata o motivo quando atinge qualquer um destes
- O formulário de origem carrega uma entrada
/XFA— pacotes XFA são um modelo de formulário paralelo e não podem ser reduzidos a dicionários de field AcroForm - Um widget vive em uma página de origem sem entrada no mapeamento de páginas, o que de outra forma silenciosamente descartaria o field ou o anexaria à página errada
- Mapeamentos de páginas 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 padrão
/DR, porque mesclar dois espaços de nomes de recursos arriscaria redirecionar um nome existente para uma font diferente - O grafo de objetos excede
MaxObjectsou a recursão excedeMaxDepth - O destino contém uma assinatura e
AllowSignedDestinationéFalse - O graft map fornecido pertence a um documento de origem diferente, ou uma referência de origem está pendurada
O caminho de escrita é igualmente conservador. O PDFiumPas emite o resultado como uma revisão incremental esparsa anexada ao destino, depois re-materializa a saída escrita e relê seu formulário: se a contagem de fields do resultado não for igual à contagem original de fields do destino mais a da origem, o graft inteiro é rejeitado e a saída é limpa. Você nunca recebe um arquivo parcialmente enxertado. O custo dessa política é real — uma colisão de /DR ou um destino assinado o para de vez, e você mesmo precisa resolvê-lo em vez de aceitar uma aproximação mesclada — mas a alternativa é um formulário que abre bem e calcula errado
Quando enxertar é a ferramenta errada
Enxertar move estrutura, então use-o quando a estrutura é o que está faltando. Se ambos os documentos já carregam o mesmo conjunto de fields e você só precisa mover valores e annotations entre eles, o caminho de export e import no artigo de dados de formulário XFDF é mais leve, padrão e reversível. Recorra ao GraftPdfAcroForm quando o destino não tem fields algum, ou tem um conjunto diferente, e você precisa que os widgets, appearance streams, ações e ordem de cálculo cheguem intactos. Uma última nota prática sobre identidade: como o graft map chaveia em número de objeto mais geração e é vinculado a um SHA-256 dos bytes de origem, re-salvar ou otimizar a origem entre execuções produz uma identidade diferente e um map que não se aplica mais. Tire um snapshot da origem da qual enxerta e mantenha-a estável para o batch; trate-a como um artefato de entrada, não como algo que um job noturno pode reescrever à vontade
O GraftPdfAcroForm, o TPdfCrossDocumentGraftMap e o toolkit PDF circundante em nível de stream embarcam com o PDFiumPas Delphi PDFium Component para Delphi, C++Builder e Lazarus, onde a página do produto carrega a referência API completa para as opções de graft, os fields do report e o resto da superfície de edição de documentos