Tehnički članak

PDFium XFA spremanje: novi redovi, emoji i restoreState

PDFium Component uređene vrijednosti XFA forme sprema točno, kroz spremanje i ponovno otvaranje, kad pokreće Windows V8 runtime pdfium.v8.dll isporučen u v3.125.2 ili novijem. Stariji runtimei dodavali su line feedove vrijednostima polja, rezali emoji na nepovezani BMP znak, tiho preskakali single-stream XFA spremanja i mogli su progutati palo konačno pisanje. Jedan simptom pri ponovnom otvaranju uopće nije defekt biblioteke: dinamička forma čiji korijenski subform nema restoreState="auto" gradi svoj layout iz predloška

Bug izvještaji za ovo svi su izgledali isto. Kupac ispuni XFA prijavni obrazac u Delphi pregledniku, spremi, ponovno otvori, i nešto je malo otklonjeno. Prazni okvir za komentare sada drži prazan red, i nakon drugog spremanja drži dva. Ime upisano s emojiom vraća se s private-use glifom. Nitko ne dobije grešku, i upravo to ove bugove čini skupima: odstupanje ispliva tjednima kasnije u tuđem izvozu

Što krene krivo kad se XFA forma spremi i ponovno otvori?

Četiri odvojena defekta u nativnom XFA putu spremanja uzrokovala su odstupanje vrijednosti, i svaki se skrivao iza spremanja koje je izgledalo uspješno. Dva su došla iz serijalizacije, jedno iz single-stream rasporeda pohrane, a jedno iz samog PDF pisača. Tablica svaki simptom mapira u njegov uzrok i u izdanje u kojem ga je PDFium Component popravio

Simptom nakon ponovnog otvaranjaUzrokPopravljeno u
Prazno polje drži line feed; vrijednostima raste novi red po spremanjuOba XFA pisača ubacivala su layout nove redove iza početnih oznakav3.125.2, pdfium.v8.dll
U+1F642 vraća se kao U+F642, ili emoji nestaje iz form paketa16-bitno wchar_t skraćivanje u dekodiranju; filtriranje surrogata u form serijalizatoruv3.125.2, pdfium.v8.dll
Izmjene u single-stream XFA dokumentu jednostavno nestanuNativno spremanje odbilo je raspored toka, ali povratna vrijednost ignorirana jev3.125.2; komentari i instrukcije obrade čuvaju se od v3.126.0
Odrezana datoteka iako je spremanje javilo uspjehKonačno međuspremljeno pisanje palo je nakon što je pisač već vratio uspjehv3.125.2 V8 runtime; v3.125.3 obični pdfium.dll
Dinamička forma od tri stranice otvara se kao dvijeKorijenski subform ne traži restoreState="auto"Pisanje forme, ne defekt biblioteke

Raniji članci zaključili su da se XFA izmjene polja uopće ne mogu očuvati s PDFiumom, što je za runtimee onog vremena bilo točno. Noviji V8 runtime XFA vrijednosti sprema nativno, pa izmjena napravljena u živoj formi stiže u spremljeni datasets paket bez paketne kirurgije na vašoj strani

Koji PDFium runtime sprema XFA vrijednosti?

Vjernost XFA spremanja ovisi o nativnom DLL-u, ne o Delphi omotaču, pa je prva provjera koji je runtime vaš proces stvarno učitao. PDFium Component isporučuje dva Windows builda po arhitekturi: obični pdfium.dll, građen bez V8 i XFA-e, i pdfium.v8.dll, koji nosi JavaScript motor i XFA form runtime. Samo pdfium.v8.dll može pokrenuti XFA formu, pa svaki XFA popravak opisan ovdje živi tamo, počevši s ponovno građenim Win32 i Win64 V8 bibliotekama u v3.125.2

