Artigo Técnico

Converta XFA estático em campos AcroForm em Delphi

O PDFium Component converte formulários XFA estáticos em campos AcroForm comuns a partir do Delphi por meio do FlattenPdfXfaToAcroForm na unit FPdfDocumentXfaFlatten. A função lê os packets de template e datasets do XFA, mapeia todo campo posicionado absolutamente para um campo terminal AcroForm com seu próprio appearance stream, e anexa o resultado como uma revisão incremental, então o PDF convertido abre como um formulário normal em viewers que nunca suportaram XFA

A motivação costuma ser um chamado de suporte em vez de um item de roadmap. Um formulário de governo ou de seguro foi autorado em um designer que o salvou como XFA, funciona em um leitor de desktop, e em todo o resto abre como uma página em branco que diz que o documento exige um viewer mais novo. O XFA foi descontinuado no PDF 2.0 (ISO 32000-2), browsers e viewers móveis nunca o implementaram, e o próprio PDFium só renderiza XFA na build com V8 habilitada. Formulários XFA estáticos, aqueles cujo layout é fixado em tempo de design, carregam geometria suficiente no template para serem reconstruídos como campos AcroForm, e é exatamente essa fatia que este conversor trata

O que o FlattenPdfXfaToAcroForm realmente produz?

O FlattenPdfXfaToAcroForm produz uma revisão AcroForm em nível de bytes anexada a uma cópia inalterada da origem, jamais uma regravação do arquivo original. O PDFium não expõe API pública para criar campos de formulário, então o componente não passa pelo PDFium para este trabalho: ele lê a revisão ativa com seu próprio leitor de objetos, constrói dicionários de campo, anotações widget, appearance XObjects e um recurso de fonte /Helv como objetos PDF brutos, e os serializa por meio de uma atualização incremental esparsa no sentido da ISO 32000-1 §7.5.6

O novo dicionário /AcroForm carrega /Fields, /DA e /DR e nenhuma entrada /XFA. Quando a origem mantém seu AcroForm como objeto indireto, esse objeto é regravado no lugar; um dicionário inline é substituído dentro do catálogo. De qualquer forma, a revisão ativa não aponta mais para os packets XFA, então um viewer que entende ambos os modelos abre os campos AcroForm em vez de tentar dispor o formulário XFA. Os bytes XFA originais ainda estão na revisão anterior, que é o que uma atualização incremental significa, mas nada na revisão atual os referencia

Pipeline de conversão de XFA estático para AcroForm no PDFium Component para Delphi: o FlattenPdfXfaToAcroForm lê os packets de template e datasets, mapeia campos posicionados absolutamente para dicionários de campo e widgets, e anexa o AcroForm como uma revisão incremental esparsa sem entrada /XFA
O FlattenPdfXfaToAcroForm jamais toca nos bytes originais: os campos convertidos, widgets e appearance streams são anexados como uma revisão cujo /AcroForm simplesmente para de apontar para os packets XFA
uses
  System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentXfaFlatten;

procedure ConvertXfaForm(const SourceFile, TargetFile: string);
var
  Source: TFileStream;
  Options: TPdfXfaFlattenOptions;
  OutputPdf: TBytes;
  Report: TPdfXfaFlattenReport;
  Fields: TPdfXfaFlattenFields;
begin
  Options := TPdfXfaFlattenOptions.Default;
  Options.MaxFields := 1024;   // o padrão é 512, a faixa permitida é 1..4096

  Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyWrite);
  try
    if not FlattenPdfXfaToAcroForm(Source, Options, OutputPdf, Report, Fields) then
      raise Exception.Create('XFA conversion failed: ' + Report.ErrorText);
  finally
    Source.Free;
  end;

  TFile.WriteAllBytes(TargetFile, OutputPdf);
  Writeln(Format('%d template fields, %d bound to datasets, %d written on %d pages',
    [Report.TemplateFields, Report.DatasetBound, Report.FieldsWritten, Report.PagesWritten]));
end;

