Teknisk artikel

Sæt formularfeltværdier i en loadet PDF med Delphi

HotPDF Delphi Component fylder et eksisterende AcroForm-felt på en loadet PDF gennem THotPDF.SetFormFieldValue, adresseret enten efter nulbaseret feltindeks eller efter fuldt kvalificeret feltnavn. At skrive den nye /V-entry er den lette del; det, der gør kaldet pålideligt på virkelighedens formularer, er, at samme metode også holder tre stykker tilstand konsistente, som er usynlige, indtil de går galt: feltets dekodede identitet, så et ikke-ASCII-navn overhovedet kan findes, /AS-appearance-tilstanden på checkbox- og radio-widgets og /I-valgindeks-arrayet på choice-felter. Den synlige appearance stream er et separat, eksplicit trin gennem EnsureLoadedFieldAppearanceStream

Scenariet er det jordnære: en kunde sender dig deres egen formular, en selvangivelse, et forsikringskrav, en indkøbsordre, nogen byggede i Acrobat for år siden, og din Delphi-applikation skal udfylde den ud fra en database og aflevere en fil, der åbner korrekt alle steder. Du har ingen kontrol over, hvordan formularen blev author'et. Feltnavne kan være UTF-16-kodede, checkbox-eksportværdier kan være 2 i stedet for Yes, og combo boxes kan bruge [export display]-optionspar. Hver af de detaljer har en regel i ISO 32000-1, og hver regel er noget, SetFormFieldValue nu håndterer for dig. Denne artikel handler om, hvad den gør, hvorfor og hvor den stopper. Til søsterproblemet, at skabe felter, der ikke findes endnu, se at tilføje AcroForm-felter til en loadet PDF i Delphi

Hvorfor finder SetFormFieldValue ikke et felt med et ikke-ASCII-navn?

Før v2.752.1 var svaret encoding: feltet bo i filen under et hexadecimalt UTF-16BE-navn, og navnecachen gemte hex-stavemåden i stedet for teksten. ISO 32000-1 §12.7.3.1 definerer det partielle feltnavn /T som en text string, og §7.9.2.2 siger, at en text string kan være UTF-16BE med et indledende FE FF byte order mark. Authoring-værktøjer serialiserer rutinemæssigt sådanne navne som hex-strenge efter §7.3.4.3, så et felt kaldet Straße ankommer som <FEFF005300740072006100DF0065>. Inder i HotPDF holder THPDFStringObject.Value den rå hexadecimale tekst, når endeligt IsHexadecimal er sat, hvilket er præcis, hvad du vil have til en tabsfri roundtrip af den originale dictionary, og præcis hvad du ikke vil have som opslagsnøgle. HPDFLoadedFormTextName adskiller de to bekymringer. Når relationscachen bygges, passerer hver /T-værdi gennem den: er strengobjektet hexadecimalt, gendanner HPDFHexToBytes bytesekvensen; begynder bytesene med FE FF og har lige længde, dekodes payloaden som UTF-16BE og re-encoderes som UTF-8; resultatet joines derefter til sit parent-navn med et punktum og danner det fuldt kvalificerede navn, som §12.7.3.1 beskriver, så en kid kaldet City under en parent kaldet Address registreres som Address.City. Cache-nøglen normaliseres til små bogstaver, hvilket gør, at SetFormFieldValue('address.city', ...) også lykkes; det er en bekvemmelighed ud over standarden, da specifikationen behandler navne som case-sensitive. Afgørende er, at kun cache-nøglen ændres. /T-objektet i feltdictionaryen beholder sin hexadecimale encoding, så at gemme dokumentet ikke omskriver identiteten af et felt, du blot udfyldte

