Artigo Técnico

Detectar e extrair formulários XFA no Delphi com o PDFium

Sim — o PDFium oferece suporte a formulários XFA, com uma divisão importante. O Componente PDFium pode detectar um formulário XFA e extrair seus pacotes de template (modelo), datasets (conjuntos de dados) e config (configuração) como XML bruto usando a DLL padrão, com zero dependência de tempo de execução. Executar um formulário XFA dinâmico — dispor sua descrição XML em páginas ativas e interativas — é um problema separado que necessita adicionalmente da versão compilada com suporte ao V8. A comunidade continua perguntando se o PDFium "suporta XFA" como se fosse uma capacidade única de sim ou não, e a resposta honesta é que ler dados XFA e renderizar um formulário XFA são duas tarefas muito diferentes com requisitos de dependência muito distintos

Este artigo percorre ambas as metades com o Componente PDFium, a vinculação VCL nativa em torno do PDFium para Delphi e C++Builder. Primeiro o caminho de detecção e extração estática — FormType, a propriedade XFA e os leitores GetXfaPacket* —, depois o caminho de tempo de execução dinâmico com sua restrição do V8, e finalmente os testes de capacidade que permitem falhar de forma amigável quando uma DLL implantada não pode executar o motor

O PDFium suporta formulários XFA?

Enxergue isso como duas capacidades, não uma. A extração estática do XFA está sempre disponível: as exportações de baixo nível FPDF_GetXFAPacket* que dão suporte a GetXfaPacketCount, GetXfaPacketName e GetXfaPacketContent são compiladas em qualquer pdfium.dll minimamente moderna, mesmo em uma versão com o XFA desativado. É por isso que extrair o XML bruto do template, datasets e config de um formulário não exige qualquer dependência de tempo de execução — trata-se de leitura puramente estrutural, sem o motor envolvido. O XFA dinâmico é a outra capacidade: de fato executar o formulário, avaliar seus cálculos e permitir que o motor de layout do XFA realize a paginação. Esse caminho existe apenas em uma compilação do PDFium feita com suporte a XFA sobre o motor JavaScript V8, e é a parte que a maioria das discussões sobre "o PDFium suporta XFA" realmente se refere

As normas definem essa divisão de forma clara. O XFA é definido fora da gramática principal do PDF no Adobe XFA 3.3 (2012), e a especificação PDF 1.7 §8.6.7 descreves como um formulário XFA é transportado dentro de um PDF: os pacotes do formulário residem sob a entrada /XFA do dicionário AcroForm, e um documento que deve ser disposto por um processador XFA antes de significar algo define /NeedsRendering true no catálogo. Esses dois marcadores são exatamente o que o Componente PDFium procura, e exatamente o que você pode usar para identificar o arquivo

Qual é a diferença entre XFA e AcroForm?

Um AcroForm é o formulário PDF clássico: widget annotations — campos de texto, caixas de seleção, botões de opção — ancorados em coordenadas fixas em páginas normais do PDF. A página que você vê é a página no arquivo, e os campos ficam posicionados sobre ela. O XFA adota a abordagem oposta. O formulário interativo é descrito inteiramente em XML e, em um documento XFA completo (dinâmico), as páginas do PDF são efetivamente marcadores de posição — o conteúdo real é gerado no momento da abertura por um motor XFA que lê o modelo XML e o dispõe, expandindo ou reduzindo para caber os dados. É isso que o /NeedsRendering true anuncia: sem um processador XFA, as páginas estáticas podem ficar em branco ou exibir apenas um aviso de "abra em um visualizador compatível"

A própria carga útil do XFA é um conjunto de pacotes nomeados, e três deles contêm tudo o que a maioria das integrações precisa. O pacote template é a definição do formulário — campos, layout, cálculos e scripts. O pacote datasets contém os dados reais da instância, os valores preenchidos nos campos como uma árvore XML. O pacote config carrega instruções de processamento para o motor XFA. Existem outros — localeSet, connectionSet, o contêiner xdp —, mas para auditar um formulário ou migrar seus dados, template mais datasets costuma ser todo o trabalho. Como esses dados são apenas XML residindo em uma stream, lê-los nunca exige executar nada, que é a razão pela qual a extração estática não possui dependências

