O PDFium Component passa agora a pôr FPDF_FORMFILLINFO.version a 2 em todos os ambientes de preenchimento de formulário que inicializa, porque a versão que uma compilação nativa do PDFium aceita é uma propriedade dessa compilação e não do documento que está a ser aberto. Um pdfium.v8.dll com XFA recusa a versão 1 sem mais, pelo que um PDF AcroForm simples aberto através dele falhava no FPDFDOC_InitFormFillEnvironment sem que houvesse XFA em lado nenhum. A correção da v3.116.0 é pequena, mas o erro por trás dela é geral e vale a pena nomeá-lo: um campo de versão de protocolo descreve a disposição de memória que o outro lado espera, e nunca deve ser derivado do facto de precisar ou não das funcionalidades que essa disposição transporta
Porque é que o FPDFDOC_InitFormFillEnvironment falha num PDF simples com o pdfium.v8.dll?
O ambiente falha porque uma compilação do PDFium com XFA valida o campo version antes de fazer seja o que for, e a lógica antiga do wrapper dava-lhe um 1 sempre que o documento atual não fosse um formulário XFA. O sintoma num host Delphi é um EPdfError lançado pelo TPdf.InitializeFormFill com a mensagem Cannot initialize form fill environment, disparado ao abrir uma fatura ou um formulário fiscal comum que não tem mais nada além de campos de texto AcroForm. O mesmo ficheiro abre bem contra o pdfium.dll simples. A mesma DLL abre bem um documento XFA a sério. Só a combinação entre a compilação V8 e um documento não XFA se quebra, que é exatamente a combinação em que um host cai depois de ligar o EnableV8Engine para ter JavaScript de AcroForm, ou depois de a seleção automática no LoadDocument já ter comprometido o processo com o pdfium.v8.dll por causa de um ficheiro XFA anterior. Esse compromisso é a nível do processo: o EnableV8Engine é lido antes do primeiro LoadLibrary, e uma vez carregada a compilação XFA, todos os PDFs simples seguintes passam pela mesma configuração de ambiente contra o mesmo binário. O host não fez nada de errado; o wrapper fez a pergunta errada ao preencher o registo. Se ainda está a decidir que binário distribuir, a nossa nota sobre distribuir a DLL do PDFium e diagnosticar falhas de carregamento cobre a escolha entre simples e V8, e este artigo assume que a compilação V8 já está no processo
O que é que o campo version do FPDF_FORMFILLINFO promete ao certo?
O FPDF_FORMFILLINFO.version diz ao PDFium que campos do registo lhe é permitido ler, e o header público fpdf_formfill.h liga os valores aceitáveis à forma 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 a FFI_DoGoToAction mais o ponteiro m_pJsPlatform. Uma compilação sem o módulo XFA aceita 1 ou 2 e, com 2, chama também os callbacks experimentais adicionais. Uma compilação com o módulo XFA exige 2, ponto final, e o header repete essa exigência duas vezes como se contasse que as pessoas a fossem perder. Em lado nenhum o contrato menciona o documento. A versão é uma afirmação sobre o registo que alocou: com um 2 está a prometer que a memória a seguir ao m_pJsPlatform existe e contém ou ponteiros de função válidos ou NULL
É na região da versão 2 que vive toda a maquinaria XFA. Começa com xfa_disabled, um FPDF_BOOL que o header descreve como ignorado abaixo da versão 2 e significativo apenas quando o módulo XFA está compilado, e continua com dezassete ponteiros de função, de FFI_DisplayCaret a FFI_DoURIActionWithKeyboardModifier. Cada um deles está documentado como obrigatório para XFA e caso contrário para ser posto a NULL. Essa formulação é a chave de toda a correção. O NULL não é um estado de erro para essas ranhuras; é o estado documentado para um host que não está a conduzir XFA. Um registo que tenha sido limpo com FillChar e depois marcado como versão 2 satisfaz o contrato numa compilação sem XFA tão bem como um registo de versão 1, e é o único registo que uma compilação XFA aceita
A seleção antiga ligava a ABI ao documento
O defeito era um único condicional que parecia razoável isoladamente. O TPdf.InitializeFormFill calcula um flag RuntimeReady a partir de três factos: o documento reporta um tipo de formulário XFA através de TPdf.XFA, os helpers de string XFA resolveram-se através de XfaFeaturesAvailable, e as exportações V8 resolveram-se através de V8FeaturesAvailable. Antes da v3.116.0, esse mesmo flag escolhia também 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 em falta voltava a fixá-la
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
Leia-o com o header na mão e a falha é óbvia. O RuntimeReady é false em todos os documentos AcroForm simples, pelo que todos os documentos simples anunciavam a versão 1. No pdfium.dll isso é pacífico. No pdfium.v8.dll, que é a compilação com XFA, o PDFium verifica o campo, encontra-o 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 uma compilação XFA nunca leia as ranhuras não atribuídas da versão 2. Defendia contra um problema que o header já descarta e criou um que o header avisa explicitamente. O código corrigido decide a versão uma vez, à cabeça, a partir daquilo que o registo é fisicamente
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // sentinela: usar a árvore de páginas estática
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// O registo completo da versão 2 é alocado e limpo acima. O PDFium
// aceita a versão 2 sem XFA e exige-a em todas as compilações
// com XFA, incluindo quando este documento não contém formulário XFA.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// O RuntimeReady limita os callbacks XFA e o xfa_disabled, nunca a versão.
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
Onde é que o RuntimeReady ainda pertence: os callbacks e o xfa_disabled
O RuntimeReady mantém o seu papel de gate do comportamento XFA; simplesmente deixa de tocar na disposição do registo. 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 o AcroForm e o XFA dependem ambos deles. Os dezassete ponteiros da versão 2 só são atribuídos dentro do ramo RuntimeReady, a par de xfa_disabled := 0. Quando o documento é XFA mas o runtime não está lá, o registo fica na versão 2 com o xfa_disabled a 1 e as ranhuras da versão 2 deixadas a NULL, e o wrapper dispara OnXfaRuntimeMissing para que o host possa sugerir reiniciar com o pdfium.v8.dll. Depois de o ambiente existir, o FPDF_LoadXFA só é chamado quando o RuntimeReady era true, e só um retorno true põe FXfaRuntimeUsable, que é o que o TPdf.XfaRuntimeAvailable reporta
if RuntimeReady then
begin
FFormFillInfo.Info.xfa_disabled := 0; // 0 = XFA ativado
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 a FFI_DoURIActionWithKeyboardModifier
end
else if XFA then
begin
// Runtime indisponível: manter a versão 2, deixar o XFA desativado, avisar 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 pormenores nesse bloco são fáceis de errar quando se escreve a sua própria ligação. O FXfaPageCountOverride é reposto a -1 como sentinela antes de acontecer mais alguma coisa, para que o PageCount recue para a árvore de páginas estática até o 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 registo e engole qualquer exceção Pascal antes de voltar ao PDFium, que é a disciplina que a nossa nota sobre endurecer a ABI do PDFium em Delphi detalha para o FFI_OpenFile. Nada na mudança de versão relaxa qualquer das duas regras
A versão 2 é segura quando a DLL não tem módulo XFA?
Sim, e a razão está no registo e não numa promessa da biblioteca. Numa compilação sem XFA, o header diz que a versão 2 faz com que os callbacks experimentais sejam também chamados, pelo que a questão é o que o PDFium encontra quando olha. O TPdfFormFillInfo é um registo packed cujo membro Info é o FPDF_FORMFILLINFO completo, incluindo todos os campos da versão 2, e o InitializeFormFill limpa tudo com FillChar antes de tocar num byte. Assim, num pdfium.dll simples com um documento simples, a biblioteca vê a versão 2, o xfa_disabled definido e NULL em todas as ranhuras experimentais, que é precisamente o estado que o header prescreve para um host que não implementa XFA. Não há registo truncado para a biblioteca ler para além dele, porque o registo nunca foi mais curto do que a versão 2. A lógica antiga defendia-se de um desencontro de disposição que a declaração Pascal já tinha eliminado
O limite que vale a pena enunciar com honestidade é aquele que o registo não consegue cobrir. A versão 2 num documento simples não liga o JavaScript, o scripting XFA nem nenhum dos eventos do host por trás desses callbacks. O m_pJsPlatform só é anexado quando V8FeaturesAvailable é true, o XFA fica desativado a menos que o RuntimeReady fosse true, e o TPdf.XFA continua a reportar o tipo de formulário a partir do FPDF_GetFormType, independentemente do que o ambiente negociou. Um host que queira saber se o XFA dinâmico vai realmente renderizar deve continuar a ler o XfaRuntimeAvailable depois de o Active passar a true, como recomenda a nossa nota sobre detetar formulários XFA e extrair pacotes XFA, em vez de inferir seja o que for do campo da versão
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Dispara do InitializeFormFill quando o documento é XFA mas o
// pdfium.dll carregado não consegue correr o motor. O ambiente de formulário abre na mesma,
// porque a versão 2 foi passada de qualquer forma; só o runtime XFA é que está desligado.
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; // já não lança exceção num PDF simples sob o pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
Versão de protocolo e disponibilidade de funcionalidades são dois eixos diferentes
A regra geral que sai desta correção é que um campo de versão numa estrutura de callbacks responde à pergunta «qual é o tamanho deste registo e o que é que lhe é permitido ler», enquanto a deteção de funcionalidades responde a «quais dessas ranhuras farão algo de útil». A primeira é fixada pelo binário nativo e pela declaração Pascal com que compilou. A segunda varia por documento, por tabela de exportações da DLL e por configuração do host. Colapsar as duas num único booleano é tentador porque o caso XFA calha a precisar de ambas, mas no momento em que uma compilação impõe uma versão mínima o colapso quebra-se para todos os documentos que não precisam da funcionalidade. Os formulários XFA, descritos na ISO 32000-1 §12.7.8 como um payload XML que vive a par do dicionário AcroForm, são aqui a funcionalidade; a disposição do registo é o protocolo, e o PDFium tem o direito de insistir na disposição antes de alguma vez olhar para o ficheiro. A mesma forma aparece em qualquer sítio onde uma biblioteca C versiona as suas estruturas: um bloco de informação de visualizador, um registo de opções de renderização, uma tabela de callbacks de plataforma. O padrão seguro é o que o InitializeFormFill corrigido segue. Declare a disposição mais recente que compreende, limpe-a por completo, ponha a versão a coincidir com essa disposição incondicionalmente, e depois deixe as verificações de capacidade decidir que ranhuras preencher. Se um futuro header 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 aplica-se tanto em Win32 como em Win64, já que as duas compilações partilham a mesma declaração de registo. Se a sua aplicação já seleciona o pdfium.v8.dll para AcroForms movidos a JavaScript, esta é a mudança que lhe permite abrir o resto do seu arquivo de PDFs através do mesmo binário, sem casos especiais no ambiente de formulário