Hvordan HotPDF resolver ikke-ASCII AcroForm-navne: HPDFHexToBytes gendanner UTF-16BE-payloaden bag en hexadecimal /T-streng, FE FF byte order mark dekodes og re-encoderes som UTF-8, og det kvalificerede navn joiner sin parent, så Applicant.FullName og et felt kaldet Straße begge lander i opslagscachen
Kun cache-nøglen ændres: feltdictionaryen beholder sin hexadecimale encoding, opslag normaliseres til små bogstaver som en bekvemmelighed ud over standarden, og at gemme dokumentet omskriver aldrig identiteten af et felt, du blot udfyldte
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Kvalificerede navne dekodes fra UTF-16BE /T-strenge og
    // joines med punktummer, så nestede og ikke-ASCII-navne resolver
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // Værdier, der ikke er Latin-1, rejser som FEFF-præfikseret UTF-16BE hex
    // og skrives som en PDF-hexadecimal streng
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

Hvad skriver SetFormFieldValue faktisk?

Begge overloads kører de samme fem trin: lokaliser feltdictionaryen, skriv /V gennem HPDFSetDictFormValue, forlig choice-valgindekser, markér dictionaryen dirty, forlig button-appearance-tilstande og registrér til sidst feltindekset gennem NoteLoadedFormFieldDirty. Det sidste trin betyder noget, hvis formularen bærer beregningsscripts, for dirty-sættet er det, den parameterløse RecalculateLoadedFormFieldsIncremental-overload indtager for kun at genkøre de beregninger, der transitivt læser et ændret felt. HPDFSetDictFormValue selv er omhyggelig med den objekttype, den erstatter. Er den eksisterende /V et name-objekt, hvilket er, hvad checkbox- og radiofelter bruger til deres eksportværdi, skrives den nye værdi som et name, aldrig som en streng, for PDF-navne er kun-ASCII af konstruktion. Ellers skriver den et strengobjekt og inspicerer den værdi, du gav: en streng, der starter med FEFF, har lige længde og består udelukkende af hex-cifre, behandles som UTF-16BE-wireformen fra §7.9.2.2 og gemmes med IsHexadecimal sat, så den serialiserer som <FEFF...> snarere end som en literal (FEFF...). Det er mekanismen, som City-linjen ovenfor læner sig op ad; enhver anden streng gemmes som en literal streng med de bytes, du gav den, så til almindelig latinsk tekst giver du almindelig tekst

Hvorfor beholder en checkbox sit gamle flueben, efter værdien ændres?

Fordi for et button-felt afgør værdien alene ikke, hvad der tegnes. ISO 32000-1 §12.7.4.2.3 specificerer, at en checkbox-widget bærer en /AS-appearance-tilstand, der navngiver, hvilken stream i /AP /N der i øjeblikket vises, og viewers maler ud fra /AS, ikke ud fra /V. Ændrer du /V til Yes men lader /AS stå på Off, er filen internt modstridende, og flattening vil gerne bage det forældede ucheckede udseende ind på siden, mens formulardataene siger checked. ReconcileLoadedButtonAppearanceStates findes for at lukke det hul: for et felt, hvis /FT er Btn, besøger den feltdictionaryen selv og hver entry i dens /Kids-array, læser on-tilstand-navnet fra /AP /N og omskriver /AS til det navn, når det matcher feltværdien, eller til Off, når det ikke gør

Hvorfor en HotPDF-checkbox beholder sit gamle flueben, når kun /V ændres: viewers maler fra /AS-appearance-tilstanden ind i /AP /N, så ReconcileLoadedButtonAppearanceStates besøger feltet og hvert kid, læser on-tilstand-navnet som den første nøgle andet end Off og omskriver /AS ved match eller til Off ellers
Radio-grupper sammenligner hvert kid med parent-værdien, som InheritedButtonValue gendanner ved at gennemløbe /Parent-kæden, så at sætte gruppen til én eksportværdi tænder præcis den widget og slukker alle søskende

