Technický článek

Nastavení hodnot polí formuláře v načteném PDF v Delphi

HotPDF Delphi Component vyplní existující pole AcroForm na načteném PDF přes THotPDF.SetFormFieldValue, adresované buď zero-based indexem pole, nebo plně kvalifikovaným názvem pole. Zapsání nové položky /V je ta snadná část; co z toho volání dělá spolehlivé na reálných formulářích, je to, že tatáž metoda taky drží v souladu tři stavy, které jsou neviditelné, dokud se nepokazí: dekódovanou identitu pole, aby se jméno mimo ASCII vůbec našlo, stav appearance /AS na checkbox a radio widgetech a pole výběrových indexů /I na choice polích. Viditelný appearance stream je oddělený, explicitní krok přes EnsureLoadedFieldAppearanceStream

Scénář je ten všední: zákazník vám pošle vlastní formulář, daňové přiznání, pojistnou událost, objednávku, kterou si někdo před lety postavil v Acrobatu, a vaše Delphi aplikace ji musí naplnit z databáze a vrátit soubor, který se všude otevře správně. Nemáte žádnou kontrolu nad tím, jak formulář vznikl. Názvy polí můžou být kódované UTF-16, exportní hodnoty checkboxů můžou být 2 místo Yes a combo boxy můžou používat páry voleb [export display]. Každý z těch detailů má pravidlo v ISO 32000-1 a každé pravidlo je něco, co teď SetFormFieldValue obstará za vás. Tenhle článek je o tom, co dělá, proč a kde končí. Pro sourozenecký problém vytváření polí, která ještě neexistují, viz přidávání polí AcroForm na načtené PDF v Delphi

Proč SetFormFieldValue nenajde pole s názvem mimo ASCII?

Před v2.752.1 byla odpověď kódování: pole bydlelo v souboru pod hexadecimálním názvem UTF-16BE a name cache ukládal hex pravopis místo textu. ISO 32000-1 §12.7.3.1 definuje částečný název pole /T jako textový řetězec a §7.9.2.2 říká, že textový řetězec může být UTF-16BE s úvodním byte order markem FE FF. Autorské nástroje rutinně serializují takové názvy jako hex řetězce podle §7.3.4.3, takže pole pojmenované Straße přijde jako <FEFF005300740072006100DF0065>. Uvnitř HotPDF drží THPDFStringObject.Value surový hexadecimální text, kdykoli je nastavené IsHexadecimal, což je přesně to, co chcete pro bezztrátový round trip původního slovníku, a přesně to, co nechcete jako lookup klíč. HPDFLoadedFormTextName odděluje ty dvě starosti. Když se staví cache vztahů, prochází skrz ni každá hodnota /T: pokud je string objekt hexadecimální, HPDFHexToBytes obnoví bajtovou sekvenci; pokud bajty začínají FE FF a mají sudou délku, payload se dekóduje jako UTF-16BE a znovu zakóduje jako UTF-8; výsledek se pak spojí s rodičovským názvem tečkou do plně kvalifikovaného názvu, jaký popisuje §12.7.3.1, takže dítě pojmenované City pod rodičem Address se registruje jako Address.City. Cache klíč se normalizuje na malá písmena, takže i SetFormFieldValue('address.city', ...) projde; to je pohodlí nad rámec standardu, protože specifikace bere jména jako case-sensitive. A hlavně: mění se jen cache klíč. Objekt /T ve slovníku pole si drží své hexadecimální kódování, takže uložení dokumentu nepřepíše identitu pole, které jste jen vyplnili

