Műszaki cikk

Miért tűnnek el az XFA mezőszerkesztések mentéskor a PDFiumban Delphihez

A TPdf.SetFocusedFormFieldText a PDFium Componentben a jelenleg fókuszált formmező élő szerkesztőpufferébe ír, és egy XFA-form esetén ez a puffer soha nem éri el a lemezre szerializált datasets csomagot — így egy érték, amit a felhasználó begépel, és amit a kódod elfogadottnak igazol, csendben eltűnik a fájl következő megnyitásakor. Az AcroForm-mezőknek nincs ez a problémájuk: ugyanaz a hívás elköteleződik a mező /V bejegyzésébe abban a pillanatban, amikor a fókusz elmozdul. Egy felhasználó, aki kitölt egy XFA-felvételi formot, ment, és újra megnyitja, hogy azt találja, az összeg mező ismét üres, nem egy renderelési hibába fut bele — a PDFium motor saját maga elérhetővé tett formadat-írási felületének szélébe fut bele

Ez egy szűkebb kérdés, mint egy XFA-form felismerése eleve, vagy JavaScriptjének futtatása: nem az, hogy "támogatja-e a PDFium az XFA-t", és nem az, hogy "hogyan futtatok AcroForm-szkripteket", hanem kifejezetten az, mi történik egy értékkel, miután a SetFocusedFormFieldText sikerről jelent. A rövid válasz az, hogy az AcroForm és az XFA nem ugyanazon formmodell két dialektusa, ami a PDFium írási útvonalát illeti — két formmodell, két teljesen eltérő kapcsolattal aközött, amit egy felhasználó begépel, és amit egy mentés ténylegesen befog, és a kettő összekeverése az, ami egy egysoros API-hívást egy támogatási jeggyé alakít három héttel azután, hogy egy ügyfél pilot-telepítése élesbe megy. Az AcroForm JavaScript cikk megmutatja az egysoros hívást, és kimondja az AcroForm-kontra-XFA eredményt egy kódmegjegyzésben; ez itt ugyanazon az API-n marad, és végigjárja a belső írási útvonalat, a datasets-csomag bizonyítékát, hogy az XFA-írás soha nem landol, hogy miért a résen belül van maga a PDFium, nem a Delphi-kötés, és egy javítsd-a-saját-XML-edet megoldást olyan dokumentumokhoz, amelyeknél a szerkesztésnek túl kell élnie egy mentést

Hogyan írja meg a SetFocusedFormFieldText egy mező értékét?

A TPdf.SetFocusedFormFieldText úgy működik, hogy szimulál egy billentyűzet-szintű szerkesztést, nem úgy, hogy közvetlenül belebök egy értéket a dokumentummodellbe. Belsőleg meghívja a FORM_SelectAllText-et, hogy kijelölje a fókuszált mező aktuális tartalmát, majd a FORM_ReplaceSelection-t, hogy felülírja a kijelölést az új sztringgel — ugyanaz a két művelet, amit egy billentyűzet-vezérelt jelöld-ki-mindent-és-gépelj kiváltana. Mivel az írás a PDFium interaktív szövegszerkesztési útvonalán keresztül megy, nem azt megkerülve, bármely billentyűzet-, formátum-, vagy számítási szkript, amely a mezőhöz van kötve, pontosan úgy sül el, ahogyan egy gépelő ember esetén tenné, ami hasznossá teszi az API-t programozott formkitöltéshez egy olyan megjelenítőben, amely élve tartja a JavaScriptet. Az olvasási oldal megfelelője a FocusedFormFieldText, a FORM_GetFocusedText által alátámasztva, és ugyanazt az élő puffert tükrözi, amit a SetFocusedFormFieldText épp megírt

if Pdf.FocusedFormFieldIndex >= 0 then
begin
  if Pdf.SetFocusedFormFieldText('1284.50') then
    Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
  else
    Log('No field is focused, or it does not accept text');
end
else
  Log('Focus a field first - FocusFormField or a real click');

Miért tartja meg az AcroForm az értéket, és veszíti el az XFA?