To detaljer fra virkelige formularer formede fixet i v2.752.3. For det første må en normal appearance-dictionary godt indeholde kun on-tilstanden; §12.7.4.2.3 navngiver off-udseendet Off, men authoring-værktøjer udelader ofte dets stream og lader viewer tegne intet. Tidligere kode bailed ud, når dictionaryen holdt færre end to entries, så de single-state checkboxes beholdt lydløst deres gamle flueben. Tjekket er nu simpelthen, at dictionaryen er ikke-tom, og on-tilstand-navnet tages som den første nøgle, der ikke er Off. For det andet er on-tilstand-navnet hvad end forfatteren valgte. Rigtige formularer bruger 2, Yes, On eller et lokaliseret ord, så sammenligningen er op mod den faktiske nøgle, case-insensitivt, aldrig op mod en hardcodet Yes. Radio-knapper tilføjer én mere rynke, beskrevet i §12.7.4.2.4: valget bor i /V på parent-feltet, mens de enkelte kids ejer widgetsene og typisk ikke har nogen /V af egen. Den nestede InheritedButtonValue-helper går derfor op ad /Parent-kæden, op til 64 niveauer, indtil den finder en ikke-tom værdi, så hvert kid sammenlignes med værdien af den gruppe, det tilhører. At sætte parent til ét kids eksportværdi tænder præcis det kid og slukker hvert søskende

// Checkbox: eksportværdien skal matche on-tilstand-nøglen i /AP /N
// (ofte 'Yes', men rigtige formularer bruger '2', 'On' eller hvad som helst andet)
Pdf.SetFormFieldValue('Consent', 'Yes');

// Radio-gruppe: /V skrives på parent; hver kid-widget får
// /AS sat til sit eget eksportnavn eller til Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Rydning af en checkbox: enhver værdi, der matcher ingen on-tilstand, giver /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Choice-felter: holde /I i trit med /V

For en combo box eller list box er /V ikke det eneste sted, et valg registreres. Tabel 231 i §12.7.4.4 definerer /I som et array af nulbaserede indekser ind i /Opt, der identificerer de valgte items, og en viewer, der finder /I pegende på option 0, mens /V navngiver option 3, kan highlighte den forkerte række. Siden v2.754.1 kører HPDFReconcileChoiceSelection inde i hvert SetFormFieldValue-kald og, når den nedarvede /FT er Ch, genopbygger /I ud fra den nye værdi. Rækkefølgen af operationerne er bevidst. Den lokale /I-entry slettes først, uden at røre dens indhold: var det gamle array et indirekte objekt delt med et andet felt, ville det at mutere det in place korruptere det andet felts valg, så rutinen dropper referencen og opretter et frisk direkte array i stedet. Den resolver derefter /Opt gennem /Parent-kæden, da choice-options kan være nedarvet, og skanner entriesene. En bare streng-option sammenlignes direkte; et [export display]-par sammenlignes på sit eksportelement, og et par med færre end to elementer springes over. Begge sider går gennem HPDFLoadedFormTextName, så en hex UTF-16-option matcher en hex UTF-16-værdi uden at du staver dem identisk. Ved det første match skrives et ét-element /I, og skanningen stopper; en skalarværdi erstatter altid enhver tidligere multi-valg, uanset MultiSelect-flaget

Hvordan HotPDF holder et choice-felt konsistent: HPDFReconcileChoiceSelection sletter det lokale /I-array, før det røres, resolver /Opt gennem /Parent-kæden, sammenligner eksport-halvdelen af hver option gennem HPDFLoadedFormTextName, skriver et ét-element /I ved det første match og skriver intet, når en redigerbar combo-værdi ikke har noget indeks
En bare streng-option sammenlignes direkte, og et export display-par på sit eksportelement, mens en værdi uden for /Opt korrekt efterlader intet indeks — et forældet /I, der peger på den forkerte række, ville være værre end intet

Når intet matcher, skrives der slet intet /I. Det er det korrekte udfald for en redigerbar combo box, hvor §12.7.4.4 tillader brugeren at taste en værdi uden for optionslisten; en sådan værdi har intet indeks, og et forældet indeks ville være værre end intet. Det er også, hvad du får, hvis du giver en visnings-etiket i stedet for en eksportværdi til en parret optionsliste, så når en combo box nægter at vise dit valg, så tjek, hvilken halvdel af paret du leverede

