Uma fatura AcroForm com um punhado de campos de texto e nenhum conteúdo XFA abre normalmente numa pdfium.dll simples, mas o mesmo arquivo pode fazer a FPDFDOC_InitFormFillEnvironment devolver NULL quando o processo está rodando a pdfium.v8.dll com XFA. Os bytes do documento são idênticos nas duas execuções. O que difere é um inteiro: builds de PDFium com XFA exigem que FPDF_FORMFILLINFO.version seja 2, e um binding que grava 1 para documentos sem XFA é mandado embora antes de o PDFium olhar um único callback
Por que a FPDFDOC_InitFormFillEnvironment rejeita um PDF AcroForm comum?
A rejeição vem do binário, e o documento não tem voz nisso. O header de form fill, fpdf_formfill.h no PDFium e sua tradução Pascal em FPdfFormFill.pas, declara que um build compilado com o módulo XFA exige a versão 2, e diz isso duas vezes no mesmo bloco de comentário. Um build sem XFA aceita 1 ou 2. Sob um binding que manda versão 1 para arquivos não XFA, só uma das quatro combinações de binário e documento falha: um registro versão 1 entregue à pdfium.v8.dll junto com um arquivo AcroForm comum. A DLL simples aceita esse registro. A DLL V8 aceita um registro versão 2 para qualquer documento. Um formulário XFA real na DLL V8 nunca recebe versão 1 de um binding que deriva a versão do documento, então a célula que falha é precisamente aquela em que ninguém pensa em testar
Um binding feito à mão vê a FPDFDOC_InitFormFillEnvironment devolver NULL nesse ponto e não tem mais nada em que se apoiar. A TPdf do PDFium Component reportaria um ambiente recusado como um EPdfError durante o Active := True, mas ela sempre grava versão 2, então essa célula nunca aparece ali. Cair nessa célula é fácil. Um host põe a global EnableV8Engine em True porque quer JavaScript de AcroForm, e a partir daí todo PDF do processo passa pelo binário V8. Ou o host nunca toca na flag, mas o primeiro arquivo que ele abre tem uma entrada /XFA no dicionário AcroForm dele ou /NeedsRendering true no catálogo: quando a biblioteca PDFium ainda não foi carregada, a TPdf escaneia o arquivo atrás desses dois marcadores e liga a EnableV8Engine antes de carregar a DLL. De um jeito ou de outro a escolha vale pelo resto do processo, e todo arquivo AcroForm comum aberto depois esbarra no build com XFA. Qual binário distribuir em primeiro lugar está coberto em o guia de deploy da DLL do PDFium e diagnóstico de falhas de carga; tudo abaixo assume que o build V8 já está carregado
O que o campo version no FPDF_FORMFILLINFO promete
O FPDF_FORMFILLINFO.version diz ao PDFium quanta parte do registro ele pode ler. A versão 1 cobre a parte estável: Release, os callbacks de FFI_Invalidate a FFI_DoGoToAction, e o ponteiro m_pJsPlatform. A versão 2 acrescenta uma cauda experimental. Um build sem XFA que receba a versão 2 também vai chamar os callbacks experimentais que encontrar lá; um build com XFA não aceita menos que isso. O header nunca menciona o documento. Ao gravar 2 você está prometendo que a memória depois de m_pJsPlatform existe e que todo slot nela guarda ou uma função cdecl válida ou NULL
Essa cauda começa com xfa_disabled, um FPDF_BOOL ignorado abaixo da versão 2, seguido de dezessete ponteiros de função de FFI_DisplayCaret a FFI_DoURIActionWithKeyboardModifier. Quinze deles, de FFI_DisplayCaret a FFI_PutRequestURL, são documentados como "Required for XFA, otherwise set to NULL". Os dois últimos, FFI_OnFocusChange e FFI_DoURIActionWithKeyboardModifier, são opcionais para todo host, e o header acrescenta que um embedder versão 2 que deixe o FFI_DoURIActionWithKeyboardModifier sem definir tem os cliques de URI redirecionados ao FFI_DoURIAction. Posto tudo junto, NULL é o valor especificado para um host que não conduz XFA. Um registro zerado com FillChar e carimbado com versão 2 cumpre o contrato num build simples exatamente como um registro versão 1 cumpre, e é também o único registro que um build com XFA aceita
O lado de JavaScript segue o mesmo padrão. O m_pJsPlatform não é usado por builds sem V8; num build V8 um ponteiro NULL impede o JavaScript de rodar enquanto o documento renderiza. O registro IPDF_JsPlatform para o qual ele aponta tem o próprio campo version, e o header diz que esse atualmente precisa ser 2
A armadilha: deixar o documento escolher a versão do ABI
O erro é um condicional que soa sensato por si só. Um binding feito à mão já precisa decidir se vale a pena ligar os callbacks XFA, e é tentador deixar o mesmo teste escolher o número da versão
// Uma forma tentadora para um binding feito à mão: a versão segue o documento
FillChar(FormInfo, SizeOf(FormInfo), 0);
IsXfa := FPDF_GetFormType(Doc) in [FORMTYPE_XFA_FULL, FORMTYPE_XFA_FOREGROUND];
if IsXfa and XfaFeaturesAvailable and V8FeaturesAvailable then
FormInfo.version := 2
else
FormInfo.version := 1; // todo documento não XFA recebe 1
FormHandle := FPDFDOC_InitFormFillEnvironment(Doc, FormInfo);
// na pdfium.v8.dll com um arquivo AcroForm comum, FormHandle é nil aqui
A intenção por trás desse branch é defensiva: manter a versão em 1 para que um build com XFA nunca leia slots que o host não preencheu. Mas o FillChar na linha de cima já os preencheu com NULL, que o header define como o valor correto para um host que não está rodando XFA. O branch protege contra um perigo que a declaração já eliminou e anda direto para dentro do que o header alerta. Ele também não pode ser pego testando arquivos XFA, já que esses tomam o caminho versão 2 toda vez
O PDFium Component constrói o registro ao contrário. Sempre que TPdf.FormFill é True, abrir um documento limpa o registro completo, grava versão 2 e xfa_disabled := 1, e só então pergunta o que o documento precisa. Dito como regra para qualquer binding: defina a versão a partir do registro que você declarou, e deixe nada mais tocá-la. Aqui está essa forma para o seu próprio código, com um wrapper THostFormFillInfo declarado mais abaixo
// FormInfo é um campo THostFormFillInfo do seu objeto viewer, então ele sobrevive
// ao form handle; HostInvalidate, HostGetPage e o resto são seus próprios
// callbacks cdecl
FillChar(FormInfo, SizeOf(FormInfo), 0);
FormInfo.Owner := Self;
FormInfo.Info.version := 2; // casa com o layout declarado, sempre
FormInfo.Info.xfa_disabled := 1; // XFA fica desligada a menos que se prove utilizável
FormInfo.Info.FFI_Invalidate := HostInvalidate;
FormInfo.Info.FFI_GetPage := HostGetPage;
FormInfo.Info.FFI_GetCurrentPage := HostGetCurrentPage;
// ... os outros callbacks da versão 1
IsXfa := FPDF_GetFormType(Doc) in [FORMTYPE_XFA_FULL, FORMTYPE_XFA_FOREGROUND];
RuntimeReady := IsXfa and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
begin
FormInfo.Info.xfa_disabled := 0;
FormInfo.Info.FFI_DisplayCaret := HostDisplayCaret;
FormInfo.Info.FFI_PageEvent := HostPageEvent;
// ... os slots restantes da versão 2
end;
FormHandle := FPDFDOC_InitFormFillEnvironment(Doc, FormInfo.Info);
if FormHandle = nil then
raise Exception.Create('Cannot initialize form fill environment');
XfaRunning := RuntimeReady and (FPDF_LoadXFA(Doc) <> 0);
Onde RuntimeReady e V8FeaturesAvailable pertencem
Checagens de capacidade decidem quais slots são preenchidos, e no PDFium Component elas nunca tocam no layout. A TPdf calcula a prontidão a partir de três fatos: o documento reporta um form type XFA através de TPdf.XFA, a global XfaFeaturesAvailable acha os helpers de string XFA exportados pela DLL carregada, e a V8FeaturesAvailable acha as exportações V8. Os callbacks da versão 1 são ligados para todo documento, porque AcroForm e XFA dependem deles. Os dezessete slots da versão 2 são atribuídos, e o xfa_disabled limpo, só quando os três fatos valem. Depois que o ambiente existe, a FPDF_LoadXFA roda só para esse caso, e só um retorno não zero faz a TPdf.XfaRuntimeAvailable reportar True. A FPDF_LoadXFA é exportada até por builds sem o engine XFA e lá simplesmente devolve false, e é por isso que a checagem da exportação sozinha não basta
Quando o documento é XFA mas o runtime falta, o registro fica na versão 2 com xfa_disabled em 1 e a cauda NULL. O ambiente abre, o conteúdo XFA fica estático, e a TPdf dispara OnXfaRuntimeMissing para que o host possa sugerir reiniciar na pdfium.v8.dll. O m_pJsPlatform segue uma terceira regra: é anexado sempre que a V8FeaturesAvailable é verdadeira, para arquivos AcroForm comuns também, e é isso que permite scripts de nível de documento, cálculos de campo e app.alert rodarem sob o build V8. O PageCount lê a árvore de páginas estática até a XFA repaginar o documento através do FFI_PageEvent; nesse ponto o novo total vem da FPDF_GetPageCount e é reportado via OnXfaPageCountChanged
No lado do host, duas propriedades respondem a duas perguntas diferentes. A XFA diz o que o arquivo contém, a partir da FPDF_GetFormType, independentemente do binário. A XfaRuntimeAvailable diz se o engine realmente partiu para este documento. Leia a segunda depois que o Active ficar True, como faz o artigo sobre detectar formulários XFA e extrair pacotes XFA, e deixe o campo de versão completamente fora do raciocínio
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Disparado enquanto o documento abre quando ele é XFA mas a DLL carregada não
// consegue rodar o engine. O ambiente de formulário continua existindo, com XFA desabilitada
StatusBar.SimpleText :=
'XFA form detected; restart with pdfium.v8.dll for 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; // arquivos AcroForm comuns abrem sob a pdfium.v8.dll também
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaNotice; // sua própria rotina de UI
end;
Erros de layout que quebram o registro antes de a versão ser lida
Um número de versão correto só ajuda se os bytes por trás dele estiverem onde o compilador C os pôs. Vários erros de binding produzem um registro que o PDFium lê errado não importa qual versão ele carregue, e um deles fica invisível no Win32. É declarar a FPDF_FORMFILLINFO como packed record. No Win32 todo campo tem quatro bytes, então layouts packed e alinhados por acaso coincidem. No Win64 o struct C preenche quatro bytes depois de version para que o Release caia numa fronteira de 8 bytes, e preenche de novo depois do xfa_disabled; uma declaração Pascal packed descarta os dois vãos e desloca todo ponteiro de callback em quatro bytes. A declaração em FPdfFormFill.pas é um record simples por essa razão
O segundo erro é um registro truncado: uma tradução que para no m_pJsPlatform mas grava versão 2. O PDFium então lê a cauda de qualquer memória que siga o registro. O terceiro é tempo de vida. O header diz que o registro passado à FPDFDOC_InitFormFillEnvironment precisa continuar válido até o form handle devolvido ser fechado, então um registro guardado numa variável local do procedimento que abre o documento é um ponteiro pendente no momento em que esse procedimento retorna. A TPdf mantém o registro dela como campo do componente, embrulhado num registro externo que põe o struct do PDFium no offset zero e um back-pointer para a TPdf dona logo depois, e é assim que todo callback estático reencontra o objeto dele
type
// Errado no Win64: o packing remove o padding que o struct C tem depois
// de version, então Release fica no offset 4 aqui e no offset 8 no PDFium
TBadFormFillInfo = packed record
version: Integer;
Release: Pointer;
// ... campos restantes
end;
// Seu próprio wrapper: o registro do PDFium primeiro, seu back-pointer depois
THostFormFillInfo = record
Info: FPDF_FORMFILLINFO; // offset 0, então pThis faz o cast direto de volta
Owner: TObject;
end;
PHostFormFillInfo = ^THostFormFillInfo;
procedure HostInvalidate(pThis: PFPDF_FORMFILLINFO; page: FPDF_PAGE;
left, top, right, bottom: Double); cdecl;
begin
try
// TMyViewer e InvalidatePdfRect representam a sua própria classe viewer
TMyViewer(PHostFormFillInfo(pThis).Owner).InvalidatePdfRect(
page, left, top, right, bottom);
except
// uma exceção Pascal não deve atravessar os frames C++ do PDFium
end;
end;
Duas outras entradas merecem cuidado. A FFI_GetLocalTime devolve um FPDF_SYSTEMTIME de 16 bytes por valor, e Delphi e MSVC discordam no Win32 sobre onde vai o ponteiro de resultado oculto, então uma implementação Pascal corromperia a FPDF_FORMFILLINFO de quem chamou. O PDFium atual marca o membro como não usado, e a declaração Pascal carrega um comentário para deixá-lo nil; o PDFium Component faz isso. Todo callback que o componente instala captura exceções Pascal e devolve o valor de falha documentado em vez disso, porque o ABI C não tem como carregar uma exceção de volta pela fronteira. O artigo sobre endurecer o ABI do PDFium no Delphi percorre essa disciplina usando a FFI_OpenFile como exemplo
Versão de protocolo e disponibilidade de recurso são dois eixos
Um campo de versão numa estrutura de callbacks responde quão grande o registro é e o que pode ser lido dele. A detecção de recurso responde quais desses slots vão fazer alguma coisa. O primeiro é fixado pelo binário nativo e pela declaração contra a qual você compilou; o segundo varia por documento, por tabela de exportações da DLL e por configuração do host. O caso XFA precisa das duas respostas ao mesmo tempo, o que torna tentador dobrá-las num único booleano, e essa dobra quebra no instante em que um build exige uma versão mínima. Formulários XFA, que a ISO 32000-1 §12.7.8 descreve como um payload XML carregado junto ao dicionário AcroForm, são o recurso desta história. O layout do registro é o protocolo, e o PDFium tem o direito de checar o layout antes de ler o arquivo
A mesma estrutura aparece onde quer que uma biblioteca C versione os structs dela: FPDF_LIBRARY_CONFIG, IPDF_JsPlatform, qualquer tabela de callbacks com um header de tamanho ou versão. Declare o layout mais novo que você entende, limpe-o por completo, grave a versão correspondente incondicionalmente e preencha os slots de acordo com o que o documento e a DLL conseguem fazer de fato. Se um futuro fpdf_formfill.h introduzir a versão 3, o trabalho é uma declaração nova e uma atribuição mudada. Um branch chaveado pelo conteúdo do documento teria de ser revisitado para cada combinação de build e arquivo, e a que ele erra costuma ser a que ninguém abriu
O tratamento de registro descrito aqui faz parte do PDFium Component para Delphi, Lazarus e C++Builder, com a mesma declaração servindo builds Win32 e Win64. Uma aplicação que já distribui a pdfium.v8.dll para AcroForms dirigidos por JavaScript pode abrir o resto do acervo, XFA ou não, por esse único binário sem tratamento especial para o ambiente de formulário