Jak HotPDF rozřešuje názvy AcroForm mimo ASCII: HPDFHexToBytes obnoví payload UTF-16BE za hexadecimálním řetězcem /T, byte order mark FE FF se dekóduje a znovu zakóduje jako UTF-8 a kvalifikovaný název se spojí s rodičem, takže Applicant.FullName i pole pojmenované Straße přistanou v lookup cache
Mění se jen cache klíč: slovník pole si drží své hexadecimální kódování, lookupy se normalizují na malá písmena jako pohodlí nad rámec standardu a uložení dokumentu nikdy nepřepíše identitu pole, které jste jen vyplnili
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Kvalifikované názvy se dekódují z řetězců /T UTF-16BE a
    // spojují tečkami, takže vnořené i ne-ASCII názvy se rozřeší
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // Hodnoty, které nejsou Latin-1, cestují jako hex UTF-16BE s prefixem FEFF
    // a zapisují se jako PDF hexadecimální řetězec
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

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

Co SetFormFieldValue doopravdy zapisuje?

Oba overloady běží týmiž pěti kroky: najít slovník pole, zapsat /V přes HPDFSetDictFormValue, vysrovnat výběrové indexy choice, označit slovník dirty, vysrovnat stavy appearance tlačítek a nakonec zaznamenat index pole přes NoteLoadedFormFieldDirty. Ten poslední krok má význam, pokud formulář nese výpočetní skripty, protože dirty set je to, co konzumuje bezparametrový overload RecalculateLoadedFormFieldsIncremental, aby znovu pustil jen výpočty, které tranzitivně čtou změněné pole. Samotné HPDFSetDictFormValue je obezřetné ohledně typu objektu, který nahrazuje. Pokud stávající /V je name objekt, což používají pole checkbox a radio pro svou exportní hodnotu, nová hodnota se zapisuje jako name, nikdy jako řetězec, protože PDF názvy jsou z konstrukce jen ASCII. Jinak zapíše string objekt a prozkoumá hodnotu, kterou jste podali: řetězec, který začíná FEFF, má sudou délku a skládá se jen z hex číslic, se bere jako drátová podoba UTF-16BE z §7.9.2.2 a ukládá se s nastaveným IsHexadecimal, takže se serializuje jako <FEFF...> místo literálu (FEFF...). To je mechanismus, na který spoléhá řádka City výše; jakýkoli jiný řetězec se ukládá jako literální řetězec s bajty, které jste mu dali, takže pro plain text v latince podáváte plain text

Proč checkbox po změně hodnoty drží svou starou značku?

Protože u pole tlačítka sama hodnota nerozhoduje o tom, co se kreslí. ISO 32000-1 §12.7.4.2.3 specifikuje, že widget checkboxu nese stav appearance /AS pojmenovávající, který stream v /AP /N je zrovna zobrazený, a prohlížeče kreslí z /AS, ne z /V. Změníte-li /V na Yes a necháte /AS na Off, soubor je interně rozporný a flattenování rádo zapeče zaostalý nezaškrtnutý vzhled do stránky, zatímco form data říkají zaškrtnuto. ReconcileLoadedButtonAppearanceStates existuje, aby tu mezeru zavřel: u pole, jehož /FT je Btn, navštíví samotný slovník pole i každou položku jeho pole /Kids, přečte název on stavu z /AP /N a přepíše /AS na ten název, když odpovídá hodnotě pole, nebo na Off, když neodpovídá

Proč checkbox HotPDF drží svou starou značku, když se mění jen /V: prohlížeče kreslí ze stavu appearance /AS do /AP /N, takže ReconcileLoadedButtonAppearanceStates navštíví pole i každé dítě, přečte název on stavu jako první klíč jiný než Off a přepíše /AS při zásahu nebo na Off jinak
Radio skupiny srovnávají každé dítě s rodičovskou hodnotou, kterou obnoví InheritedButtonValue projitím řetězce /Parent, takže nastavení skupiny na jednu exportní hodnotu zapne přesně ten widget a každého sourozence vypne