Popravak konačnog pisanja generički je PDF pisačev kod, pa je bitan i za obične dokumente. v3.125.3 ponovno je sagradio obične pdfium.dll biblioteke da nose isti popravak. Zajednički izvorni kod nije dokaz zajedničkog ponašanja: dok se binarna datoteka ne sagradi ponovno, stari DLL zadržava stari bug

Druga zamka sjedila je u učitaču. Prije v3.125.2 postavljanje EnableV8Engine na True tjeralo je vezu da odabere zadano ime pdfium.v8.dll i ignorira punu putanju u LibraryName. Aplikacija usmjerena na svježe uveden runtime mogla je nastaviti učitavati stariju kopiju iz druge mape. Od v3.125.2 LibraryName koji sadrži direktorij bira upravo tu datoteku u bilo kojem modu motora, a nedostajuća putanja pada umjesto da padne natrag na drugu priloženu biblioteku

uses
  System.SysUtils, PDFium;

procedure SelectXfaRuntime;
begin
  // Direktorij u LibraryName pripada ovoj točnoj datoteci (v3.125.2 i novije);
  // ako datoteka nedostaje, učitavanje podiže iznimku umjesto da padne natrag
{$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;  // padaj pri pokretanju, ne pri prvom spremanju
end;

Nakon otvaranja dokumenta, TPdf.XFA govori vam da datoteka sadrži XFA-u, a TPdf.XfaRuntimeAvailable da učitani DLL stvarno može izvršiti. Ako trebate razlikovati i statičke od dinamičkih formi, TPdf.FormType vraća ftXfaFull ili ftXfaForeground; članak o otkrivanju XFA formi i izvlačenju XFA paketa u Delphiju detaljno pokriva to sondiranje

Zašto spremljena XFA polja dobivaju dodatne line feedove?

Spremljena XFA polja dobila je line feedove jer su oba nativna XFA pisača, generički pisac XML elemenata i serijalizator form paketa, pisali izlaz s novim redom iza početnih oznaka. U većini XML-a taj je razmak kozmetički. U XFA podacima nije: kad se datasets paket ponovno parsira, tekst između <Comments> i </Comments> jest vrijednost polja, novi red uključen. Prazno se polje zato otvaralo držeći jedan LF, i svaki daljnji ciklus spremanja i otvaranja mogao je dodati još jedan

PDFium Component dijagram XFA ciklusa spremanja u kojem pisač dodaje novi red iza početnih oznaka, ponovno otvoreni parser LF između Comments oznaka čita kao vrijednost polja, i svako daljnje spremanje nadodaje još jedan line feed dok v3.125.2 ne ukloni samo whitespace sintetiziran od serijalizatora
Jedan ciklus spremanje-otvaranje posađuje prvi line feed i svaki daljnji krug dodaje još jedan, pa je odstupanje puni oblik pokazalo tek na drugoj generaciji

Očigledan popravak, obrezivanje vrijednosti pri učitavanju, bio bi kriv. Korisnici u XFA polja upisuju vodeće razmake, prateće razmake i namjerni višeredni tekst, i blok adrese ili kod fiksne širine mora preživjeti bajt po bajt. Popravak iz v3.125.2 zato uklanja samo whitespace koji je serijalizator sam sintetizirao oko oznaka. Korisničke vrijednosti, postojeći tekstovni čvorovi i CDATA sekcije prolaze netaknuti, pa " indented" ostaje uvučeno, a namjerno prazno polje ostaje prazno

Zašto se emoji vraća kao drugi znak?

Emoji se vraćao krivo jer je Windows wchar_t širok 16 bita, i dva dekodirajuća puta spremala su punu Unicode skalarnu vrijednost u jedan wchar_t. UTF-8 dekoder tokova i parser numeričkih referenci znakova poput &#x1F642; obojica su to radili. U+1F642, blago nasmijano lice, ne stane u 16 bita, pa su visoki bitovi otpali i umjesto njega pojavio se U+F642: kodna točka u Private Use Area koju većina fontova renderira kao kvadratić ili ništa

Form serijalizator imao je suprotni problem. Filtrirao je znakove po jedan wchar_t, vidio dvije surrogate kodne jedinice koje su zasebno neispravne, i bacio ih obje, pa je emoji nestao iz form paketa u potpunosti. U v3.125.2 dekoder svaku skalarnu vrijednost konzumira u potpunosti i ispisuje pravi surrogate par. Kad ostane samo jedan izlazni slot, donji surrogate drži na čekanju i ne javlja kraj toka dok je ta jedinica još u međuspremniku. UTF-8 sekvenca razdijeljena preko blokova čitanja prenosi se u sljedeće čitanje umjesto da se odbaci. Form izvoznik sada drži valjane surrogate parove zajedno, i numeričke reference znakova proizvode točne parove

PDFium Component dijagram rukovanja surrogatima u kojem U+1F642 stiže kao UTF-16 par D83D DE42 i dva defektna puta ga oštećuju: 16-bitni wchar_t dekoderi skalarnu režu na U+F642 u private use području, dok form serijalizator filtrira usamljene surrogate i baca emoji u potpunosti
Windows wchar_t širok je 16 bita, pa je skalar kojem treba surrogate par ili izgubio visoku polovicu ili nestao iz paketa dok oba puta nisu naučila držati parove zajedno

Latin-1 testni podaci nikad ne pokazuju ništa od ovoga, pa svaki XFA test povratnog ciklusa treba najmanje jedan znak iz dopunske ravnine

Single-stream XFA i neuspjesi spremanja koje nitko nije vidio

Single-stream XFA dokument gubio je izmjene jer je nativni helper spremanja odbio taj raspored pohrane, a njegov pozivatelj ignorirao je neuspjeh. ISO 32000-1 §12.7.8 dopušta da unos /XFA rječnika interaktivne forme bude ili polje imena paketa i tokova ili jedan tok koji drži cijeli XDP dokument. Polja paketa uobičajeni su slučaj, ali pojedinačni tokovi savršeno su legalni, i PDF spremanje završilo je kao da se ništa nije dogodilo dok su podaci forme ostali na starim vrijednostima

Od v3.125.2 V8 runtime rukuje podržanim single-stream podskupom. Prvo izvozi oba živa paketa, datasets i form, u staging područje i validira ih, i tek onda zamjenjuje odgovarajuće pakete u izvornom XDP-u. Ostali paketi i deklaracije korijenskog namespaca čuvaju se. Ako staging padne, perzistentni XFA tok nikad se ne dira i dokument zadržava svoju oznaku izmjene

XML komentari i instrukcije obrade trebali su dodatnu njegu jer ih unutarnji XML DOM odbacuje. U v3.125.2 njihova je prisutnost natjerala spremanje da padne odmah, umjesto da sadržaj tiho izgubi. v3.126.0 čuva ih: prije parsiranja svaki komentar ili instrukcija obrade zamjenjuje se markerom sagrađenim iz prefiksa koji se nigdje ne pojavljuje u izvornom tekstu. Nakon što se živi paketi zamijene, svaki se marker mora pojaviti točno jednom prije nego se izvorni token obnovi i tok zapiše. Tokeni izvan zamijenjenih paketa zato zadržavaju svoj tekst i redoslijed, uključujući tokene u prologu, predlošku i ostalim paketima

Neki se unosi i dalje odbijaju namjerno, i svaka je odbijenica izričit neuspjeh spremanja:

  • Komentari ili instrukcije obrade unutar živih paketa datasets ili form, jer se njihove izvorne pozicije ne mogu mapirati u svježe izvezeni sadržaj
  • DTD deklaracije i XMLDSig potpisi, jer prereivanje XDP-a ne može zadržati XML potpis valjanim
  • Neispravno UTF-8 ili UTF-16 kodiranje, nedovršene oznake, neispravne reference znakova, nepoznati entiteti i deformirane instrukcije obrade, koji se odbijaju umjesto da se tiho poprave
PDFium Component single-stream XFA pipeline spremanja u kojem se živi datasets i form paketi izvoze u staging, validiraju, zatim zamjenjuju unutar izvornog XDP-a s komentarima očuvanima kroz markere, dok staging padovi i unosi poput DTD-ova ili XMLDSig-a izričito odbijaju spremanje
Staging izvoz validira se prije nego se bilo što zamijeni, pa palo spremanje ostavlja perzistentni XFA tok netaknutim i dokument zadržava svoju oznaku izmjene

Single-stream izlaz jest UTF-8 i čuva XML model sadržaja, ne izvorni raspored bajtova ni deklaraciju kodiranja

Zadnji defekt sjedio je ispod XFA-e. Nativni pisac datoteka izlaz međusprema u blokove od 32 KB i konačni nepuni blok ispirao je tek u svojem destruktoru, nakon što je pisac dokumenta već javio uspjeh. Greška punog diska ili I/O na tom zadnjem bloku bila je nevidljiva pozivatelju. Od v3.125.2 u V8 runtimeu i v3.125.3 u običnom runtimeu, to konačno ispiranje dio je rezultata spremanja, a XFA oznaka izmjene briše se tek nakon stvarnog uspjeha. Na Delphi strani, TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean piše u privremenu datoteku pokraj ciljane i premješta je na mjesto tek kad spremanje vrati True, pa palo spremanje ostavlja prethodnu datoteku netaknutom

Zašto se dinamička XFA forma otvara s manje stranica?

Dinamička XFA forma otvara se s manje stranica kad njezin korijenski subform ne deklarira restoreState="auto", a to je odluka pri pisanju forme, a ne defekt PDFium Componenta. U XFA 3.3, restoreState na korijenskom subformu po zadanom je manual. Pod manual-om, XFA procesor iz spremljenog form paketa obnavlja samo ograničeno stanje, a ostalo prepušta autorovim skriptama. Spremljene vrijednosti polja i brojevi instanci ponavljajućih subformi i dalje se vraćaju, ali geometrijska svojstva postavljena u vrijeme izvođenja ne

Slučaj koji je ovo izložio bila je forma od tri stranice čija je skripta rasprstila subform na h="450pt". Spremljeni form paket držao je novu visinu, vrijednosti i brojeve instanci. Pri ponovnom otvaranju, međutim, layout je ponovno izgrađen iz visina predloška i forma se prelijevala na dvije stranice. Runtime je bio u pravu: predložak nikad nije tražio automatsku obnovu. Deklariranje na korijenskom subformu popravlja ponovno otvaranje:

<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">
      <!-- polja; skripte mogu mijenjati h ili dodavati instance u izvođenju -->
    </subform>
  </subform>
</template>

Ako ne posjedujete predložak, ne krpite oko njega u pregledniku: forma koja se oslanja na manual mod očekuje da vlastite skripte obnove stanje. Repaginacija uživo dok korisnik tipka zasebna je tema, pokrivena u kako PDFium Component prati dinamičke XFA brojeve stranica i preseljena polja

Kako provjeriti XFA spremanje u Delphiju?

Jedina pouzdana provjera XFA spremanja jest ponovno otvoriti spremljenu datoteku u svježoj instanci TPdf i pročitati spremljene podatke natrag. TPdf.GetXfaDatasets vraća datasets paket onako kako je spremljen u dokumentu, a ne živi XFA model podataka, pa ga zovete li prije spremanja pokazuje stare vrijednosti. Nakon ponovnog otvaranja pokazuje točno ono što je zapisano. Single-stream dokument nema zasebno imenovanih paketa: PDFium cijeli XDP izvještava kao jedan paket s praznim imenom, pa GetXfaPacketByName('datasets') i GetXfaDatasets ne vraćaju ništa, a fallback čita cijeli tok kroz 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;          // layout polja paketa
    if Length(Bytes) = 0 then
    begin
      Packets := Pdf.GetXfaFormPackets;   // jedan tok: jedan neimenovani paket
      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);  // spremljeni XDP izlaz je UTF-8
  finally
    Pdf.Free;
  end;
