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
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
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
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