Artigo Técnico

FPDF_FORMFILLINFO versão 2 no Delphi: siga a ABI da DLL

O PDFium Component agora define FPDF_FORMFILLINFO.version como 2 para todo ambiente de preenchimento de formulário que inicializa, porque a versão que um build nativo do PDFium aceita é propriedade desse build, e não do documento sendo aberto. Uma pdfium.v8.dll com XFA recusa a versão 1 de imediato, então um PDF AcroForm comum aberto por ela falhava em FPDFDOC_InitFormFillEnvironment sem nenhum XFA à vista. A correção da v3.116.0 é pequena, mas o erro por trás dela é geral e vale nomear: um campo de versão de protocolo descreve o layout de memória que o outro lado espera, e nunca deve ser derivado do fato de você precisar ou não dos recursos que esse layout carrega

Por que FPDFDOC_InitFormFillEnvironment falha num PDF comum com a pdfium.v8.dll?

O ambiente falha porque um build do PDFium com XFA valida o campo version antes de fazer qualquer outra coisa, e a lógica antiga do wrapper entregava 1 sempre que o documento corrente não era um formulário XFA. O sintoma num host Delphi é um EPdfError levantado de TPdf.InitializeFormFill com a mensagem Cannot initialize form fill environment, lançado ao abrir uma nota fiscal ou um formulário de imposto comum que tenha apenas campos de texto AcroForm. O mesmo arquivo abre sem problema na pdfium.dll simples. A mesma DLL abre um documento XFA de verdade sem problema. Só a combinação do build V8 com um documento não XFA quebra, que é exatamente a combinação em que um host cai depois de ligar EnableV8Engine para ter JavaScript de AcroForm, ou depois que a seleção automática em LoadDocument já comprometeu o processo com a pdfium.v8.dll por causa de um arquivo XFA anterior. Esse compromisso é do processo inteiro: EnableV8Engine é lido antes do primeiro LoadLibrary, e uma vez carregado o build XFA todo PDF comum posterior passa pela mesma configuração de ambiente contra o mesmo binário. O host não fez nada errado; o wrapper perguntou a coisa errada ao preencher o registro. Se você ainda está decidindo qual binário distribuir, nossa nota sobre distribuir a DLL do PDFium e diagnosticar falhas de carga cobre a escolha entre a versão simples e a V8, e este artigo assume que o build V8 já está no processo

Diagrama do PDFium Component das quatro combinações da pdfium.dll simples e da pdfium.v8.dll com XFA contra documentos AcroForm e XFA: um registro versão 1 quebrava só o build V8 com um formulário comum, EPdfError em FPDFDOC_InitFormFillEnvironment, enquanto o registro versão 2 corrigido abre as quatro
Um condicional amarrou a versão da ABI ao documento, então a escolha da ABI V8 para o processo inteiro transformava todo PDF comum posterior numa inicialização de ambiente fracassada

O que o campo version em FPDF_FORMFILLINFO promete de fato?

FPDF_FORMFILLINFO.version diz ao PDFium quais campos do registro ele pode ler, e o header público fpdf_formfill.h amarra os valores aceitáveis a como a biblioteca foi compilada, e não ao documento. Em paráfrase, o contrato tem três partes. A versão 1 cobre os callbacks estáveis de FFI_Invalidate até FFI_DoGoToAction mais o ponteiro m_pJsPlatform. Um build sem o módulo XFA aceita 1 ou 2 e, com 2, também chama os callbacks experimentais adicionais. Um build com o módulo XFA exige 2, ponto final, e o header repete essa exigência duas vezes como se esperasse que as pessoas a ignorassem. Em nenhum lugar o contrato menciona o documento. A versão é uma afirmação sobre o registro que você alocou: com um 2 você promete que a memória depois de m_pJsPlatform existe e guarda ponteiros de função válidos ou NULL

A região da versão 2 é onde mora toda a maquinaria de XFA. Ela começa com xfa_disabled, um FPDF_BOOL que o header descreve como ignorado abaixo da versão 2 e significativo só quando o módulo XFA está compilado, e continua com dezessete ponteiros de função, de FFI_DisplayCaret até FFI_DoURIActionWithKeyboardModifier. Cada um deles é documentado como obrigatório para XFA e, fora disso, como algo a definir como NULL. Esse fraseado é a chave de toda a correção. NULL não é estado de erro para esses slots; é o estado documentado para um host que não está conduzindo XFA. Um registro que foi limpo com FillChar e depois marcado como versão 2 satisfaz o contrato num build sem XFA exatamente tão bem quanto um registro versão 1, e é o único registro que um build XFA aceita

Diagrama do PDFium Component do registro FPDF_FORMFILLINFO no Delphi: a versão 1 cobre os callbacks de FFI_Invalidate até FFI_DoGoToAction mais m_pJsPlatform, a versão 2 acrescenta xfa_disabled e dezessete ponteiros da era FFI_DisplayCaret, FillChar limpa todo byte, e slots NULL são o estado documentado para um host que não está conduzindo XFA
O registro Pascal é sempre o layout completo da versão 2, então um build com XFA o aceita e um build sem XFA simplesmente nunca chama os slots experimentais que ficam NULL