Az AcroForm szöveg- és legördülő mezői azért maradnak fenn, mert a PDFium saját form-kitöltési környezete elkötelezi a szerkesztőpuffert helyetted: abban a pillanatban, amikor a mező elveszíti a fókuszt, a puffer beíródik a mező /V bejegyzésébe, ugyanabba a kulcsba, amit minden szabványkövető PDF-olvasó megnéz, hogy megtudja egy mező tárolt értékét. A TPdf.ClearFormFieldFocus — amely a motorháztető alatt a FORM_ForceToKillFocus-t hívja — igény szerint kikényszeríti ezt az elkötelezést, így a kód, amely programozottan állít be egy értéket, nem kell megvárja egy valódi egérkattintást a UI valahol máshol. Ments azonnal utána, és az új szöveg része a dokumentum objektumgráfjának, mielőtt a TPdf.SaveAs egyáltalán futna, mert a /V egy valódi bejegyzés egy valódi mezőszótárban, nem valami, amit utólag csavartak rá

Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus;              // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');

// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50');   // passes

Hol él ténylegesen egy XFA mezőszerkesztés?

Az XFA-mezőknek nincs ilyen bekötése. A szöveg, amit egy felhasználó begépel, egy CPWL_Edit pufferbe landol, amely a PDFium XFA-renderelési és -interakciós rétegéhez tartozik, és annak a rétegnek nincs olyan kódútvonala, amely visszamásolná a puffert a PDF-ben tárolt datasets csomagba. A TPdf.GetXfaDatasets láthatóvá teszi a rést: hívd meg egy XFA-mező szerkesztése előtt és után, és a visszaadott bájtok azonosak, mert a metódus az eredeti csomagot olvassa, amivel a dokumentum megnyílt, soha nem az épp szerkesztett widget élő állapotát. Ebben semmi nem gyorsítótárazási hiba vagy frissítési időzítési probléma — a lemezen lévő datasets-csomag és a memóriabeli szerkesztőpuffer egyszerűen két különböző állapotdarab, amit a PDFium nyilvános API-ja soha nem köt össze

var
  Before, After: TBytes;
begin
  Before := Pdf.GetXfaDatasets;
  Pdf.FocusFormField(FieldIndex);
  Pdf.SetFocusedFormFieldText('1284.50');
  After := Pdf.GetXfaDatasets;
  // Before and After are byte-for-byte identical on an XFA document -
  // the edit never touched the packet GetXfaDatasets reads from
end;

Ez egy PDFium Component-hiba, vagy egy PDFium-korlátozás?

A hiányzó darab magában a PDFiumban ül, nem a fölötte lévő Delphi-kötésben. A PDFium nyilvános API-jának nincs FPDF_SetXFAPacket-je egy frissített csomag beinjektálásához, és nincs FPDF_SaveAsXFA-ja, hogy megkérje az XFA-motort, szerializálja aktuális DOM-ját vissza datasets XML-be egy mentés előtt. Az FPDF_SaveAsCopy — az export, amely a TPdf.SaveAs-t alátámasztja — kiírja a dokumentum objektumgráfot, amivel a PDFium már rendelkezik; nincs kampója, hogy megkérje az XFA-motort, előbb öntse ki élő állapotát, mert az a kampó nem létezik upstream. A PDFium Component nem tud hozzáadni egyeztetést, amit maga a PDFium soha nem valósított meg, és egy házilag barkácsolt DOM-XML szerializáló kiszállítása, amely találgat a PDFium belső XFA-állapotára, rosszabb lenne, mint az őszinte rés: úgy nézne ki, mintha működne, egészen a következő PDFium-verzióig, amely megváltoztat valamit, amit senki nem lát a projekten kívül

Ez a határ ugyanazon v2.13.2-es audit során bukkant fel, amely eleve felépítette a SetFocusedFormFieldText-et. A FORM_ReplaceSelection verziók óta be volt kötve a DLL-import táblába anélkül, hogy valaha is hívták volna Pascal kódból, és az írási útvonal hozzáadása, amely végül használta, az, ami konkréttá tette a perzisztencia-rést ahhoz, hogy dokumentálják, ne csak elméletileg létezzen. Ugyanaz a auditkör egy nem kapcsolódó, de szellemében rokon rést hozott elő: az AcroForm JavaScript csendben letiltva volt a v2.13.0 óta, mert a JS-platform csak az XFA-inicializálási ágon belül volt bekötve, így a közönséges AcroForm-dokumentumok app.alert-tel vagy számított mezőkkel soha nem kaptak szkriptmotort egyáltalán. Az javítható volt — a JS-platform kiterjesztése minden dokumentumra, függetlenül az XFA-tól —, és ugyanabban a verzióban ment ki; az itt tárgyalt perzisztencia-rés a fenti okokból nem volt javítható. A JavaScript-javítást és a körülötte lévő gazda-vétó eseményeket az AcroForm JavaScript futtatása PDFium Componenttel cikk tárgyalja