Dva detaily z reálných formulářů tvarovaly opravu ve v2.752.3. Za prvé, normálnímu appearance slovníku se dovoluje obsahovat jen on stav; §12.7.4.2.3 pojmenovává off vzhled Off, ale autorské nástroje jeho stream často vynechávají a nechají prohlížeč nekreslit nic. Dřívější kód odpochyboval, když slovník držel méně než dvě položky, takže ty jedno-stavové checkboxy potichu držely svou starou značku. Kontrola je teď prostě to, že slovník není prázdný, a název on stavu se bere jako první klíč, který není Off. Za druhé, název on stavu je cokoli, co autor zvolil. Reálné formuláře používají 2, Yes, On nebo lokalizované slovo, takže srovnání je proti skutečnému klíči, bez ohledu na velikost písmen, nikdy proti natvrdo zapsanému Yes. Radio buttony přidávají ještě jednu vrásku, popsanou v §12.7.4.2.4: výběr bydlí v /V na rodičovském poli, zatímco jednotlivá děti vlastní widgety a typicky nemají žádné vlastní /V. Vnořený helper InheritedButtonValue proto jde nahoru po řetězci /Parent, až do 64 úrovní, dokud nenajde neprázdnou hodnotu, takže každé dítě se srovnává s hodnotou skupiny, do níž patří. Nastavení rodiče na exportní hodnotu jednoho dítěte zapne přesně to dítě a každého sourozence vypne

// Checkbox: exportní hodnota musí odpovídat klíči on stavu v /AP /N
// (často 'Yes', ale reálné formuláře používají '2', 'On' nebo cokoli jiného)
Pdf.SetFormFieldValue('Consent', 'Yes');

// Radio skupina: /V se zapisuje na rodiče; každý widget dítěte dostane
// /AS nastavené na vlastní exportní název nebo na Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Vymazání checkboxu: jakákoli hodnota, která neodpovídá žádnému on stavu, dá /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Choice pole: držení /I v kroku s /V

U combo boxu nebo list boxu není /V jediné místo, kde se výběr zaznamenává. Tabulka 231 v §12.7.4.4 definuje /I jako pole zero-based indexů do /Opt, které identifikuje vybrané položky, a prohlížeč, který najde /I mířící na volbu 0, zatímco /V pojmenovává volbu 3, může zvýraznit špatnou řádku. Od v2.754.1 běží HPDFReconcileChoiceSelection uvnitř každého volání SetFormFieldValue a když je zděděné /FT Ch, znovu staví /I z nové hodnoty. Pořadí operací je záměrné. Místní položka /I se maže nejdřív, bez dotyku jejího obsahu: pokud bylo staré pole nepřímý objekt sdílený s jiným polem, mutace na místě by pokazila výběr toho druhého pole, takže rutina upustí referenci a vytvoří čerstvé přímé pole. Pak rozřeší /Opt přes řetězec /Parent, protože volby choice mohou být zděděné, a skenuje položky. Holá string volba se srovnává přímo; pár [export display] se srovnává na svém exportním prvku a pár s méně než dvěma prvky se přeskočí. Obě strany jdou přes HPDFLoadedFormTextName, takže hex UTF-16 volba matchne hex UTF-16 hodnotu, aniž byste je museli hláskovat identicky. Při prvním zásahu se zapíše /I o jednom prvku a sken se zastaví; skalární hodnota vždycky vymění jakoukoli předchozí multi-volbu, bez ohledu na příznak MultiSelect

Jak HotPDF drží choice pole konzistentní: HPDFReconcileChoiceSelection maže místní pole /I, než do něj sahne, rozřeší /Opt přes řetězec /Parent, srovnává exportní polovinu každé volby přes HPDFLoadedFormTextName, zapisuje jednoprvkové /I při prvním zásahu a nezapisuje nic, když hodnota editovatelného comba nemá index
Holá string volba se srovnává přímo a pár export display na svém exportním prvku, zatímco hodnota mimo /Opt správně nezanechává žádný index — zaostalé /I mířící na špatnou řádku by bylo horší než žádné

Když nic neodpovídá, nezapisuje se vůbec žádné /I. To je správný výsledek pro editovatelný combo box, kde §12.7.4.4 dovoluje uživateli napsat hodnotu mimo seznam voleb; taková hodnota nemá index a zaostalý index by byl horší než žádný. To samé dostanete, když podáte display label místo exportní hodnoty do párovaného seznamu voleb, takže když combo box odmítá ukázat váš výběr, zkontrolujte, kterou polovinu páru jste dodali