A seleção antiga amarrou a ABI ao documento

O defeito era um único condicional que parecia razoável isolado. TPdf.InitializeFormFill calcula uma flag RuntimeReady a partir de três fatos: o documento reporta um tipo de formulário XFA por TPdf.XFA, os helpers de string de XFA foram resolvidos por XfaFeaturesAvailable, e as exportações V8 foram resolvidas por V8FeaturesAvailable. Antes da v3.116.0 essa mesma flag também escolhia a versão

// v3.115.0 e anteriores: a versão da ABI seguia o documento
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

if RuntimeReady then
  FFormFillInfo.Info.version := 2
else
  FFormFillInfo.Info.version := 1;

// ... e o ramo de runtime ausente a fixava de novo
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Leia isso com o header em mãos e a falha fica óbvia. RuntimeReady é false para todo documento AcroForm comum, então todo documento comum anunciava a versão 1. Na pdfium.dll isso está certo. Na pdfium.v8.dll, que é o build com XFA, o PDFium confere o campo, encontra um valor abaixo do 2 exigido e devolve um FPDF_FORMHANDLE nulo, que o CheckPdf transforma na exceção acima. A intenção do código antigo era defensiva: manter a versão 1 para que um build XFA nunca leia os slots não atribuídos da versão 2. Ele se defendia de um problema que o próprio header descarta e criou um que o header avisa explicitamente. O código corrigido decide a versão uma vez, de antemão, a partir do que o registro fisicamente é

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // sentinela: usa a árvore de páginas estática
  if not FormFill then
    Exit;

  FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
  FFormFillInfo.Pdf := Self;

  // O registro completo da versão 2 é alocado e limpo acima. O PDFium
  // aceita a versão 2 sem XFA e a exige em todo build com XFA
  // habilitado, inclusive quando este documento não contém formulário XFA.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady controla os callbacks de XFA e xfa_disabled, nunca a versão.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Onde RuntimeReady ainda é o lugar certo: os callbacks e xfa_disabled

O RuntimeReady mantém seu papel de portão do comportamento de XFA; simplesmente deixou de mexer no layout do registro. Os callbacks da versão 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction e o resto desse bloco, são ligados incondicionalmente porque AcroForm e XFA dependem deles. Os dezessete ponteiros da versão 2 são atribuídos só dentro do ramo RuntimeReady, junto com xfa_disabled := 0. Quando o documento é XFA mas o runtime não está lá, o registro fica na versão 2 com xfa_disabled em 1 e os slots da versão 2 deixados como NULL, e o wrapper dispara OnXfaRuntimeMissing para que o host possa sugerir reiniciar na pdfium.v8.dll. Depois que o ambiente existe, FPDF_LoadXFA só é chamado quando RuntimeReady era true, e só um retorno true define FXfaRuntimeUsable, que é o que TPdf.XfaRuntimeAvailable reporta

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA habilitado
    FFormFillInfo.Info.FFI_DisplayCaret := FormFillDisplayCaret;
    FFormFillInfo.Info.FFI_GetCurrentPageIndex := FormFillGetCurrentPageIndex;
    FFormFillInfo.Info.FFI_SetCurrentPage := FormFillSetCurrentPage;
    FFormFillInfo.Info.FFI_GotoURL := FormFillGotoURL;
    FFormFillInfo.Info.FFI_GetPageViewRect := FormFillGetPageViewRect;
    FFormFillInfo.Info.FFI_PageEvent := FormFillPageEvent;
    FFormFillInfo.Info.FFI_PopupMenu := FormFillPopupMenu;
    FFormFillInfo.Info.FFI_OpenFile := FormFillOpenFile;
    FFormFillInfo.Info.FFI_EmailTo := FormFillEmailTo;
    // ... de FFI_UploadTo até FFI_DoURIActionWithKeyboardModifier
  end
  else if XFA then
  begin
    // Runtime indisponível: mantém a versão 2, deixa o XFA desligado, avisa o host.
    if Assigned(FOnXfaRuntimeMissing) then
      FOnXfaRuntimeMissing(Self);
  end;

  FFormHandle := FPDFDOC_InitFormFillEnvironment(FDocument, FFormFillInfo.Info);
  CheckPdf(FFormHandle <> nil, 'Cannot initialize form fill environment');
  if RuntimeReady then
    FXfaRuntimeUsable := FPDF_LoadXFA(FDocument) <> 0;

Dois detalhes nesse bloco são fáceis de errar quando você escreve a própria binding. FXfaPageCountOverride é zerado para -1 como sentinela antes de qualquer outra coisa acontecer, para que PageCount caia na árvore de páginas estática até FFI_PageEvent reportar uma repaginação; um zero ali reivindicaria em silêncio um documento vazio. E cada um dos callbacks da versão 2 é uma rotina cdecl estática que recupera o TPdf dono a partir do registro e engole qualquer exceção Pascal antes de voltar para o PDFium, que é a disciplina que nossa nota sobre endurecer a ABI do PDFium no Delphi detalha para FFI_OpenFile. Nada na mudança de versão afrouxa qualquer uma das duas regras

