O PDFium Component converte formulários XFA estáticos em campos AcroForm ordinários a partir do Delphi através do FlattenPdfXfaToAcroForm na unidade FPdfDocumentXfaFlatten. A função lê os pacotes template e datasets do XFA, mapeia cada campo absolutamente posicionado para um campo terminal AcroForm com o seu próprio fluxo de aparência, e anexa o resultado como uma revisão incremental, pelo que o PDF convertido abre como um formulário normal em visualizadores que nunca suportaram XFA
A motivação é normalmente um bilhete de suporte em vez de um item de roteiro. Um formulário governamental ou de seguros foi autorado num designer que o gravou como XFA, funciona num leitor desktop, e em todo o resto abre como uma página em branco que diz que o documento requer um visualizador mais recente. O XFA foi descontinuado no PDF 2.0 (ISO 32000-2), browsers e visualizadores móveis nunca o implementaram, e o próprio PDFium só renderiza XFA com a build habilitada para V8. Formulários XFA estáticos, aqueles cujo layout é fixado em tempo de design, transportam geometria suficiente no template para serem reconstruídos como campos AcroForm, e é exatamente essa fatia que este conversor trata
O que produz afinal o FlattenPdfXfaToAcroForm?
O FlattenPdfXfaToAcroForm produz uma revisão AcroForm ao nível de bytes anexada a uma cópia inalterada da origem, nunca uma reescrita do ficheiro original. O PDFium não expõe API pública para criar campos de formulário, pelo que o componente nem sequer passa pelo PDFium para este trabalho: lê a revisão ativa com o seu próprio leitor de objetos, constrói dicionários de campos, anotações widget, XObjects de aparência e um recurso de fonte /Helv como objetos PDF brutos, e serializa-os através de uma atualização incremental esparsa no sentido da ISO 32000-1 §7.5.6
O novo dicionário /AcroForm transporta /Fields, /DA e /DR e nenhuma entrada /XFA. Quando a origem mantém o seu AcroForm como objeto indireto, esse objeto é reescrito no lugar; um dicionário inline é substituído dentro do catálogo. De qualquer forma a revisão ativa já não aponta para os pacotes XFA, pelo que um visualizador que compreende ambos os modelos abre os campos AcroForm em vez de tentar dispor o formulário XFA. Os bytes XFA originais continuam 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; // a predefinição é 512, o intervalo permitido é 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 enche o Report.ErrorText para qualquer entrada rejeitada ou malformada, e restaura a posição do fluxo de origem à saída. O cancelamento é o único caminho de exceção que se propaga: se o Options.CancellationToken disparar, o EPdfOperationCancelled é relançado depois de o relatório ser preenchido. O TPdfXfaFlattenReport também transporta SourceSha256 e OutputSha256, que é cómodo quando um trabalho em lote tem de provar que entrada produziu que ficheiro convertido. O TPdfXfaFlattenOptions.Default define um orçamento de pacote de 8 MiB, 512 campos e um teto de saída de 512 MiB; a origem não pode estar encriptada
Ler os pacotes template e datasets
O PDFium Component aceita ambos os esquemas que a ISO 32000-1 §12.7.8 permite para a entrada /XFA: a forma de matriz que alterna nomes de pacotes e referências de fluxos, e a forma XDP de fluxo único em que um fluxo guarda todo o documento <xdp:xdp>. Os fluxos de pacotes podem ser não comprimidos ou /FlateDecode sem preditor, e a descompressão é delimitada pelo MaxPacketBytes
O caso de fluxo único é onde o tratamento XML ingénuo parte. Os designers costumam escrever <xfa:datasets> com um prefixo de namespace enquanto o template está sem prefixo, pelo que fatiar por nome literal de etiqueta encontra um pacote e perde o outro. O conversor casa nomes de elementos e atributos pelo nome local, depois de uma verificação separada de que as etiquetas de abertura e fecho se emparelham pelo seu nome completo com prefixo. Se só precisa de olhar para os pacotes em vez de os converter, o guia de detetar formulários XFA e extrair os seus pacotes com o PDFium VCL cobre esse caminho só de leitura
Os valores dos campos vêm de dois lugares. Um elemento <value> no template fornece a predefinição, e o pacote datasets fornece os dados preenchidos, vinculados pelo nome do campo. Nomes de campos repetem-se em formulários reais, pelo que o vinculador compara a cadeia de ancestrais <subform> nomeados com o caminho de ancestrais nos dados e toma a correspondência mais longa sem conflito. Quando dois campos folha partilham um nome sob subforms diferentes, o /T AcroForm torna-se SubformPath.Name para que os dois campos permaneçam distintos após a conversão
Por que é que o choiceList precisa de uma flag Combo explícita?
O XFA e o PDF têm predefinições opostas para campos de escolha, pelo que um conversor que não copie flags transforma todos os menus suspensos em caixas de lista. Um <choiceList> XFA com o open="userControl" predefinido ou com open="onEntry" é um menu suspenso; um campo /FT /Ch PDF sem flags é uma caixa de lista com deslocação, e só a flag Combo (posição de bit 18 na Tabela 230 da ISO 32000-1, valor 131072, o mesmo bit que o PDFium chama FPDF_FORMFLAG_CHOICE_COMBO) a torna uma combo box. O conversor por isso define a flag Combo para os casos de menu suspenso e deixa as flags vazias só quando o template diz open="always" ou open="multiSelect", que mantêm a lista permanentemente aberta. Builds anteriores à 3.181.1 trocavam os valores Combo e Pushbutton e procuravam um open="on" não padrão, pelo que formulários achatados com elas valem a pena ser reprocessados
O resto do mapeamento de interface segue as tabelas de flags de campo na ISO 32000-1 §12.7.4:
textEdit,numericEditedateTimeEdittornam-se/FT /Tx; um campo de texto multilinha recebe a flag Multiline (/Ff 4096, Tabela 228) e um fluxo de aparência dividido por linhaspasswordEdittorna-se/FT /Txcom a flag Password (/Ff 8192) e uma aparência que desenha um asterisco por escalar Unicode, não por unidade de código UTF-16checkButtontorna-se uma caixa de verificação/FT /Btncujo valor é/Yesquando os dados vinculados são1e/Offcaso contrário<exclGroup>torna-se um pai de rádio/FT /Btn /Ff 32768(Tabela 226) com/V,/DV,/Opte/Kids, mais um widget filho por membro com o seu próprio/ASe/APde dois estadosimageEdittorna-se um pushbutton cuja aparência pinta a imagem; um JPEG de linha de base é incorporado à letra sob/DCTDecode, enquanto PNG é descodificado e recodificado como píxeis/FlateDecodecom um/SMaskextra quando o PNG transporta transparência alfa outRNS
Valores que não sejam ASCII imprimível simples, incluindo quebras de linha, são escritos como strings <FEFF...> UTF-16BE, pelo que um nome japonês ou um endereço de duas linhas sobrevive à viagem. A saída Fields espelha o que foi escrito, o que a torna um rasto de auditoria cómodo
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 encaminhamento de páginas
A geometria XFA é convertida para o espaço de utilizador PDF virando cada campo contra a altura MediaBox da 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 do fundo. Deslocamentos de elementos <subform> aninhados acumulam-se a caminhar para baixo no template, e todos os campos têm de acabar com largura e altura positivas ou a conversão é rejeitada
As medidas aceitam as unidades que designers reais emitem: números simples e pt, mais in (72 pontos), mm (72 / 25.4), cm (72 / 2.54), pc (12), px (0.75) e mp (0.001, milipontos). A análise é independente de locale, pelo que uma instalação Windows alemã não transforma 8.5in em disparates. Um sufixo de unidade desconhecido é rejeitado em vez de ser lido como pontos, o que deslocaria silenciosamente o campo
O encaminhamento de páginas segue o <pageSet>: elementos <pageArea> irmãos são numerados a partir de um pela ordem de declaração e reclamam os campos aninhados sob eles, enquanto campos sem ancestral <pageArea> ficam na página um. Cada página recebe a sua própria atualização de /Annots. Um campo encaminhado para além da última página do PDF é um erro em vez de um widget largado na página um
Que formulários XFA o conversor recusa?
O conversor recusa tudo o que tenha um layout que não possa ser conhecido sem correr o motor de layout XFA. Isso significa um atributo layout diferente de position num <subform> ou <exclGroup> (fluxo tb, lr-tb e amigos, que é com que os formulários dinâmicos são construídos), qualquer elemento <script>, e qualquer tipo de interface fora dos sete listados acima. Formulários dinâmicos que crescem páginas em tempo de execução pertencem a uma cadeia de ferramentas diferente; o artigo sobre repaginação XFA dinâmica e contagens de páginas no PDFium Component mostra o que esse layout em vivo envolve
A recusa é deliberada. Um subform de fluxo não tem coordenadas fixas, pelo que qualquer "conversão" teria de inventar posições, e um formulário com scripts 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 do que um erro claro no Report.ErrorText. A especificação XFA 3.3 define muito mais do que este subconjunto, e o conversor não finge o contrário
Armadilhas ao escrever e verificar a revisão AcroForm
Dois problemas apareceram ao construir o escritor, e ambos se aplicam a quem produza atualizações incrementais à mão. O primeiro: o catálogo tem de fazer sempre parte do conjunto de atualização, mesmo quando os seus bytes não mudam porque o AcroForm é um objeto indireto reescrito noutro lugar. O escritor de atualização esparsa deriva o /Root do trailer dos objetos que transporta, e uma atualização sem o objeto raiz produz uma revisão que os visualizadores não conseguem ancorar
O segundo diz respeito à verificação. Carregar o ficheiro convertido através do invólucro PDFium despoleta a inicialização do ambiente de formulários assim que o documento tem um /AcroForm, e o pdfium.dll anfitrião na configuração de teste não tinha a exportação FORM_GetTextDirection, pelo que a inicialização falhava antes de um único campo poder ser conferido. As verificações de releitura usam portanto o leitor de objetos do componente, que analisa 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 tem de referenciar um AcroForm; um AcroForm inline
// escrito pelo conversor não transporta entrada /XFA
Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
finally
Stream.Free;
end;
end;
Os fluxos de aparência são deliberadamente simples: Helvetica a 10 pontos para texto e valores de escolha, um quadrado traçado para caixas de verificação, um quadrado interior preenchido para o estado selecionado do rádio. Tornam o formulário convertido legível em todo o lado, mas não são uma cópia ao píxel da renderização XFA original, pelo que fontes, cores e bordas definidas no template não são transportadas. Se um formulário tem de parecer idêntico à saída do designer, renderize-o uma vez com a build XFA e compare antes de mudar um pipeline de produção
FAQ
O FlattenPdfXfaToAcroForm precisa da build V8 ou XFA do PDFium?
Não. O FlattenPdfXfaToAcroForm analisa os pacotes e escreve a revisão com código do PDFium Component, pelo que funciona com o pdfium.dll padrão e não executa qualquer script XFA
O conversor consegue tratar documentos XFA encriptados?
Não diretamente. Uma origem encriptada é rejeitada com um erro, por isso desencripte o documento primeiro e converta a cópia desencriptada
Os campos convertidos continuam editáveis?
Sim. A saída contém campos AcroForm reais de texto, caixa de verificação, rádio, escolha e pushbutton com valores e fluxos de aparência, pelo que os utilizadores podem continuar a preencher o formulário em qualquer visualizador com suporte a AcroForm
Conversão de XFA estático, extração de pacotes XFA e o runtime XFA habilitado para V8 fazem todos parte do PDFium Component for Delphi and C++Builder, que embrulha o PDFium para projetos Delphi, C++Builder e Lazarus