PDFium Component вече задава FPDF_FORMFILLINFO.version на 2 за всяка form-fill среда, която инициализира, защото версията, която нативен PDFium build приема, е свойство на този build, не на документа, който се отваря. XFA-включен pdfium.v8.dll отказва версия 1 изцяло, така че обикновен AcroForm PDF, отварян през него, се проваливаше в FPDFDOC_InitFormFillEnvironment без никакво XFA на видимо място. Поправката във v3.116.0 е малка, но грешката зад нея е обща и заслужава да бъде назована: полето за протоколна версия описва memory оформлението, което другата страна очаква, и никога не бива да се извежда от това дали случайно се нуждаете от функционалностите, които това оформление носи
Защо FPDFDOC_InitFormFillEnvironment се проваля на обикновен PDF с pdfium.v8.dll?
Средата се проваля, защото XFA-включен PDFium build валидира полето version преди да направи каквото и да е друго, а старата wrapper логика му подаваше 1 винаги, когато текущият документ не е XFA форма. Симптомът в Delphi host е EPdfError, вдигнат от TPdf.InitializeFormFill със съобщение Cannot initialize form fill environment, хвърлен при отваряне на обикновена фактура или данъчна форма, в която няма нищо освен AcroForm текстови полета. Същият файл се отваря добре срещу обикновения pdfium.dll. Същият DLL отваря добре истински XFA документ. Само комбинацията от V8 build и не-XFA документ се чупи — точно комбинацията, в която host каца, след като включи EnableV8Engine, за да получи AcroForm JavaScript, или след като auto-селекцията в LoadDocument вече е ангажирала процеса с pdfium.v8.dll за по-ранен XFA файл. Този ангажимент е процес-обхватен: EnableV8Engine се чете преди първия LoadLibrary, и щом XFA build-ът е зареден, всеки следващ обикновен PDF минава през същата setup на средата срещу същия бинарен файл. Host-ът не е сгрешил нищо; wrapper-ът зададе грешния въпрос, когато попълваше записа. Ако все още решавате кой бинарен файл изобщо да доставяте, бележката ни за разполагане на PDFium DLL и диагностика на load провали покрива избора между обикновен и V8, а тази статия предполага, че V8 build-ът вече е в процеса
Какво обещава действително полето version в FPDF_FORMFILLINFO?
FPDF_FORMFILLINFO.version казва на PDFium кои полета на записа може да чете, а публичният header fpdf_formfill.h завързва приемливите стойности с начина, по който библиотеката е компилирана, а не с документа. Парафразирано, договорът има три части. Версия 1 покрива стабилните callback-и от FFI_Invalidate до FFI_DoGoToAction плюс указателя m_pJsPlatform. Build без XFA модула приема както 1, така и 2, а с 2 вика и допълнителните експериментални callback-и. Build с XFA модула изисква 2, точка, а header-ът повтаря това изискване два пъти, сякаш е очаквал хората да го пропуснат. Никъде договорът не споменава документа. Версията е изказване за записа, който сте алокирали: с 2 вие обещавате, че паметта след m_pJsPlatform съществува и държи или валидни указатели към функции, или NULL
Регионът версия 2 е мястото, където живее цялата XFA механика. Той започва с xfa_disabled — FPDF_BOOL, който header-ът описва като игнориран под версия 2 и смислен само когато XFA модулът е компилиран — и продължава със седемнайсет указателя към функции, FFI_DisplayCaret до FFI_DoURIActionWithKeyboardModifier. Всеки от тях е документиран като изискван за XFA и иначе да се зададе NULL. Тази формулировка е ключът към цялата поправка. NULL не е състояние на грешка за тези слотове; той е документираното състояние за host, който не управлява XFA. Запис, изчистен с FillChar и после маркиран като версия 2, удовлетворява договора на не-XFA build точно толкова добре, колкото запис версия 1, и е единственият запис, който XFA build ще приеме
Старата селекция завърза ABI-то за документа
Дефектът беше един-единствен условен израз, който изглеждаше разумен в изолация. TPdf.InitializeFormFill изчислява флаг RuntimeReady от три факта: документът докладва XFA формов тип чрез TPdf.XFA, XFA string помощниците са разрешени чрез XfaFeaturesAvailable, а V8 export-ите са разрешени чрез V8FeaturesAvailable. Преди v3.116.0 същият флаг избираше и версията
// v3.115.0 и по-рано: ABI версията следваше документа
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
FFormFillInfo.Info.version := 2
else
FFormFillInfo.Info.version := 1;
// ... и клонът runtime-липсващ го заковаваше пак
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
Прочетете го с header-а в ръка и провалът е очевиден. RuntimeReady е false за всеки обикновен AcroForm документ, така че всеки обикновен документ съобщаваше версия 1. На pdfium.dll това е наред. На pdfium.v8.dll, който е XFA-включеният build, PDFium проверява полето, намира го под изискваните 2, и връща нулев FPDF_FORMHANDLE, което CheckPdf превръща в горния exception. Намерението на стария код беше дефанзивно: пази версия 1, така че XFA build никога да не прочете незададените слотове версия 2. Той се защитаваше от проблем, който header-ът вече изключва, и създаде един, за който header-ът изрично предупреждава. Коригираният код решава версията веднъж, отпред, по това какъв е записът физически
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // страж: ползвай статичното page дърво
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// Пълният запис версия 2 е алокиран и изчистен горе. PDFium
// приема версия 2 без XFA и я изисква във всеки XFA-включен
// build, включително когато този документ не съдържа XFA форма.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// RuntimeReady порта XFA callback-ите и xfa_disabled, никога версията.
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
Къде RuntimeReady все още принадлежи: callback-ите и xfa_disabled
RuntimeReady запазва работата си като порт за XFA поведение; той просто вече не пипа оформлението на записа. Callback-ите версия 1 — FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction и останалата част от този блок — се окабеляват безусловно, защото AcroForm и XFA и двете зависят от тях. Седемнайсетте указателя версия 2 се задават само вътре в клона RuntimeReady, заедно с xfa_disabled := 0. Когато документът е XFA, но runtime-ът го няма, записът остава на версия 2 с xfa_disabled на 1 и слотовете версия 2 оставени NULL, а wrapper-ът вдига OnXfaRuntimeMissing, така че host-ът може да предложи рестарт върху pdfium.v8.dll. След като средата съществува, FPDF_LoadXFA се вика само когато RuntimeReady е бил true, и само истинско връщане задава FXfaRuntimeUsable, което TPdf.XfaRuntimeAvailable докладва
if RuntimeReady then
begin
FFormFillInfo.Info.xfa_disabled := 0; // 0 = XFA включено
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;
// ... FFI_UploadTo през FFI_DoURIActionWithKeyboardModifier
end
else if XFA then
begin
// Runtime недостъпен: пази версия 2, остави XFA изключено, кажи на 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;
Два детайла в този блок лесно се объркват, когато пишете собствено обвързване. FXfaPageCountOverride се рестартира на -1 като страж преди всичко останало да се случи, така че PageCount пада обратно към статичното page дърво, докато FFI_PageEvent докладва репагинация; нула там би заявявала тихо празен документ. И всеки от callback-ите версия 2 е статична cdecl рутина, която възстановява притежаващия TPdf от записа и гълта всяко Pascal exception, преди да се върне на PDFium — дисциплината, която бележката ни за втвърдяване на PDFium ABI в Delphi изписва за FFI_OpenFile. Нищо в промяната на версията не облекчава нито едно от двете правила
Безопасна ли е версия 2, когато DLL-ът няма XFA модул?
Да, и причината е в записа, не в обещание от библиотеката. На не-XFA build header-ът казва, че версия 2 кара и експерименталните callback-и да се викат, така че въпросът е какво намира PDFium, когато погледне. TPdfFormFillInfo е packed запис, чиито член Info е пълният FPDF_FORMFILLINFO, включително всяко поле версия 2, а InitializeFormFill изчиства цялото с FillChar, преди да докосне байт. Така че на обикновен pdfium.dll с обикновен документ библиотеката вижда версия 2, зададен xfa_disabled и NULL във всеки експериментален слот — точно състоянието, което header-ът предписва за host, който не имплементира XFA. Няма съкратен запис, от който библиотеката да прочете отвъд, защото записът никога не е бил по-къс от версия 2 на първо място. Старата логика се защитаваше от разминаване на оформление, което Pascal декларацията вече беше премахнала
Границата, която си заслужава да се признае честно, е онази, която записът не може да покрие. Версия 2 на обикновен документ не включва JavaScript, XFA скриптове или някое от host събитията зад тези callback-и. m_pJsPlatform се прикачва само когато V8FeaturesAvailable е true, XFA остава изключено освен ако RuntimeReady не е бил true, а TPdf.XFA продължава да докладва формовия тип от FPDF_GetFormType независимо от това какво средата е договорила. Host, който иска да знае дали динамично XFA наистина ще се рендерира, трябва да продължи да чете XfaRuntimeAvailable след като Active стане true — както препоръчва бележката ни за откриване на XFA форми и извличане на XFA пакети — вместо да извежда каквото и да е от полето версия
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Светва от InitializeFormFill, когато документът е XFA, а зареденият
// pdfium.dll не може да пусне engine-а. Формовата среда все още се отваря,
// защото версия 2 е подадена и в двата случая; само XFA runtime-ът е изключен.
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; // вече не хвърля на обикновен PDF под pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
Протоколна версия и наличност на функционалности са две различни оси
Общото правило, което изпада от тази поправка, е, че поле за версия в callback структура отговаря на въпроса „колко е голям този запис и какво можете да четете от него“, докато откриването на функционалности отговаря на „кой от тези слотове ще направи нещо полезно“. Първото е фиксирано от нативния бинарен файл и от Pascal декларацията, срещу която сте компилирали. Второто варира на документ, на DLL export таблица и на host конфигурация. Сливането на двете в един boolean е изкушаващо, защото XFA случай случайно се нуждае и от двете, но в момента, в който build налага минимална версия, сливането се чупи за всеки документ, който не се нуждае от функционалността. XFA формите, описани в ISO 32000-1 §12.7.8 като XML payload, живеещ до AcroForm речника, са функционалността тук; оформлението на записа е протоколът, а PDFium има правото да настоява за оформлението, преди изобщо да погледне файла. Същата форма се появява навсякъде, където C библиотека верионира своите структури: блок viewer-info, запис render-options, таблица с platform callback-и. Безопасният модел е онзи, който коригираният InitializeFormFill следва. Декларирайте най-новото оформление, което разбирате, изчистете го напълно, задайте версията да съответства на това оформление безусловно, и после нека проверките за възможности решат кои слотове да се попълнят. Ако бъдещ PDFium header добави версия 3, промяната е в декларацията и в това едно задаване, не в документо-зависим клон, който ще бъде грешен за онази комбинация, която никой не е тествал
Коригираната form-fill инициализация се доставя в PDFium Component за Delphi, Lazarus и C++Builder, и се прилага еднакво на Win32 и Win64, тъй като и двата build-а споделят същата декларация на записа. Ако вашето приложение вече избира pdfium.v8.dll за JavaScript-задвижвани AcroForm-и, това е промяната, която му позволява да отвори останалата част от PDF архива ви през същия бинарен файл, без специални случаи за формовата среда