A versão 2 é segura quando a DLL não tem módulo XFA?

É, e a razão está no registro, não numa promessa da biblioteca. Num build sem XFA o header diz que a versão 2 faz os callbacks experimentais serem chamados também, então a pergunta é o que o PDFium encontra quando olha. TPdfFormFillInfo é um registro packed cujo membro Info é o FPDF_FORMFILLINFO completo, incluindo todo campo da versão 2, e InitializeFormFill limpa o conjunto com FillChar antes de tocar num byte. Então numa pdfium.dll simples com um documento comum a biblioteca vê versão 2, xfa_disabled definido e NULL em todo slot experimental, que é precisamente o estado que o header prescreve para um host que não está implementando XFA. Não existe registro truncado para a biblioteca ler além, porque o registro nunca foi menor que a versão 2. A lógica antiga estava defendendo uma incompatibilidade de layout que a própria declaração Pascal já havia eliminado

O limite que vale declarar com honestidade é o que o registro não consegue cobrir. A versão 2 num documento comum não liga JavaScript, script de XFA nem qualquer dos eventos de host por trás desses callbacks. m_pJsPlatform só é anexado quando V8FeaturesAvailable é true, o XFA continua desligado a menos que RuntimeReady fosse true, e TPdf.XFA segue reportando o tipo de formulário de FPDF_GetFormType independentemente do que o ambiente negociou. Um host que queira saber se XFA dinâmico vai de fato renderizar deve continuar lendo XfaRuntimeAvailable depois que Active virar true, como nossa nota sobre detectar formulários XFA e extrair pacotes XFA recomenda, em vez de inferir qualquer coisa do campo de versão

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Dispara de InitializeFormFill quando o documento é XFA mas a pdfium.dll
  // carregada não consegue rodar o motor. O ambiente de formulário ainda abre,
  // porque a versão 2 foi passada nos dois casos; só o runtime de XFA está fora.
  StatusBar.SimpleText :=
    'XFA form detected; restart with pdfium.v8.dll to enable dynamic rendering';
end;

procedure TMainForm.OpenDocument(const FileName: string);
begin
  Pdf.Active := False;
  Pdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
  Pdf.FormFill := True;
  Pdf.FileName := FileName;
  Pdf.Active := True;   // não lança mais exceção num PDF comum sob a pdfium.v8.dll
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Versão de protocolo e disponibilidade de recurso são dois eixos diferentes

A regra geral que sai dessa correção é que um campo de versão numa estrutura de callbacks responde à pergunta de quão grande é este registro e o que você pode ler dele, enquanto a detecção de recurso responde quais desses slots vão fazer algo útil. O primeiro é fixado pelo binário nativo e pela declaração Pascal contra a qual você compilou. O segundo varia por documento, por tabela de exportações da DLL e por configuração do host. Colapsar os dois num único booleano é tentador porque o caso XFA por acaso precisa dos dois, mas no momento em que um build exige uma versão mínima o colapso quebra para todo documento que não precisa do recurso. Formulários XFA, descritos na ISO 32000-1 §12.7.8 como um payload XML que vive ao lado do dicionário AcroForm, são o recurso aqui; o layout do registro é o protocolo, e o PDFium tem o direito de insistir no layout antes mesmo de olhar o arquivo. A mesma forma aparece em qualquer lugar onde uma biblioteca C versiona suas estruturas: um bloco de informações de visualizador, um registro de opções de renderização, uma tabela de callbacks de plataforma. O padrão seguro é o que o InitializeFormFill corrigido segue. Declare o layout mais novo que você entende, limpe-o por completo, defina a versão compatível com esse layout incondicionalmente, e depois deixe as checagens de capacidade decidirem quais slots preencher. Se um header futuro do PDFium acrescentar uma versão 3, a mudança é na declaração e nessa única atribuição, não num ramo dependente do documento que estará errado para a combinação que ninguém testou

Diagrama do PDFium Component separando os dois eixos por trás do FPDF_FORMFILLINFO: a versão de protocolo fixada pelo layout do registro e pelo binário nativo, e a disponibilidade de recurso em que RuntimeReady controla xfa_disabled, dezessete slots da versão 2, FPDF_LoadXFA e m_pJsPlatform por documento e por host
Um campo de versão descreve a memória que o outro lado pode ler, as checagens de capacidade decidem quais slots fazem algo útil, e colapsar os dois num único booleano quebra o build que exige uma versão mínima

A inicialização de preenchimento de formulário corrigida vem no PDFium Component para Delphi, Lazarus e C++Builder, e vale igualmente em Win32 e Win64, já que as duas compilações compartilham a mesma declaração de registro. Se a sua aplicação já seleciona a pdfium.v8.dll para AcroForms movidos a JavaScript, esta é a mudança que a deixa abrir o resto do seu acervo de PDF pelo mesmo binário sem tratar o ambiente de formulário como caso especial