// /Opt je [[US United States] [CA Canada] [MX Mexico]]:
// match na exportní hodnotu a /I se stane [1]
Pdf.SetFormFieldValue('Country', 'CA');

// Editovatelné combo s hodnotou mimo /Opt: /V se zapíše,
// /I se odstraní a žádný index se nevymyslí
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Hodnota a vzhled jsou dvě oddělené operace

SetFormFieldValue se nikdy nedotkne appearance streamu textového ani choice pole. Po volání drží /V nový text, zatímco /AP /N pořád kreslí ten starý, a to, který ze dvou prohlížeč ukáže, závisí na tom, zda slovník AcroForm nese /NeedAppearances true podle §12.7.3.3 a zda mu prohlížeč věří. Pokud potřebujete, aby soubor vykreslil novou hodnotu v každém readeru, včetně flattenérů a generátorů miniatur, které příznak ignorují, zavolejte EnsureLoadedFieldAppearanceStream s indexem pole. Postaví Form XObject ze zděděného řetězce /DA, quaddingu /Q, comb rozložení /MaxLen a hodnoty, rozřeší pojmenovaný font přes resource AcroForm /DR, takže Type0 font si drží vlastní descendant font místo degradace na Helvetica, a vrací True, když aspoň jeden widget dostal stream. Overload SetFormFieldValue podle názvu vám nevrací žádný index, takže si jeden vyzvedněte přes GetFormField, který vrací THPDFLoadedFormField, který vlastníte a musíte uvolnit. Regresní sada změny ve v2.752.1 je k tomuh rozdělení explicitní: nastaví hodnotu, zavolá EnsureLoadedFieldAppearanceStream, pak vykreslí stránku a zkontroluje, že pixely uvnitř obdélníku widgetu se změnily, zatímco pixely venku ne. Ověření, že se změnilo /V, nedokazuje nic o tom, co uživatel uvidí

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // Vykreslete novou hodnotu do /AP, aby prohlížeče, které ignorují
    // /NeedAppearances, ji pořád ukazovaly
    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;

Limity, které stojí za znát, než na tom postavíte

ReconcileLoadedButtonAppearanceStates testuje místní /FT slovníku, který jste adresovali, takže jedná na radio rodiči nebo na checkboxu, který nese vlastní /FT; widget dítě adresovaný sám o sobě, s /FT jen na rodiči, se touto cestou nevysrovnává. HPDFReconcileChoiceSelection obsluhuje jedinou skalární hodnotu a zapisuje nejvýše jeden index; multi-volbové list boxy s několika zvolenými položkami jsou mimo to, co SetFormFieldValue modeluje. Ani jedna rutina nevaliduje hodnotu, kterou podáte, proti /Opt ani proti klíčům on stavu, takže překlep produkuje checkbox Off nebo combo bez indexu místo výjimky. A GetFormFieldValue vrací uložený text /V, jak sedí ve slovníku, což u hex kódované hodnoty znamená hexadecimální pravopis, ne dekódovaný text

Jakmile jsou hodnoty v a vzhledy vykreslené, dva přirozené další kroky sedí po obou stranách téhle operace. Výměna dat polí s externími systémy v dávkách, místo jednoho volání SetFormFieldValue po druhém, je to, čemu se věnuje import a export XFDF v Delphi. A když je vyplněný formulář finální a už nemá být editovatelný, flattenování polí AcroForm a XFA v Delphi zapeče přesně ty stavy /AS a appearance streamy popsané tady do statického obsahu stránky, proto není jejich uvedení do souladu před flattenováním volitelné

Editační API načtených formulářů z tohohle článku, včetně SetFormFieldValue, EnsureLoadedFieldAppearanceStream a inkrementálního grafu přepočtu, dodává jako součást HotPDF Delphi Component pro Delphi a C++Builder