A função devolve False e preenche o Report.ErrorText para qualquer entrada rejeitada ou malformada, e restaura a posição do stream de origem na saída. Cancelamento é o único caminho de exceção que se propaga: se o Options.CancellationToken dispara, o EPdfOperationCancelled é relançado depois de o relatório ser preenchido. O TPdfXfaFlattenReport também carrega SourceSha256 e OutputSha256, o que é prático quando um job em lote precisa provar qual entrada produziu qual arquivo convertido. O TPdfXfaFlattenOptions.Default define um orçamento de packet de 8 MiB, 512 campos e um teto de saída de 512 MiB; a origem não pode ser criptografada

Lendo os packets de template e datasets

O PDFium Component aceita ambos os layouts que a ISO 32000-1 §12.7.8 permite para a entrada /XFA: a forma de array que alterna nomes de packet e referências de stream, e a forma XDP de stream único em que um stream guarda o documento <xdp:xdp> inteiro. Streams de packet podem ser descomprimidos ou /FlateDecode sem predictor, e a descompressão é limitada por MaxPacketBytes

O caso de stream único é onde tratamento de XML ingênuo quebra. Designers comumente gravam <xfa:datasets> com um prefixo de namespace enquanto o template senta sem prefixo, então fatiar por nome literal de tag acha um packet e perde o outro. O conversor casa nomes de elemento e de atributo pelo nome local, depois de uma checagem separada de que tags de abertura e fechamento se emparelham pelo nome prefixado completo. Se você só precisa olhar os packets em vez de convertê-los, o guia de detectar formulários XFA e extrair seus packets com PDFium VCL cobre esse caminho somente leitura

Os valores de campo vêm de dois lugares. Um elemento <value> no template fornece o padrão, e o packet de datasets fornece os dados preenchidos, atados pelo nome do campo. Nomes de campo se repetem em formulários reais, então o binder compara a cadeia de ancestrais <subform> nomeados com o caminho de ancestrais nos dados e toma o casamento mais longo sem conflito. Quando dois campos folha compartilham um nome sob subforms diferentes, o /T do AcroForm vira SubformPath.Name para os dois campos permanecerem distintos depois da conversão

Por que choiceList precisa de uma flag Combo explícita?

XFA e PDF têm padrões opostos para campos de escolha, então um conversor que copie nenhuma flag transforma todo drop-down em uma list box. Um <choiceList> XFA com o padrão open="userControl" ou com open="onEntry" é um drop-down; um campo /FT /Ch PDF sem flags é uma list box rolável, e apenas a flag Combo (posição de bit 18 na ISO 32000-1 Tabela 230, valor 131072, o mesmo bit que o PDFium nomeia FPDF_FORMFLAG_CHOICE_COMBO) o torna uma combo box. O conversor portanto define a flag Combo para os casos de drop-down e deixa as flags vazias apenas quando o template diz open="always" ou open="multiSelect", que mantêm a lista permanentemente aberta. Builds antes da 3.181.1 trocavam os valores de Combo e Pushbutton e procuravam por um open="on" fora do padrão, então formulários achatados com elas valem uma nova rodada

Decisão de flag de campo de escolha na conversão de XFA estático para AcroForm do PDFium Component: um atributo open de userControl ou onEntry define a flag Combo, bit 18 valor 131072, para produzir uma combo box drop-down, enquanto open always ou multiSelect deixa as flags vazias para uma list box permanentemente aberta
Sem a flag Combo um drop-down XFA se achata em uma list box rolável, então o conversor lê o atributo open e define o bit 18 apenas para os casos de drop-down

