Teknisk artikkel

Hvorfor XFA-feltredigeringer forsvinner ved lagring i PDFium for Delphi

TPdf.SetFocusedFormFieldText i PDFium-komponenten skriver inn i den for øyeblikket fokuserte skjemafeltets levende redigeringsbuffer, og for et XFA-skjema når den bufferen aldri datasets-pakken som serialiseres til disk — så en verdi en bruker taster inn, og koden din bekrefter ble akseptert, er stille borte neste gang filen åpnes. AcroForm-felt har ikke dette problemet: det samme kallet forplikter seg til feltets /V-oppføring i det øyeblikket fokuset flytter seg bort. En bruker som fyller ut et XFA-inntaksskjema, lagrer, og åpner på nytt for å finne beløpsfeltet tomt igjen, treffer ikke en gjengivelsesglitch — de treffer kanten av hva selve PDFium-motoren eksponerer for å skrive skjemadata

Dette er et snevrere spørsmål enn å oppdage et XFA-skjema i utgangspunktet, eller å få JavaScript-en dets til å kjøre: ikke «støtter PDFium XFA» og ikke «hvordan kjører jeg AcroForm-skript» men spesifikt hva som skjer med en verdi etter at SetFocusedFormFieldText rapporterer suksess. Den korte versjonen er at AcroForm og XFA ikke er to dialekter av den samme skjemamodellen så langt PDFiums skrivevei er opptatt; de er to skjemamodeller med to fullstendig forskjellige forhold mellom hva en bruker taster inn og hva en lagring faktisk fanger, og å blande de to sammen er det som gjør ett enkelt API-kall til en support-sak tre uker etter at en kundes pilotutrulling går live. AcroForm-JavaScript-artikkelen viser ett-linjes-kallet og fastslår AcroForm-versus-XFA-utfallet i en kodekommentar; denne holder seg til det samme API-et og går gjennom den interne skrivevveien, datasets-pakke-beviset på at XFA-skrivingen aldri lander, hvorfor gapet sitter i selve PDFium snarere enn Delphi-bindingen, og en lapp-din-egen-XML-omgåelse for dokumenter som trenger redigeringen til å overleve en lagring

Hvordan skriver SetFocusedFormFieldText en feltverdi?

TPdf.SetFocusedFormFieldText fungerer ved å simulere en tastetrykk-nivå-redigering, ikke ved å dytte en verdi direkte inn i dokumentmodellen. Internt kaller den FORM_SelectAllText for å velge det fokuserte feltets gjeldende innhold, deretter FORM_ReplaceSelection for å overskrive valget med den nye strengen — de samme to operasjonene en tastatur-drevet velg-alt-og-skriv ville utløst. Fordi skrivingen går gjennom PDFiums interaktive tekstredigeringsvei i stedet for rundt den, utløses ethvert tastetrykk-, format-, eller beregnings-skript bundet til feltet nøyaktig slik det ville for et menneske som skriver, noe som er det som gjør API-et nyttig for programmatisk skjemautfylling i en fremviser som holder JavaScript levende. Lese-side-motparten er FocusedFormFieldText, støttet av FORM_GetFocusedText, og den reflekterer den samme levende bufferen SetFocusedFormFieldText nettopp skrev

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');

Hvorfor beholder AcroForm verdien og XFA mister den?

AcroForm-tekst- og kombinasjonsfelt overlever fordi PDFiums eget skjema-utfyllings-miljø forplikter redigeringsbufferen for deg: i det øyeblikket feltet mister fokus, skrives bufferen inn i feltets /V-oppføring, den samme nøkkelen enhver konform PDF-leser ser på for å vite et felts lagrede verdi. TPdf.ClearFormFieldFocus — som kaller FORM_ForceToKillFocus under panseret — tvinger frem den forpliktelsen på forespørsel, slik at kode som setter en verdi programmatisk, ikke trenger å vente på et ekte museklikk et annet sted i UI-et. Lagre umiddelbart etter, og den nye teksten er en del av dokumentobjektgrafen før TPdf.SaveAs noensinne kjører, fordi /V er en ekte oppføring i en ekte felt-ordbok, ikke noe boltet på i etterkant

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

Hvor bor en XFA-feltredigering egentlig?

XFA-felt har ingen slik kobling. Teksten en bruker taster inn, havner i en CPWL_Edit-buffer som tilhører PDFiums XFA-gjengivelses- og interaksjonslag, og det laget har ingen kodevei som kopierer bufferen tilbake inn i datasets-pakken lagret i PDF-en. TPdf.GetXfaDatasets gjør gapet synlig: kall den før og etter en redigering på et XFA-felt, og bytene den returnerer, er identiske, fordi metoden leser den opprinnelige pakken dokumentet ble åpnet med, aldri den levende tilstanden til widgeten du nettopp redigerte. Ingenting ved det er en bufring-bug eller et oppfrisknings-tidsproblem — datasets-pakken på disk og redigeringsbufferen i minnet er ganske enkelt to forskjellige biter av tilstand PDFiums offentlige API aldri kobler sammen

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;