end;

Spremna rutina zatim predaje zatečenu izmjenu, provjerava rezultat SaveAs i uspoređuje ponovno otvorenu vrijednost. TPdf.ClearFormFieldFocus ubija fokus forme, što je trenutak kad PDFium predaje međuspremnik uređivanja fokusiranog polja. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean popunjava fokusirano polje programski, ali se oslanja na fokus koji omotač prati kroz FocusFormField, koji šeta anotacije widgeta. Dinamička XFA stranica obično ih nema, pa tamo tekst obično stiže kroz tipkovnički unos u TPdfView, a funkcija vraća False kad nijedno praćeno polje nema fokus

function XmlText(const S: string): string;
begin
  Result := StringReplace(S, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
end;

procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
  Expected: string);
var
  Saved: string;
begin
  // Neobavezan skriptirani fill; False znači da nema praćenog polja s fokusom
  if (Pdf.FocusedFormFieldIndex >= 0) and
     not Pdf.SetFocusedFormFieldText(Expected) then
    raise EPdfError.Create('Could not write the focused field');

  Pdf.ClearFormFieldFocus;              // predaj međuspremnik uređivanja
  if not Pdf.SaveAs(FileName) then      // uključuje konačni 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;

Test podniza tretirajte kao smoke test. Prazan element može se serijalizirati kao <Tag/>, atributi se mogu pojaviti na podatkovnim elementima, i escapiranje izvan & i < izbor je serijalizatora. Za produkcijske provjere učitajte ponovno otvoreni XML pravim XML parserom i usporedite tekstovni čvor vezanog podatkovnog elementa. Pokrenite provjeru i dvaput zaredom, jer je defekt novog reda puni oblik pokazao tek na drugoj generaciji

