PDFium Component записва редактираните XFA стойности на формата точно, през запис и повторно отваряне, когато върти Windows V8 runtime pdfium.v8.dll, доставен в v3.125.2 или по-нов. По-старите runtime-и долепяха line feeds към стойностите на полетата, режеха emoji до несвързан знак от BMP, тихо прескачаха single-stream XFA записите и можеха да погълнат провален финален запис. Един симптом при повторно отваряне изобщо не е дефект на библиотеката: dynamic форма, чиито root subform липсва restoreState="auto", си преизгражда layout-а от шаблона
Бъг репортите за всичко това си приличаха. Клиент попълва XFA форма за искане в Delphi viewer, записва, отваря пак, и нещо е леко настрани. Празна кутия за коментари вече държи празен ред, а след втори запис — два. Име, написано с emoji, се връща с glyph от private-use областта. Никой не получава грешка, а точно това прави тези бъгове скъпи: разминаването излиза наяве седмици по-късно в чужд экспорт
Какво се чупи, когато XFA форма бъде записана и отворена пак?
Четири отделни дефекта в native XFA save пътя причиняваха разминаване на стойностите, и всеки се криеше зад запис, изглеждащ успешен. Две идваха от сериализацията, едно от подредбата на single-stream съхранението и едно от самия PDF writer. Таблицата нанася всеки симптом срещу причината му и срещу release-а, в който PDFium Component го е поправил
| Симптом при повторно отваряне | Причина | Поправено в |
|---|---|---|
| Празно поле държи line feed; стойностите добиват по един нов ред на запис | И двата XFA writer-а вмъкваха layout нови редове след start тагове | v3.125.2, pdfium.v8.dll |
| U+1F642 се връща като U+F642, или emoji-то изчезва от form пакета | 16-битово отрязване в wchar_t при декодиране; surrogate филтриране в form сериализатора | v3.125.2, pdfium.v8.dll |
| Редакциите в single-stream XFA документ просто ги няма | Native save отхвърля stream подредбата, но връщаната стойност беше игнорирана | v3.125.2; коментари и processing инструкции се пазят от v3.126.0 |
| Отрязан файл, макар записът да докладва успех | Последният буфериран запис проваля, след като writer-ът вече е върнал успех | v3.125.2 V8 runtime; v3.125.3 обикновен pdfium.dll |
| Тристранична dynamic форма се отваря наново като две страници | Root subform не иска restoreState="auto" | Авторство на формата, не дефект на библиотеката |
По-ранни текстове заключваха, че редакциите на XFA полета изобщо не могат да се запазят с PDFium, което беше вярно за runtime-ите по онова време. По-новият V8 runtime записва XFA стойностите нативно, така че редакция, направена в живата форма, стига до записания datasets пакет без да си правите surgery върху пакети от ваша страна
Кой PDFium runtime записва XFA стойности?
XFA save точността зависи от native DLL-а, не от Delphi wrapper-а, така че първата проверка е кой runtime реално е заредил процесът ви. PDFium Component доставя по два Windows билда на архитектура: обикновения pdfium.dll, билднат без V8 и XFA, и pdfium.v8.dll, който носи JavaScript двигателя и XFA form runtime-а. Само pdfium.v8.dll може да пусне XFA форма, така че всяка XFA поправка, описана тук, живее там — почвайки от преизградените Win32 и Win64 V8 библиотеки в v3.125.2
Поправката за финалния запис е генеричен PDF writer код, така че има значение и за обикновени документи. v3.125.3 преизгради обикновените pdfium.dll библиотеки, за да носят същата поправка. Споделен source не е доказателство за споделено поведение: докато бинарият не се преизгради, старият DLL пази стария бъг
Втори капан седеше в loader-а. Преди v3.125.2 задаването на EnableV8Engine на True караше binding-а да вземе default името pdfium.v8.dll и да игнорира пълен път в LibraryName. Приложение, сочащо току-що деплойнат runtime, можеше да продължи да зарежда по-старо копие от друга папка. От v3.125.2 насам LibraryName, съдържащ директория, избира точно този файл и в двата режима на двигателя, а липсващ път проваля вместо да се прехвърли към друга вградена библиотека
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// Директория в LibraryName закача точно този файл (v3.125.2 и по-нов);
// ако файлът липсва, зареждането вдига грешка вместо да пада назад
{$IFDEF WIN64}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
PDFium.EnableV8Engine := True;
PDFium.LoadLibrary; // провали се при стартиране, не при първия запис
end;
След отваряне на документ TPdf.XFA ви казва, че файлът съдържа XFA, а TPdf.XfaRuntimeAvailable — че заредената DLL може реално да го изпълни. Ако трябва и да различите static от dynamic форми, TPdf.FormType връща ftXfaFull или ftXfaForeground; статията за разпознаване на XFA форми и извличане на XFA пакети в Delphi покрива това проучване в детайли
Защо записаните XFA полета добиват излишни line feeds?
Записаните XFA полета добиваха line feeds, защото и двата native XFA writer-а — генеричният XML element writer и сериализаторът на form пакет — pretty-print-ваха изхода си с нов ред след start таговете. В повечето XML този whitespace е козметичен. В XFA данните не е: когато datasets пакетът се разпарси пак, текстът между <Comments> и </Comments> е стойността на полето — новият ред включително. Празно поле затова се отваряше наново с един LF, а всеки следващ цикъл запис-отваряне можеше да долепи още един
Очевидната поправка — подрязване на стойностите при зареждане — би била грешна. Потребителите пишат водещи интервали, завършващи интервали и нарочно многоредов текст в XFA полета, а адресен блок или код с фиксирана ширина трябва да оцеляват байт по байт. Поправката в v3.125.2 затова маха само whitespace-а, който самият сериализатор е синтезирал около таговете. Потребителските стойности, съществуващите текстови възли и CDATA секциите минават непипнати, така че " indented" си остава с отстъп, а нарочно празно поле си остава празно
Защо emoji се връща като друг знак?
Emoji се връщаше объркано, защото Windows wchar_t е широк 16 бита, а два декодиращи пътя записваха пълна Unicode скаларна стойност в един-единствен wchar_t. UTF-8 stream декодерът и парсерът за числови символни референции като 🙂 и двамата го правеха. U+1F642, леко усмихнатото лице, не се събира в 16 бита, така че високите битове отпадаха и вместо него излизаше U+F642 — code point в Private Use Area, който повечето шрифтове рисуват като кутия или нищо
Form сериализаторът имаше обратния проблем. Той филтрираше знаците по един wchar_t наведнъж, виждаше две surrogate кодови единици, невалидни поотделно, и ги хвърляше и двете, така че emoji-то изчезваше изцяло от form пакета. В v3.125.2 декодерът изяжда всяка скаларна стойност докрай и извежда правилна surrogate двойка. Когато остане само един изходен слот, държи ниската surrogate на чакане и не докладва край на stream, докато тази единица е още в буфера. UTF-8 последователност, разцепена между read блокове, се пренася към следващото четене вместо да се изхвърля. Form exporter-ът вече държи валидните surrogate двойки заедно, а числовите символни референции също дават правилни двойки
Latin-1 тестови данни никога не показват нищо от това, така че всеки XFA round-trip тест се нуждае от поне един знак от supplementary равнината
Single-stream XFA и записани провали, които никой не е виждал
Single-stream XFA документ губеше редакциите си, защото native save помощникът отхвърляше тази подредба на съхранението, а извикващият го игнорираше провала. ISO 32000-1 §12.7.8 позволява записът /XFA на речника на интерактивната форма да е или масив от имена на пакети и streams, или единичен stream, държащ целия XDP документ. Масивите от пакети са обичайният случай, но единичните streams са съвсем законни, а PDF записът се завършваше сякаш нищо не се е случило, докато данните на формата си оставаха на старите стойности
От v3.125.2 насам V8 runtime-ът обслужва поддържаното single-stream подмножество. Първо той експортира и двата живи пакета, datasets и form, в staging област и ги валидира, и чак тогава заменя съответните пакети в оригиналния XDP. Другите пакети и декларациите на root namespace се пазят. Ако staging провали, персистентният XFA stream изобщо не се пипа и документът пази своята отметка за промяна
XML коментарите и processing инструкциите поискаха допълнителни грижи, защото вътрешният XML DOM ги изхвърля. В v3.125.2 наличието им караше записа да провали направо, вместо съдържание да се губи тихо. v3.126.0 ги запазва: преди парсването всеки коментар или processing инструкция се разменя за маркер, построен от префикс, който не се среща никъде в оригиналния текст. След заменянето на живите пакети всеки маркер трябва да се появи точно веднъж, преди оригиналният токен да бъде възстановен и stream-ът да бъде записан. Токените извън заменените пакети затова пазят текста и реда си — включително токените в prolog-а, template-а и другите пакети
Някои входове се отказват и до днес нарочно, и всеки отказ е изричен провал на записа:
- Коментари или processing инструкции вътре в живите пакети
datasetsилиform, защото оригиналните им позиции не могат да се нанасят върху току-що експортирано съдържание - DTD декларации и XMLDSig подписи, защото презаписването на XDP не може да държи XML подпис валиден
- Невалидно UTF-8 или UTF-16 кодиране, непълни тагове, невалидни символни референции, неизвестни entities и повредени processing инструкции, които се отхвърлят вместо да се поправят тихо
Single-stream изходът е UTF-8 и запазва XML content модела, а не оригиналната байтова подредба или декларацията за кодиране
Последният дефект седеше под XFA. Native file writer-ът буферира изхода в блокове от 32 KB и изплакваше последния частичен блок само в своя деструктор — след като document writer-ът вече е докладвал успех. Disk-full или I/O грешка на този последен блок беше невидима за извикващия. От v3.125.2 в V8 runtime-а и v3.125.3 в обикновения runtime този финален flush е част от резултата на записа, а отметката за XFA модификация се изчиства само след истински успех. От Delphi страната TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean пише във временен файл до целевия и го мества на място само когато записът върне True, така че провален запис оставя предишния файл непокътнат
Защо dynamic XFA форма се отваря наново с по-малко страници?
Dynamic XFA форма се отваря наново с по-малко страници, когато root subform-ът ѝ не декларира restoreState="auto", а това е решение при авторството на формата, не дефект на PDFium Component. В XFA 3.3 restoreState на root subform-а по подразбиране е manual. Под manual XFA процесорът възстановява само ограничено състояние от записания form пакет и оставя останалото на скриптовете на автора. Записаните стойности на полета и бройките на повторяеми subform инстанции пак се връщат, но геометричните свойства, зададени по време на изпълнение, не
Случаят, който показа това, беше тристранична форма, чийто скрипт разраства subform до h="450pt". Записаният form пакет държеше новата височина, стойностите и бройките инстанции. При повторното отваряне обаче layout-ът беше преизграден от височините на шаблона и формата се преля на две страници. Runtime-ът беше прав: шаблонът никога не е искал автоматично възстановяване. Декларирането му върху root subform-а оправя повторното отваряне:
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb" restoreState="auto">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
<medium stock="letter"/>
</pageArea>
</pageSet>
<subform name="Details" layout="tb" w="7.5in">
<!-- полета; скриптовете могат да сменят h или да добавят инстанции по време на работа -->
</subform>
</subform>
</template>
Ако не притежавате шаблона, не го заобикаляйте с кръпки в viewer-а: форма, разчитаща на режим manual, очаква собствените си скриптове да възстановят състоянието. Живата repagination, докато потребителят пише, е отделна тема, разгледана в как PDFium Component следи dynamic XFA броя страници и преместените полета
Как да верифицирате XFA запис в Delphi?
Единствената надеждна проверка на XFA запис е да отворите записания файл наново в свеж екземпляр TPdf и да прочетете записаните данни обратно. TPdf.GetXfaDatasets връща datasets пакета такъв, какъвто е записан в документа, а не живия XFA модел на данните, така че извикването му преди записа показва старите стойности. След повторното отваряне показва точно това, което е написано. Single-stream документ няма отделно назовани пакети: PDFium докладва целия XDP като един пакет с празно име, така че GetXfaPacketByName('datasets') и GetXfaDatasets не връщат нищо, а fallback-ът чете пълния stream чрез GetXfaFormPackets
uses
System.SysUtils, PDFium, FPdfXfa;
function ReadSavedXfaData(const FileName: string): string;
var
Pdf: TPdf;
Packets: TXfaPacketList;
Bytes: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Bytes := Pdf.GetXfaDatasets; // подредба с масив от пакети
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // единичен stream: един неназован пакет
if Length(Packets) = 1 then
begin
SetLength(Bytes, Length(Packets[0].Content));
if Length(Bytes) > 0 then
Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
end;
end;
Result := TEncoding.UTF8.GetString(Bytes); // записаният XDP изход е UTF-8
finally
Pdf.Free;
end;
end;
Рутината за запис после предава чакащата редакция, проверява резултата от SaveAs и сравнява повторно отворената стойност. TPdf.ClearFormFieldFocus убива form фокуса — моментът, в който PDFium предава edit буфера на фокусираното поле. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean попълва фокусираното поле програмно, но разчита на фокус, който wrapper-ът следи чрез FocusFormField, обходяща widget annotations. Dynamic XFA страница обикновено няма такива, така че там текстът обикновено пристига чрез клавиатурен вход в TPdfView, а функцията връща False, когато нито едно проследено поле не е на фокус
function XmlText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
end;
procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
Expected: string);
var
Saved: string;
begin
// Опционално запълване по скрипт; False значи, че нищо проследено поле не е на фокус
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // предава edit буфера
if not Pdf.SaveAs(FileName) then // включва окончателния flush (v3.125.2+)
raise EPdfError.CreateFmt('Saving %s failed', [FileName]);
Saved := ReadSavedXfaData(FileName);
if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
Saved) = 0 then
raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;
Третирайте substring теста като smoke test. Празен елемент може да се сериализира като <Tag/>, атрибути могат да излязат върху data елементи, а escaping-ът отвъд & и < е избор на сериализатора. За production проверки заредете повторно отворения XML с истински XML парсер и сравнете текстовия възел на свързания data елемент. Пуснете проверката и два пъти подред, защото дефектът с новия ред показа пълния си вид чак на второто поколение
Бърза справка: чеклист за XFA save точност
- Деплойнете
pdfium.v8.dllот v3.125.2 или по-нов за XFA форми, и v3.125.3 или по-нов за обикновенияpdfium.dll, така че поправката за финалния запис да е и на двете - Насочете
LibraryNameкъм пълен път и задайтеEnableV8Engineна True; липсващ път проваля вместо да зареди друго копие - Потвърдете
TPdf.XFAиTPdf.XfaRuntimeAvailableслед отварянето на документа - Викайте
ClearFormFieldFocusпредиSaveAs, така че фокусираното поле да бъде предадено - Никога не игнорирайте Boolean резултата на
SaveAs; False оставя предишния файл на място - Верифицирайте чрез повторно отваряне в нов
TPdfи четене наGetXfaDatasets, с fallback къмGetXfaFormPacketsза single-stream XFA - Тествайте с празни стойности, водещи интервали, многоредов текст,
&и знак от supplementary равнината, през две поколения запис - Очаквайте изрични провали на записа за DTD, XMLDSig и коментари вътре в живите пакети на single-stream XFA
- Ако dynamic форма губи изпълнена геометрия при повторно отваряне, проверете root subform-а за
restoreState="auto", преди да подозирате библиотеката
За callback структурата, която XFA runtime-ът очаква от host приложение, вижте FPDF_FORMFILLINFO version 2 и XFA ABI в Delphi. V8 runtime-ът, Delphi и C++Builder wrapper-ът и viewer контролата са всички част от PDFium Component за Delphi и C++Builder, който включва и двата Windows runtime-а за Win32 и Win64