Er dette en bug i PDFium-komponenten eller en PDFium-begrensning?

Den manglende biten sitter i selve PDFium, ikke i Delphi-bindingen oppå den. PDFiums offentlige API har ingen FPDF_SetXFAPacket for å injisere en oppdatert pakke og ingen FPDF_SaveAsXFA for å be XFA-motoren serialisere sin gjeldende DOM tilbake til datasets-XML før en lagring. FPDF_SaveAsCopy — eksporten som støtter TPdf.SaveAs — skriver ut dokumentobjektgrafen PDFium allerede har; den har ingen krok for å be XFA-motoren tømme sin levende tilstand først, fordi den kroken ikke finnes oppstrøms. PDFium-komponenten kan ikke legge til avstemming PDFium selv aldri implementerte, og å sende ut en hjemmelaget DOM-til-XML-serialiserer som gjetter på PDFiums interne XFA-tilstand, ville vært verre enn det ærlige gapet: det ville sett ut som det fungerer inntil neste PDFium-versjon endrer noe ingen utenfor prosjektet kan se

Denne grensen dukket opp under den samme v2.13.2-revisjonen som bygde SetFocusedFormFieldText i utgangspunktet. FORM_ReplaceSelection hadde vært bundet i DLL-import-tabellen i flere versjoner uten noensinne å bli kalt fra Pascal-kode, og å legge til skrivevveien som endelig brukte den, er det som gjorde vedvarenhets-gapet konkret nok til å dokumentere snarere enn teoretisk. Den samme revisjonsrunden avdekket et urelatert, men beslektet-i-ånd, gap: AcroForm-JavaScript hadde vært stille deaktivert siden v2.13.0 fordi JS-plattformen bare var koblet opp inne i XFA-initialiseringsgrenen, så vanlige AcroForm-dokumenter med app.alert eller beregnede felt fikk aldri noen skriptmotor i det hele tatt. Det var reparerbart — å utvide JS-plattformen til hvert dokument uansett XFA — og det ble levert i den samme versjonen; vedvarenhets-gapet dekket her var ikke reparerbart, av grunnene ovenfor. JavaScript-fiksen og host-veto-hendelsene rundt den er dekket i å kjøre AcroForm JavaScript med PDFium-komponenten

Hva bør man gjøre med det i Delphi?

For AcroForm-dokumenter er løsningen ikke mer enn en god vane: kall ClearFormFieldFocus (eller flytt fokus bort på annen måte) før SaveAs hver gang en verdi ble satt programmatisk, i stedet for å anta at en senere UI-interaksjon vil utløse forpliktelsen for deg. For et dokument som kan være enten AcroForm eller XFA — noe som er det vanlige tilfellet i en generell fremviser — sjekk FormType eller den boolske XFA før man lover en kaller at en lagring vil holde, og les å oppdage XFA-skjemaer og trekke ut XFA-pakker for hele settet med prober, inkludert XFAF-tilfellet der XFA-innhold er lagt over ellers-vanlige AcroForm-widgeter som faktisk respekterer /V

For et genuint dynamisk XFA-skjema der de redigerte verdiene må overleve en lagring, er den interaktive redigeringsbufferen ikke det riktige verktøyet i det hele tatt. Den varige veien er å behandle GetXfaDatasets som din basislinje, ikke ditt resultat: les den én gang når dokumentet åpnes, hold din egen oversikt over hva brukeren endret felt for felt — nøyaktig de verdiene UI-et ditt allerede har, ettersom PDFium ikke vil gi dem tilbake til deg i etterkant — lapp de inn i basislinje-XML-en selv, og driv din egen utdata. En skriving som går gjennom XML din egen kode kontrollerer, overlever en lagring en CPWL_Edit-buffer aldri kunne

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;

Å fange gapet før en kunde gjør det

TPdf.SaveAs returnerer True uansett om en XFA-feltverdi overlevde eller ikke, fordi fra PDFiums synspunkt lyktes lagringen genuint — den skrev hver byte den ble bedt om å skrive. Det gjør dette til nøyaktig den typen defekt som glir forbi en smoke-test og når en kunde: ingenting kaster et unntak, ingenting logger, filen åpnes greit, bare den spesifikke verdien er feil. En rundtur-test som faktisk åpner den lagrede filen på nytt og sammenligner feltets verdi — eller sammenligner GetXfaDatasets før og etter, i tråd med det tidligere eksempelet — hører hjemme i regresjonstestsuiten for enhver fremviser som lar brukere redigere XFA-innhold, ikke bare AcroForm-veiene som tilfeldigvis fungerer som standard

Ingenting av dette er en defekt å melde inn mot PDFium-komponenten, så mye som en grense å designe rundt: SetFocusedFormFieldText gjør presis hva navnet dets sier for begge skjemamodellene, og forskjellen i utfall spores rent til hva AcroForm og XFA hver kobler den bufferen til på PDFium-siden. API-et, fokus- og lagrings-primitivene, og pakke-leserne referert her, er en del av PDFium-komponenten for Delphi og C++Builder