Brza referenca: kontrolni popis vjernosti XFA spremanja

  • Isporučujte pdfium.v8.dll iz v3.125.2 ili novijeg za XFA forme, a v3.125.3 ili noviji za obični pdfium.dll, da popravak konačnog pisanja bude u oba
  • Usmjerite LibraryName na punu putanju i postavite EnableV8Engine na True; nedostajuća putanja pada umjesto da učita drugu kopiju
  • Potvrdite TPdf.XFA i TPdf.XfaRuntimeAvailable nakon otvaranja dokumenta
  • Zovite ClearFormFieldFocus prije SaveAs da se fokusirano polje predaje
  • Nikad ne ignorirajte Boolean rezultat SaveAs-a; rezultat False ostavlja prethodnu datoteku na mjestu
  • Provjerite ponovnim otvaranjem u novi TPdf i čitanjem GetXfaDatasets, s padom natrag na GetXfaFormPackets za single-stream XFA
  • Testirajte s praznim vrijednostima, vodećim razmacima, višerednim tekstom, & i znakom iz dopunske ravnine, kroz dvije generacije spremanja
  • Očekujte izričite neuspjehe spremanja za DTD-ove, XMLDSig i komentare unutar živih paketa single-stream XFA-e
  • Ako dinamička forma izgubi geometriju izvođenja pri ponovnom otvaranju, provjerite korijenski subform za restoreState="auto" prije nego posumnjate na biblioteku

Za strukturu povratnih poziva koju XFA runtime očekuje od host aplikacije pogledajte FPDF_FORMFILLINFO verziju 2 i XFA ABI u Delphiju. V8 runtime, Delphi i C++Builder omotač i kontrola preglednika svi su dio PDFium Componenta za Delphi i C++Builder, koji uključuje oba Windows runtimea za Win32 i Win64