Detectando um formulário XFA no Delphi

A detecção começa da mesma forma que toda tarefa do Componente PDFium: defina FileName, mude para Active := True e verifique se ele realmente abriu. Active := True nunca gera exceções — um arquivo ausente, senha incorreta ou DLL inexistente apenas deixa Active como False —, de modo que a verificação é obrigatória. Assim que o documento é aberto, FormType indica qual modelo de formulário é utilizado. Dois dos valores de TPdfFormType são XFA: ftXfaFull é um formulário XFA dinâmico completo, do tipo que precisa de renderização, enquanto ftXfaForeground é o XFAF, o subconjunto de primeiro plano onde o conteúdo XFA é desenhado sobre páginas AcroForm normais para que as páginas estáticas permaneçam legíveis. O booleano XFA é um atalho para "este é um documento XFA de qualquer um dos tipos". Se você também trabalha com campos de nível de widget em um documento XFAF ou AcroForm, a mecânica é descrita no guia para navegação de campos de formulário do PDFium

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'application-form.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;                        // danificado, criptografado ou DLL ausente
    case Pdf.FormType of
      ftXfaFull:
        Log('Formulário XFA completo (dinâmico)');
      ftXfaForeground:
        Log('XFAF: XFA em primeiro plano sobre páginas AcroForm estáticas');
      ftAcroForm:
        Log('AcroForm clássico');
    else
      Log('Sem formulário interativo');
    end;
    if Pdf.XFA then
      Log('O documento carrega uma carga útil de pacotes XFA');
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Extraindo pacotes XFA como XML bruto

A extração é a metade de alto valor e livre de dependências. Faça a verificação com XfaStaticReadable — que confirma se as exportações do leitor de pacotes foram resolvidas na DLL carregada — e depois percorra os pacotes por índice. O GetXfaPacketCount retorna quantos pacotes o formulário carrega, o GetXfaPacketName fornece o nome de cada um e o GetXfaPacketContent retorna os bytes brutos que, para os pacotes conhecidos, são XML em UTF-8. Quando você já sabe qual pacote deseja, os auxiliares nomeados GetXfaTemplate, GetXfaDatasets e GetXfaConfig resolvem o nome e entregam o TBytes diretamente. Para um trabalho de migração de dados, o que importa é o GetXfaDatasets: ele fornece os valores dos campos preenchidos como um documento XML que você pode analisar e mapear em seu próprio esquema, o mesmo tipo de trabalho subsequente que você faria após extrair texto de um PDF

procedure ExtractXfaData(Pdf: TPdf; const Dir: string);
var
  I, Count: Integer;
  PacketName: string;
  Content, Datasets: TBytes;
begin
  if not Pdf.XfaStaticReadable then
    Exit;                          // exportações do leitor de pacotes ausentes
  Count := Pdf.GetXfaPacketCount;
  for I := 0 to Count - 1 do
  begin
    PacketName := Pdf.GetXfaPacketName(I);
    Content := Pdf.GetXfaPacketContent(I);   // XML em UTF-8 bruto
    TFile.WriteAllBytes(
      TPath.Combine(Dir, PacketName + '.xml'), Content);
  end;

  // Ou vá direto para os valores dos campos preenchidos por nome:
  Datasets := Pdf.GetXfaDatasets;
  if Length(Datasets) > 0 then
    ParseAndMap(Datasets);
end;

Por que a renderização dinâmica de XFA precisa do motor V8?

A conclusão primeiro: porque um formulário XFA dinâmico é um pequeno programa, não um desenho estático. O pacote template carrega cálculos, validações e scripts de eventos em FormCalc e JavaScript, e o motor de layout do XFA precisa executá-los para decidir como as páginas devem ser exibidas. O PDFium implementa esse motor apenas em uma compilação feita com suporte a XFA sobre o V8, o motor JavaScript do Google — a mesma razão pela qual a DLL habilitada para V8 (pdfium32v8.dll ou pdfium64v8.dll) é sensivelmente maior do que a compilação padrão. Sem o V8, não há quem execute os scripts, portanto, não há páginas renderizadas para exibir, apenas o XML bruto que você já pode ler de forma estática