Mit kellene tenned emiatt Delphiben?

AcroForm-dokumentumoknál a javítás nem több mint jó szokás: hívd meg a ClearFormFieldFocus-t (vagy egyébként mozgasd el a fókuszt) a SaveAs előtt, valahányszor egy értéket programozottan állítottak be, ahelyett hogy feltételeznéd, egy későbbi UI-interakció majd kiváltja neked az elkötelezést. Egy dokumentumnál, amely lehet AcroForm vagy XFA — ami a gyakori eset egy általános célú megjelenítőben —, ellenőrizd a FormType-ot vagy az XFA logikai értéket, mielőtt megígérnéd egy hívónak, hogy egy mentés ragad, és olvasd el az XFA-formák felismerése és XFA-csomagok kinyerése cikket a teljes próbahalmazért, beleértve az XFAF esetet, ahol XFA-tartalom rétegződik egyébként közönséges AcroForm-widgetek fölé, amelyek tiszteletben tartják a /V-t

Egy valódi dinamikus XFA-formnál, ahol a szerkesztett értékeknek túl kell élniük egy mentést, az interaktív szerkesztőpuffer egyáltalán nem a helyes eszköz. A tartós útvonal az, hogy a GetXfaDatasets-et alapvonalként kezeld, ne eredményként: olvasd be egyszer, amikor a dokumentum megnyílik, tartsd meg a saját feljegyzésedet arról, mit változtatott a felhasználó mezőnként — pontosan azokat az értékeket, amiket a UI-d már ismer, mivel a PDFium nem fogja visszaadni neked utólag —, foltozd be azokat magad az alapvonal XML-be, és vezéreld a saját kimenetedet. Egy írás, amely a saját kódod által vezérelt XML-en keresztül megy, túlél egy mentést, amit egy CPWL_Edit puffer soha nem tudna

function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
  NewValue: string): TBytes;
var
  DatasetsXml: string;
begin
  // GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
  // your own helper over your own XML library, nothing PDFium provides
  DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
  DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
  Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;

A rés elkapása, mielőtt egy ügyfél megtenné

A TPdf.SaveAs True-t ad vissza, akár túlélte egy XFA-mezőérték, akár nem, mert a PDFium szemszögéből a mentés valóban sikerült — kiírt minden bájtot, amit kértek tőle. Ez pontosan azzá a fajta hibává teszi ezt, amely átcsúszik egy füstteszten, és elér egy ügyfelet: semmi nem dob kivételt, semmi nem naplóz, a fájl jól megnyílik, csak a konkrét érték rossz. Egy oda-vissza teszt, amely ténylegesen újra megnyitja az elmentett fájlt, és összehasonlítja a mező értékét — vagy összehasonlítja a GetXfaDatasets-et előtte és utána, a korábbi példa szerint — helye van a regressziós tesztkészletben bármely megjelenítőhöz, amely lehetővé teszi a felhasználóknak XFA-tartalom szerkesztését, nem csak azokban az AcroForm-útvonalakban, amelyek alapértelmezetten véletlenül működnek

Ebből semmi nem annyira hiba, amit be kellene jelenteni a PDFium Component ellen, inkább egy határ, amit köré kell tervezni: a SetFocusedFormFieldText pontosan azt teszi, amit a neve mond, mindkét formmodellhez, és az eredménybeli különbség tisztán visszavezethető arra, mihez köti mindegyik, az AcroForm és az XFA, azt a puffert a PDFium oldalán. Az itt hivatkozott API, a fókusz- és mentés-primitívek, és a csomagolvasók a Delphihez és C++Builderhez készült PDFium Komponens részei