A HotPDF Delphi Component egy meglévő AcroForm mezőt tölt ki egy betöltött PDF-en a THotPDF.SetFormFieldValue-vel, nullától indexelt mezőindexszel vagy teljesen minősített mezőnévvel címezve. Az új /V bejegyzés megírása a könnyű rész; ami a hívást megbízhatóvá teszi a való világ űrlapjain, az az, hogy ugyanez a metódus három olyan állapotot is konzisztensen tart, amik láthatatlanok, amíg el nem romlanak: a mező dekódolt identitását, hogy egy nem ASCII nevű mezőt egyáltalán meg lehessen találni, a checkbox- és radio-widgetek /AS megjelenési állapotát, és a choice mezők /I kiválasztásiindex-tömbjét. A látható megjelenési stream külön, explicit lépés az EnsureLoadedFieldAppearanceStream-en keresztül
A forgatókönyv a hétköznapi: egy ügyfél elküldi a saját űrlapját, egy adóbevallást, egy biztosítási kárigényt, egy beszerzési megrendelést, amit valaki évekkel ezelőtt épített az Acrobatban, a Delphi alkalmazásodnak pedig adatbázisból kell feltöltenie, és visszaadnia egy fájlt, ami mindenhol helyesen nyílik meg. Afölött nincs kontrollod, hogyan szerkesztették az űrlapot. A mezőnevek UTF-16-tal kódoltak lehetnek, a checkbox exportértékei 2-esek lehetnek Yes helyett, a combo boxok pedig [export display] opciópárokat használhatnak. Ezen részletek mindegyikének van szabálya az ISO 32000-1-ben, és minden szabályt mostantól a SetFormFieldValue kezel helyetted. Ez a cikk arról szól, mit tesz, miért, és hol áll meg. A testvérproblémához, a még nem létező mezők létrehozásához lásd a AcroForm mezők hozzáadását betöltött PDF-hez Delphiben
Miért nem találja meg a SetFormFieldValue egy nem ASCII nevű mezőt?
A v2.752.1 előtt a válasz a kódolás volt: a mező egy hexadecimális UTF-16BE név alatt élt a fájlban, a névcache pedig a hex írásmódot tárolta a szöveg helyett. Az ISO 32000-1 §12.7.3.1 a részleges mezőnevet, a /T-t szöveges stringként definiálja, a §7.9.2.2 pedig kimondja, hogy egy szöveges string lehet UTF-16BE vezető FE FF byte order markkal. A szerkesztőeszközök rutinszerűen hex stringként sorosítják az ilyen neveket a §7.3.4.3 szerint, így egy Straße nevű mező <FEFF005300740072006100DF0065> formában érkezik. A HotPDF-ben a THPDFStringObject.Value a nyers hexadecimális szöveget tartja, valahányszor az IsHexadecimal be van állítva, ami pontosan az, amit az eredeti szótár veszteségmentes oda-vissza útjához akarsz, és pontosan az, amit nem akarsz keresőkulcsként. A HPDFLoadedFormTextName szétválasztja a két szempontot. Amikor a kapcsolat-cache felépül, minden /T érték átmegy rajta: ha a stringobjektum hexadecimális, a HPDFHexToBytes visszaállítja a bájtsorozatot; ha a bájtok FE FF-el kezdődnek és páros hosszúságúak, a hasznos teher UTF-16BE-ként dekódolódik és UTF-8-ként kódolódik újra; az eredményt pedig egy ponttal a szülőnevéhez fűzi, hogy megalkossa a §12.7.3.1 által leírt teljesen minősített nevet, így egy Address nevű szülő alatti City nevű gyerek Address.City-ként regisztrálódik. A cache-kulcs kisbetűsre normalizálódik, amitől a SetFormFieldValue('address.city', ...) is sikerül; ez egy kényelmi szolgáltatás a szabványon túl, mivel a specifikáció a neveket kis- és nagybetű-érzékenyként kezeli. A lényeg, hogy csak a cache-kulcs változik. A mezőszótárban lévő /T objektum megtartja a hexadecimális kódolását, így a dokumentum mentése nem írja át egy olyan mező identitását, amit csak kitöltöttél
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// A minősített nevek UTF-16BE /T stringekből dekódolódnak és
// pontokkal kapcsolódnak össze, így a beágyazott és nem ASCII nevek is feloldódnak
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');
// A nem Latin-1 értékek FEFF-prefixes UTF-16BE hexként utaznak,
// és PDF hexadecimális stringként íródnak ki
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
Mit ír valójában a SetFormFieldValue?
Mindkét overload ugyanazt az öt lépést futtatja: megkeresi a mezőszótárat, megírja a /V-t a HPDFSetDictFormValue-n keresztül, összehangolja a choice kiválasztási indexeit, piszkosnak jelöli a szótárat, összehangolja a button megjelenési állapotokat, és végül rögzíti a mezőindexet a NoteLoadedFormFieldDirty-n keresztül. Ez az utolsó lépés akkor számít, ha az űrlap számítási szkripteket hordoz, mert a piszkos halmaz az, amit a paraméter nélküli RecalculateLoadedFormFieldsIncremental overload elfogyaszt, hogy csak azokat a számításokat futtassa újra, amik tranzitívan olvassák egy megváltozott mezőt. Maga a HPDFSetDictFormValue gondos azzal kapcsolatban, milyen objektumtípust cserél le. Ha a meglévő /V egy névobjektum, amit a checkbox- és radio-mezők használnak az exportértékükhöz, az új érték névként íródik, soha nem stringként, mert a PDF-nevek konstrukció szerint csak ASCII-ból állnak. Máskülönben stringobjektumot ír, és megvizsgálja az átadott értéket: az a string, ami FEFF-el kezdődik, páros hosszúságú, és kizárólag hexadecimális számjegyekből áll, a §7.9.2.2 szerinti UTF-16BE átviteli formaként kezelődik, és IsHexadecimal beállítással tárolódik, így <FEFF...>-ként sorosul literál (FEFF...) helyett. Ez az a mechanizmus, amire a fenti City sor támaszkodik; minden más string literál stringként tárolódik azokkal a bájtokkal, amiket megadtál, így egyszerű latin szöveghez egyszerű szöveget adj át
Miért tartja meg a checkbox a régi pipáját az érték megváltozása után?
Mert egy button mezőnél az érték önmagában nem dönti el, mi rajzolódik. Az ISO 32000-1 §12.7.4.2.3 előírja, hogy egy checkbox widget /AS megjelenési állapotot hordoz, ami megnevezi, melyik stream látszik épp a /AP /N-ben, a megjelenítők pedig a /AS-ből festenek, nem a /V-ből. Ha a /V-t Yes-re változtatod, de az /AS-t Off-on hagyod, a fájl belsőleg ellentmondásos lesz, a flattenelés pedig vígan beleégeti az elavult, kipipálatlan megjelenést az oldalba, miközben az űrlapadat kipipáltat mond. A ReconcileLoadedButtonAppearanceStates azért létezik, hogy bezárja ezt a rést: egy olyan mezőnél, aminek a /FT-je Btn, meglátogatja magát a mezőszótárat és a /Kids tömbjének minden bejegyzését, kiolvassa az on-állapot nevét az /AP /N-ből, és az /AS-t arra a névre írja át, amikor az illeszkedik a mező értékére, vagy Off-ra, amikor nem
Két részlet a valódi űrlapokról formálta a v2.752.3 javítást. Először, egy normál megjelenési szótár tartalmazhat csak az on állapotot; a §12.7.4.2.3 az off megjelenést Off-nak nevezi, a szerkesztőeszközök viszont gyakran kihagyják a streamjét, és hagyják, hogy a megjelenítő semmit ne rajzoljon. A korábbi kód feladta, amikor a szótár kevesebb mint két bejegyzést tartalmazott, így azok az egyállapotú checkboxok csendben megtartották a régi pipájukat. A vizsgálat mostantól egyszerűen az, hogy a szótár nem üres, az on-állapot nevét pedig az első Off-tól különböző kulcsként veszi. Másodszor, az on-állapot neve az, amit a szerző választott. A valódi űrlapok 2-t, Yes-t, On-t vagy egy lokalizált szót használnak, így az összehasonlítás a tényleges kulcs ellen történik, kis-nagybetűt nem megkülönböztetve, soha nem egy beégetett Yes ellen. A radio gombok még egy redőt hozzáadnak, amit a §12.7.4.2.4 ír le: a kiválasztás a szülő mezőn a /V-ben él, míg az egyes gyerekek birtokolják a widgeteket, és jellemzően nincs saját /V-jük. A beágyazott InheritedButtonValue helper ezért felmegy a /Parent láncon, legfeljebb 64 szinten, amíg egy nem üres értéket nem talál, így minden gyerek ahhoz a csoporthoz hasonlítódik, amelyikhez tartozik. A szülő beállítása az egyik gyerek exportértékére pontosan azt a gyereket kapcsolja be, és minden testvérét ki
// Checkbox: az exportértéknek egyeznie kell az /AP /N on-állapot kulcsával
// (gyakran 'Yes', de a valódi űrlapok '2'-t, 'On'-t vagy bármi mást használnak)
Pdf.SetFormFieldValue('Consent', 'Yes');
// Radio csoport: a /V a szülőre íródik; minden gyerek widget /AS-e
// a saját exportnevére vagy Off-ra áll
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Checkbox törlése: minden on-állapotra nem illeszkedő érték /AS Off-ot ad
Pdf.SetFormFieldValue('Newsletter', 'Off');
Choice mezők: az /I lépést tart a /V-vel
Egy combo boxnál vagy list boxnál a /V nem az egyetlen hely, ahol kiválasztás rögzül. A §12.7.4.4 231. táblázata az /I-t az /Opt-ba mutató nullától indexelt indexek tömbjeként definiálja, ami azonosítja a kiválasztott elemeket, és egy megjelenítő, ami azt találja, hogy az /I a 0. opcióra mutat, míg a /V a 3-at nevezi meg, a rossz sort emelheti ki. A v2.754.1 óta a HPDFReconcileChoiceSelection lefut minden SetFormFieldValue hívásban, és amikor az örökölt /FT Ch, újraépíti az /I-t az új értékből. A műveletek sorrendje szándékos. A helyi /I bejegyzés törlődik először, a tartalmához hozzá sem nyúlva: ha a régi tömb egy másik mezővel megosztott indirekt objektum volt, a helyben módosítás elrontaná a másik mező kiválasztását, ezért a rutin eldobja a hivatkozást, és inkább egy friss direkt tömböt hoz létre. Ezután a /Parent láncon keresztül feloldja az /Opt-ot, mivel a choice opciók örökölhetők, és átvizsgálja a bejegyzéseket. Egy csupasz string opció közvetlenül hasonlítódik; egy [export display] pár az export elemén hasonlítódik, a kettőnél kevesebb elemet tartalmazó pár pedig kimarad. Mindkét oldal átmegy a HPDFLoadedFormTextName-en, így egy hex UTF-16 opció illeszkedik egy hex UTF-16 értékhez anélkül, hogy betűre azonosan írnád le őket. Az első találatnál egy egyelemű /I íródik, és a pásztázás leáll; egy skalár érték mindig lecseréli a korábbi többes kiválasztást, a MultiSelect flagtól függetlenül
Amikor semmi nem illeszkedik, egyáltalán nem íródik /I. Ez a helyes kimenet egy szerkeszthető combo boxnál, ahol a §12.7.4.4 megengedi, hogy a felhasználó az opciólistán kívüli értéket gépeljen be; egy ilyen értéknek nincs indexe, és egy elavult index rosszabb lenne a semminél. Ezt kapod akkor is, ha egy párosított opciólistának display címkét adsz át exportérték helyett, így amikor egy combo box nem hajlandó megmutatni a kiválasztásodat, nézd meg, a pár melyik felét adtad meg
// Az /Opt [[US United States] [CA Canada] [MX Mexico]]:
// az exportértékre illeszkedünk, és az /I [1] lesz
Pdf.SetFormFieldValue('Country', 'CA');
// Szerkeszthető combo /Opt-on kívüli értékkel: a /V kiíródik,
// az /I törlődik, és nem gyártunk indexet
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
Az érték és a megjelenés két külön művelet
A SetFormFieldValue soha nem nyúl egy szöveges vagy choice mező megjelenési streamjéhez. A hívás után a /V az új szöveget tartja, míg a /AP /N még a régit festi, és hogy a kettő közül melyiket mutatja egy megjelenítő, azon múlik, hogy az AcroForm szótár hordoz-e /NeedAppearances true értéket a §12.7.3.3 szerint, és hogy a megjelenítő tiszteli-e. Ha azt akarod, hogy a fájl minden olvasóban az új értéket renderelje, beleértve a flattenereket és a flaget figyelmen kívül hagyó thumbnail-generátorokat, hívd meg az EnsureLoadedFieldAppearanceStream-et a mezőindexszel. Az örökölt /DA stringből, a /Q quaddingból, a /MaxLen comb-elrendezésből és az értékből Form XObjectet épít, a megnevezett betűkészletet az AcroForm /DR erőforrásain keresztül oldja fel, hogy egy Type0 betűkészlet megtartsa a saját leszármazott betűkészletét ahelyett, hogy Helvetica-ra degradálódna, és True-t ad vissza, amikor legalább egy widget kapott streamet. A SetFormFieldValue név szerinti overloadja nem ad vissza indexet, így szerezz egyet a GetFormField-en keresztül, ami egy általad birtokolt és felszabadítandó THPDFLoadedFormField-et ad vissza. A v2.752.1 változás regressziós sorozata explicit ezzel a szétválasztással kapcsolatban: beállít egy értéket, meghívja az EnsureLoadedFieldAppearanceStream-et, majd rendereli az oldalt, és ellenőrzi, hogy a widget téglalapján belüli pixelek megváltoztak, a kívül lévők viszont nem. Annak igazolása, hogy a /V megváltozott, semmit nem bizonyít arról, mit fog látni a felhasználó
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// Fesd bele az új értéket az /AP-ba, hogy a /NeedAppearances-t
// figyelmen kívül hagyó megjelenítők is mutassák
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;
Korlátok, amiket érdemes tudni, mielőtt erre építesz
A ReconcileLoadedButtonAppearanceStates a címezett szótár helyi /FT mezőjét vizsgálja, így a radio szülőn vagy egy olyan checkboxon hat, ami a saját /FT-jét hordozza; egy önmagában címzett gyerek widget, aminek csak a szülőjén van /FT, nem hangolódik össze azon az útvonalon. A HPDFReconcileChoiceSelection egyetlen skalár értéket kezel, és legfeljebb egy indexet ír; a több kiválasztott bejegyzésű, multi-selection list boxok kívül esnek azon, amit a SetFormFieldValue modellez. Egyik rutin sem validálja az átadott értéket az /Opt vagy az on-állapot kulcsai ellen, így egy elírás Off checkboxot vagy index nélküli combo boxot ad kivétel helyett. A GetFormFieldValue pedig a tárolt /V szöveget adja vissza úgy, ahogy a szótárban ül, ami egy hexadecimálisan kódolt értéknél a hexadecimális írásmódot jelenti, nem a dekódolt szöveget
Amint az értékek bekerültek és a megjelenések megfestődtek, a két természetes következő lépés ennek a műveletnek a két oldalán ül. A mezőadatok cseréje külső rendszerekkel nagy tételben, nem egyesével SetFormFieldValue hívásokkal, az, amit az XFDF import és export Delphiben tárgyal. És amikor a kitöltött űrlap végleges, és már nem szabad szerkeszthetőnek lennie, az AcroForm és XFA mezők flattenelése Delphiben pontosan az itt leírt /AS állapotokat és megjelenési streameket égeti bele statikus oldaltartalomba, ezért nem opcionális, hogy a flattenelés előtt konzisztenssé tegyük őket
Az ebben a cikkben leírt betöltött űrlapos szerkesztési API, beleértve a SetFormFieldValue-t, az EnsureLoadedFieldAppearanceStream-et és az inkrementális újraszámítási gráfot, a HotPDF Delphi Component részeként jelenik meg Delphihez és C++Builderhez