Artigo Técnico

Combinar Formulários PDF em Delphi: Regras de Campos Duplicados

A PDF Library for Delphi combina dois documentos AcroForm com uma política explícita para campos que partilham um nome. MergeDocumentEx recebe o identificador do documento de origem e uma de três estratégias: dfsReject recusa a combinação, dfsMerge mantém o nome partilhado e sincroniza os valores, e dfsAutoNumber renomeia os campos recebidos de forma determinística. A verificação de nomes acontece antes de qualquer número de objeto mudar, pelo que uma combinação rejeitada deixa ambos os documentos totalmente utilizáveis

Quem já montou um pacote de candidatura em PDF já se deparou com isto. Três formulários, cada um com um campo chamado Signature, Date ou Total, são combinados num único ficheiro. Num AcroForm, o nome de campo totalmente qualificado é a identidade do campo, pelo que dois campos com o mesmo nome não são de todo dois campos: preencher um preenche o outro, e uma assinatura aplicada a um cobre um âmbito que ninguém pretendia

Por que razão o conflito de nomes é decidido antes da combinação?

O antigo MergeDocument concatena os dois arrays de campos raiz do AcroForm e não oferece escolha. Pior ainda, quando o resultado é inutilizável, a descoberta acontece depois de os números de objeto terem sido renumerados e as árvores de páginas cosidas, o que deixa o chamador com um documento num estado em que nenhum dos originais estava

MergeDocumentEx inverte a ordem. Recolhe os nomes de campo de topo de ambos os documentos, compara-os, e aplica a estratégia antes de qualquer coisa se mover. Uma rejeição é, portanto, uma não operação limpa: o documento alvo fica intocado, o documento de origem fica intocado, e ambos permanecem abertos e utilizáveis, o que o teste de combinação verifica lendo de volta um valor de campo do documento de origem depois de uma combinação recusada

A comparação usa um conjunto de nomes ordenado e sensível a maiúsculas e minúsculas, pelo que o custo é proporcional à contagem combinada de campos vezes um fator logarítmico, e não ao produto das duas contagens. A sensibilidade a maiúsculas e minúsculas é a escolha correta aqui porque os nomes de campo em PDF são sensíveis a maiúsculas e minúsculas; dobrá-los combinaria campos que a especificação trata como distintos

As três estratégias, e quando cada uma é a certa

dfsReject é a estratégia para pipelines automatizados que não podem produzir documentos ambíguos. A combinação devolve zero e LastErrorCode reporta 705, um código dedicado para que os nomes duplicados possam ser distinguidos de qualquer outra falha de combinação e encaminhados para uma correção específica, normalmente renomear campos a montante

dfsMerge mantém o nome partilhado deliberadamente e sincroniza o valor alvo e o valor predefinido no campo de origem, pelo que um visualizador conforme trata os vários widgets como um único campo logicamente nomeado, o que é o comportamento AcroForm normal para um campo com várias anotações de widget. O que não faz é fundir diferentes dicionários de campo num único objeto. Cada campo mantém a sua própria associação de página, aparência e ações, porque colapsá-los descartaria silenciosamente formatação e comportamento que pertencem ao documento recebido

dfsAutoNumber renomeia os duplicados recebidos acrescentando um sufixo numérico a começar em _2 e usando o primeiro livre. O resultado é reproduzível: depende apenas dos nomes presentes, nunca dos números de objeto de campo, pelo que combinar o mesmo par de documentos duas vezes produz os mesmos nomes em ambas as vezes. Essa propriedade importa quando código a jusante, uma importação FDF ou um mapeamento de base de dados referencia campos por nome:

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  TargetDoc, SourceDoc: Integer;
begin
  Lib := TPDFlib.Create;
  try
    TargetDoc := Lib.SelectedDocument;
    Lib.LoadFromFile('application-part1.pdf', '');

    SourceDoc := Lib.NewDocument;
    Lib.LoadFromFile('application-part2.pdf', '');

    Lib.SelectDocument(TargetDoc);
    if Lib.MergeDocumentEx(SourceDoc, dfsReject) = 0 then
    begin
      if Lib.LastErrorCode = 705 then
      begin
        // Ambos os documentos continuam intactos - tentar de novo com uma política
        Log('duplicate field names; retrying with auto-numbering');
        Lib.MergeDocumentEx(SourceDoc, dfsAutoNumber);
      end;
    end;

    Lib.SaveToFile('application-complete.pdf');
  finally
    Lib.Free;
  end;
end;

Repare no padrão em dois passos nesse código, que só é possível porque a rejeição não é destrutiva. Experimente primeiro a política estrita, inspecione o erro, depois decida. Com uma combinação que falhe a meio, o mecanismo de recurso teria de recomeçar recarregando ambos os ficheiros

Que aspeto tem o formulário combinado depois

Sob dfsMerge, um campo alvo chamado Shared a transportar "Target value" e um campo de origem com o mesmo nome produzem dois campos, ambos chamados Shared, ambos a reportar o valor alvo, porque o valor alvo e o valor predefinido são sincronizados no campo recebido. Essa é a semântica pretendida para um nome partilhado: um campo lógico, vários widgets, um valor

Sob dfsAutoNumber, a mesma entrada produz Shared e Shared_2 como campos separados com valores independentes. Escolha entre os dois fazendo uma única pergunta: preencher um controlo deve preencher o outro? Para o nome de um signatário repetido em todas as partes de um pacote, sim, e dfsMerge é a opção certa. Para um total que significa algo diferente em cada formulário, não, e a numeração automática é a opção certa:

// Depois de uma combinação, enumere o que realmente obteve
for I := 1 to Lib.FormFieldCount do
  Log(Format('%d: %s = %s',
    [I, Lib.GetFormFieldTitle(I), Lib.GetFormFieldValue(I)]));

Notas práticas para montar pacotes de formulários

Uma combinação bem-sucedida consome o documento de origem: este é removido da lista de documentos da biblioteca, e é por isso que DocumentCount desce de dois para um. Não continue a usar o identificador de origem depois disso. A versão do documento é elevada para a mais alta das duas, pelo que combinar um formulário PDF 2.0 num documento 1.7 produz um ficheiro 2.0

A ordem importa para os nomes. Combinar A em B e combinar B em A produzem resultados de numeração automática diferentes, já que o documento que faz a combinação mantém os seus nomes inalterados. Quando um pacote tem um formulário primário canónico, faça desse o alvo

Os campos de assinatura merecem consideração própria. Uma assinatura aplicada antes de uma combinação só cobre a revisão que assinou, pelo que a combinação a invalida no sentido prático de que o ficheiro mudou desde a assinatura. Monte primeiro e assine o documento montado, em vez de combinar partes já assinadas. Quando a combinação é sobre conteúdo de página em vez de formulários, o percurso mais rápido descrito em combinação rápida de PDF com deslocamento de referências por byte é a ferramenta melhor

Por fim, planeie o lado dos dados do pacote em conjunto com a combinação. Se os valores de campo chegam de um sistema externo, decida se esse sistema endereça campos por nome antes de escolher a numeração automática, porque Shared_2 não vai corresponder a um mapeamento que espera Shared. Os formatos de importação e exportação estão descritos em intercâmbio de dados de formulário FDF, XFDF e XFA, e o comportamento de scripting ao nível de campo, também afetado pela renomeação, está descrito em ações de formulário interativas e JavaScript

A combinação de formulários, o intercâmbio de dados e a assinatura correm na mesma biblioteca para Delphi, C++Builder e Free Pascal; a lista completa de funcionalidades está na página da PDF Library for Delphi