O resto do mapeamento de UI segue as tabelas de field flags da ISO 32000-1 §12.7.4:

  • textEdit, numericEdit e dateTimeEdit viram /FT /Tx; um campo de texto multilinha recebe a flag Multiline (/Ff 4096, Tabela 228) e um appearance stream dividido por linha
  • passwordEdit vira /FT /Tx com a flag Password (/Ff 8192) e uma aparência que desenha um asterisco por scalar Unicode, não por code unit UTF-16
  • checkButton vira uma check box /FT /Btn cujo valor é /Yes quando o dado atado é 1 e /Off caso contrário
  • <exclGroup> vira um pai de rádio /FT /Btn /Ff 32768 (Tabela 226) com /V, /DV, /Opt e /Kids, mais um widget filho por membro com seu próprio /AS e /AP de dois estados
  • imageEdit vira um pushbutton cuja aparência pinta a imagem; um JPEG de baseline é embutido verbatim sob /DCTDecode, enquanto PNG é decodificado e recodificado como pixels /FlateDecode com um /SMask extra quando o PNG carrega transparência alfa ou tRNS

Valores que não são ASCII imprimível simples, incluindo quebras de linha, são gravados como strings UTF-16BE <FEFF...>, então um nome japonês ou um endereço de duas linhas sobrevive à viagem. A saída de Fields espelha o que foi gravado, o que a torna uma trilha de auditoria conveniente

procedure DumpConvertedFields(const Fields: TPdfXfaFlattenFields);
const
  KindNames: array[TPdfXfaFlattenFieldKind] of string = (
    'text', 'check', 'choice', 'image', 'numeric', 'dateTime', 'password', 'radio');
var
  I: Integer;
  F: TPdfXfaFlattenField;
begin
  for I := 0 to High(Fields) do
  begin
    F := Fields[I];
    Writeln(Format('%s.%s [%s] page %d at %.1f,%.1f size %.1f x %.1f',
      [F.SubformPath, F.Name, KindNames[F.Kind], F.PageIndex + 1, F.X, F.Y, F.W, F.H]));
    case F.Kind of
      fxfkChoice:
        Writeln(Format('  %d items, list box: %s', [Length(F.Choices), BoolToStr(F.ListBox, True)]));
      fxfkRadio:
        Writeln(Format('  %d radio widgets, value "%s"', [Length(F.RadioWidgets), F.Value]));
      fxfkImage:
        Writeln(Format('  %d image bytes', [Length(F.ImageData)]));
    else
      Writeln('  value "' + F.Value + '"');
    end;
  end;
end;

Geometria, unidades de medida e roteamento de páginas

A geometria XFA é convertida para o espaço de usuário do PDF invertendo cada campo contra a altura do MediaBox de sua própria página, porque o XFA mede para baixo a partir do topo da página e o PDF mede para cima a partir da base. Deslocamentos de elementos <subform> aninhados acumulam na descida pelo template, e todo campo precisa acabar com largura e altura positivas ou a conversão é rejeitada

Conversão de geometria XFA para PDF no PDFium Component para Delphi: um retângulo de campo medido para baixo a partir da borda superior no template XFA é invertido contra a altura do MediaBox de sua própria página para o espaço de usuário do PDF, onde a coordenada y cresce para cima a partir da base
O template XFA para baixo e o espaço de usuário do PDF para cima se encontram em uma única subtração contra a altura do MediaBox, que é o que posiciona todo widget convertido em sua posição projetada

Medidas aceitam as unidades que designers reais emitem: números puros e pt, mais in (72 pontos), mm (72 / 25.4), cm (72 / 2.54), pc (12), px (0.75) e mp (0.001, millipoints). O parsing é independente de locale, então uma instalação Windows alemã não transforma 8.5in em absurdo. Um sufixo de unidade desconhecido é rejeitado em vez de ser lido como pontos, o que reposicionaria o campo silenciosamente

O roteamento de páginas segue o <pageSet>: elementos <pageArea> irmãos são numerados a partir de um na ordem de declaração e reivindicam os campos aninhados sob eles, enquanto campos sem ancestral <pageArea> ficam na página um. Cada página recebe sua própria atualização de /Annots. Um campo roteado além da última página do PDF é um erro em vez de um widget derrubado na página um

Quais formulários XFA o conversor recusa?