// /Opt is [[US United States] [CA Canada] [MX Mexico]]:
// match på eksportværdien, og /I bliver [1]
Pdf.SetFormFieldValue('Country', 'CA');

// Redigerbar combo med en værdi uden for /Opt: /V skrives,
// /I fjernes, og intet indeks fabrikeres
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Værdi og udseende er to separate operationer

SetFormFieldValue rører aldrig appearance streamen af et tekst- eller choice-felt. Efter kaldet holder /V den nye tekst, mens /AP /N stadig maler den gamle, og hvilken af de to en viewer viser, afhænger af, om AcroForm-dictionaryen bærer /NeedAppearances true efter §12.7.3.3, og om viewer ærer den. Har du brug for, at filen renderer den nye værdi i hver reader, inklusive flatteners og thumbnail-generatorer, der ignorerer flaget, så kald EnsureLoadedFieldAppearanceStream med feltindekset. Den bygger et Form XObject ud fra den nedarvede /DA-streng, /Q-quadding, /MaxLen-comb-layoutet og værdien, resolver den navngivne font gennem AcroForm-/DR-ressourcerne, så en Type0-font beholder sin egen descendant font i stedet for at degradere til Helvetica, og returnerer True, når mindst én widget modtog en stream. By-name-overloadet af SetFormFieldValue giver dig intet indeks tilbage, så hent ét gennem GetFormField, som returnerer en THPDFLoadedFormField, du ejer og skal frigøre. Regressionssuiten for v2.752.1-ændringen er eksplicit om denne opdeling: den sætter en værdi, kalder EnsureLoadedFieldAppearanceStream, renderer derefter siden og tjekker, at pixelsene inde i widget-rectanglet ændrede sig, mens pixelsene udenfor ikke gjorde. At verificere, at /V ændrede sig, beviser intet om, hvad en bruger vil se

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // Mal den nye værdi ind i /AP, så viewers, der ignorerer
    // /NeedAppearances, stadig viser den
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

Grænser værd at kende, før du bygger videre på dette

ReconcileLoadedButtonAppearanceStates tester den lokale /FT af den dictionary, du adresserede, så den virker på radio-parenten eller på en checkbox, der bærer sin egen /FT; en kid-widget adresseret alene, med /FT kun på sin parent, forliges ikke gennem den vej. HPDFReconcileChoiceSelection håndterer en enkelt skalarværdi og skriver højst ét indeks; multi-valg list boxes med flere valgte entries ligger uden for, hvad SetFormFieldValue modellerer. Ingen af rutinerne validerer den værdi, du giver, op mod /Opt eller op mod on-tilstand-nøglerne, så en tastefejl giver en Off-checkbox eller en indeks-løs combo i stedet for en exception. Og GetFormFieldValue returnerer den gemte /V-tekst, som den ligger i dictionaryen, hvilket for en hex-kodet værdi betyder den hexadecimale stavemåde, ikke den dekodede tekst

Når værdierne er inde, og udseendene er malet, sidder de to naturlige næste trin på hver sin side af denne operation. At udveksle feltdata med eksterne systemer i bulk, snarere end ét SetFormFieldValue-kald ad gangen, er det, XFDF-import og -eksport i Delphi dækker. Og når den udfyldte formular er endelig og ikke længere skal kunne redigeres, bager flattening af AcroForm- og XFA-felter i Delphi præcis de /AS-tilstande og appearance streams, der er beskrevet her, ind i statisk sideindhold, hvilket er grunden til, at at få dem konsistente før flattening ikke er valgfrit

Loaded-form-redigerings-API'en i denne artikel, inklusive SetFormFieldValue, EnsureLoadedFieldAppearanceStream og den inkrementelle genberegningsgraf, følger med som en del af HotPDF Delphi Component til Delphi og C++Builder