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
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
O resto do mapeamento de UI segue as tabelas de field flags da ISO 32000-1 §12.7.4:
textEdit,numericEditedateTimeEditviram/FT /Tx; um campo de texto multilinha recebe a flag Multiline (/Ff 4096, Tabela 228) e um appearance stream dividido por linhapasswordEditvira/FT /Txcom a flag Password (/Ff 8192) e uma aparência que desenha um asterisco por scalar Unicode, não por code unit UTF-16checkButtonvira uma check box/FT /Btncujo valor é/Yesquando o dado atado é1e/Offcaso contrário<exclGroup>vira um pai de rádio/FT /Btn /Ff 32768(Tabela 226) com/V,/DV,/Opte/Kids, mais um widget filho por membro com seu próprio/ASe/APde dois estadosimageEditvira um pushbutton cuja aparência pinta a imagem; um JPEG de baseline é embutido verbatim sob/DCTDecode, enquanto PNG é decodificado e recodificado como pixels/FlateDecodecom um/SMaskextra quando o PNG carrega transparência alfa outRNS
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
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