O conversor recusa qualquer coisa cujo layout não possa ser conhecido sem rodar o engine de layout do XFA. Isso significa um atributo layout diferente de position em um <subform> ou <exclGroup> (os fluídos tb, lr-tb e companhia, que é com que formulários dinâmicos são construídos), qualquer elemento <script>, e qualquer tipo de UI fora dos sete listados acima. Formulários dinâmicos que crescem páginas em tempo de execução pertencem a uma toolchain diferente; o artigo sobre repaginação XFA dinâmica e contagens de páginas no PDFium Component mostra o que esse layout ao vivo envolve

A recusa é deliberada. Um subform fluído não tem coordenadas fixas, então qualquer "conversão" teria de inventar posições, e um formulário com script frequentemente calcula totais ou alterna visibilidade que um AcroForm estático não consegue reproduzir. Produzir um PDF que parece completo mas perdeu silenciosamente um total calculado é pior que um erro claro no Report.ErrorText. A especificação XFA 3.3 define muito mais que este subset, e o conversor não finge o contrário

Armadilhas ao gravar e verificar a revisão AcroForm

Dois problemas apareceram enquanto o writer era construído, e ambos valem para qualquer um produzindo atualizações incrementais à mão. O primeiro: o catálogo precisa sempre fazer parte do conjunto de atualização, mesmo quando seus bytes não mudam porque o AcroForm é um objeto indireto regravado em outro lugar. O writer de atualização esparsa deriva o /Root do trailer dos objetos que carrega, e uma atualização sem o objeto raiz produz uma revisão que viewers não conseguem ancorar

O segundo diz respeito à verificação. Carregar o arquivo convertido pelo wrapper PDFium dispara a inicialização do ambiente de formulários assim que o documento tem um /AcroForm, e a pdfium.dll hospedeira na configuração de teste carecia do export FORM_GetTextDirection, então a inicialização falhava antes de um único campo poder ser conferido. As checagens de leitura de volta usam portanto o leitor de objetos do componente, que interpreta a revisão ativa sem tocar na camada de formulários

uses
  System.SysUtils, System.Classes, FPdfCompress;

function HasActiveAcroForm(const Pdf: TBytes): Boolean;
var
  Stream: TBytesStream;
  Objects: TPdfActiveObjects;
  Revision: TPdfSparseRevisionInfo;
  ErrorText: string;
begin
  Stream := TBytesStream.Create(Pdf);
  try
    if not ReadPdfActiveObjects(Stream, TPdfActiveObjectReadOptions.Default,
      Objects, Revision, ErrorText) then
      raise Exception.Create(ErrorText);
    // o catálogo ativo precisa referenciar um AcroForm; um AcroForm inline
    // gravado pelo conversor não carrega entrada /XFA
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

Appearance streams são deliberadamente simples: Helvetica a 10 pontos para texto e valores de escolha, um quadrado contornado para check boxes, um quadrado interno preenchido para o estado de rádio selecionado. Eles tornam o formulário convertido legível em todo lugar, mas não são uma cópia em pixels da renderização XFA original, então fontes, cores e bordas definidas no template não são levadas adiante. Se um formulário precisa ficar idêntico à saída do designer, renderize-o uma vez com a build XFA e compare antes de trocar um pipeline de produção

FAQ

O FlattenPdfXfaToAcroForm precisa da build V8 ou XFA do PDFium?

Não. O FlattenPdfXfaToAcroForm interpreta os packets e grava a revisão com código do PDFium Component, então funciona com a pdfium.dll padrão e não executa nenhum script XFA

O conversor lida com documentos XFA criptografados?

Não diretamente. Uma origem criptografada é rejeitada com um erro, então decifre o documento primeiro e converta a cópia decifrada

Os campos convertidos continuam editáveis?

Sim. A saída contém campos AcroForm reais de texto, check box, rádio, escolha e pushbutton com valores e appearance streams, então usuários podem continuar preenchendo o formulário em qualquer viewer com suporte a AcroForm

Conversão de XFA estático, extração de packets XFA e o runtime XFA com V8 habilitada fazem todos parte do PDFium Component for Delphi and C++Builder, que embrulha o PDFium para projetos Delphi, C++Builder e Lazarus