A seleção dessa compilação tem uma restrição rígida contornável: é uma decisão de processo único e executada uma única vez. Um único processo não pode carregar a pdfium.dll padrão e a compilação habilitada para V8 simultaneamente — elas exportam os mesmos símbolos e o V8 mantém o estado global do isolate —, portanto, o Componente PDFium deve escolher a compilação do V8 antes que a biblioteca seja carregada pela primeira vez. Para tornar isso automático, o LoadDocument verifica previamente o arquivo em busca dos marcadores /XFA e /NeedsRendering true (a mesma verificação exposta como a função independente PdfFileLooksXfa) e define EnableV8Engine := True antes de carregar quando os encontra. Uma vez que uma pdfium.dll comum já esteja residente, a alternância não pode mais fazer efeito, e esse é o caso relatado pelo tempo de execução (runtime) ausente

begin
  // Analisa o arquivo em busca de XFA antes que o PDFium seja carregado pela primeira vez,
  // para que a compilação com suporte a V8 ainda possa ser selecionada. O PdfFileLooksXfa
  // busca por /XFA e /NeedsRendering true sem analisar o arquivo completo.
  if PdfFileLooksXfa('dynamic-form.pdf') then
    EnableV8Engine := True;

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'dynamic-form.pdf';
    Pdf.Active := True;
    if Pdf.Active and Pdf.XFA then
    begin
      if Pdf.XfaRuntimeAvailable then
        RenderXfaPages(Pdf)          // o motor está ativo: layout dinâmico
      else
        ExtractXfaData(Pdf, 'C:\out'); // fallback para os pacotes estáticos
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Tratando um runtime do XFA ausente de forma amigável

Três verificações dizem exatamente qual capacidade XFA você tem, e o código deve checá-las em vez de presumir. A global XfaFeaturesAvailable verifica se as exportações auxiliares do XFA/V8 estão presentes na DLL carregada. A XfaStaticReadable confirma, por documento, que os leitores de pacotes foram resolvidos — o controle para extração. A XfaRuntimeAvailable é a mais forte: ela é true apenas quando este documento específico ativou o motor XFA, significando que ele é XFA, a DLL com V8 foi carregada e a função FPDF_LoadXFA teve sucesso. Um documento pode relatar XFA = True e mesmo assim XfaRuntimeAvailable = False sempre que a DLL implantada carecer de XFA ou a compilação padrão já tiver sido carregada

Quando um documento XFA é aberto, mas o motor não pode ser executado, o Componente PDFium não assume riscos. Ele fixa o formulário no modo estático seguro em vez de solicitar um runtime dinâmico que não pode cumprir, e gera o evento OnXfaRuntimeMissing uma vez para que o host possa reagir — normalmente informando o usuário para reiniciar com a compilação habilitada para V8, enquanto ainda oferece os dados legíveis estáticos nesse ínterim. Configurar esse manipulador é a diferença entre um fallback limpo e um formulário que silenciosamente não mostra nada

procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Disparado uma vez quando um documento XFA é aberto mas o motor não pode rodar:
  // uma pdfium.dll padrão já foi carregada, ou a compilação carece de XFA/V8.
  ShowMessage('Este é um formulário XFA dinâmico. Reinicie com a versão ' +
    'habilitada para V8 para renderizá-lo; seus dados de pacotes ainda são legíveis.');
end;

Seja honesto sobre essa limitação no seu próprio produto também. Se você nunca enviar a DLL habilitada para V8, formulários XFA dinâmicos nunca serão renderizados para os seus usuários — mas você ainda poderá detectá-los, extrair todos os pacotes e retirar os dados enviados, o que é suficiente para a grande classe de tarefas voltadas para extrair dados de formulários legados do governo e de bancos, em vez de apresentá-los de forma interativa. Reserve a compilação V8 mais pesada para os casos que genuinamente necessitam de um formulário dinâmico e preenchível, e deixe que as verificações de capacidade guiem cada documento pelo caminho viável. Se o seu pipeline de extração também manipular arquivos incorporados, a mesma filosofia de leitura aplica-se a anexos de PDF no Delphi

As APIs FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig e as verificações de capacidade mostradas aqui são fornecidas no Componente PDFium para Delphi e C++Builder