Tehnički članak

Postavljanje vrednosti polja formulara u PDF-u uz Delphi

HotPDF Delphi Component popunjava postojeće AcroForm polje na učitanom PDF-u kroz THotPDF.SetFormFieldValue, adresirano bilo indeksom polja sa bazom nula bilo punim kvalifikovanim imenom polja. Upisivanje novog /V unosa je laki deo; ono što poziv čini pouzdanim na formularima iz stvarnog sveta je to što ista metoda održava konzistentnim i tri dela stanja koja su nevidljiva dok ne krenu naopako: dekodirani identitet polja, da bi ne-ASCII ime uopšte moglo da se nađe, /AS stanje izgleda na checkbox i radio widget-ima, i /I niz indeksa izbora na choice poljima. Vidljivi appearance stream je odvojen, eksplicitan korak kroz EnsureLoadedFieldAppearanceStream

Scenario je onaj obični: korisnik vam pošalje sopstveni formular, poresku prijavu, osiguravajući zahtev, narudžbenicu koju je neko napravio u Acrobat-u pre godina, i vaša Delphi aplikacija treba da ga popuni iz baze i vrati fajl koji se ispravno otvara svuda. Nemate kontrolu nad tim kako je formular napravljen. Imena polja mogu biti UTF-16 kodirana, izvozne vrednosti checkbox-a mogu biti 2 umesto Yes, a combo box-ovi mogu koristiti parove opcija [export display]. Svaki od tih detalja ima pravilo u ISO 32000-1, i svako pravilo je nešto što SetFormFieldValue sada rešava umesto vas. Ovaj članak je o tome šta radi, zašto, i gde se zaustavlja. Za srodni problem pravljenja polja koja još ne postoje, pogledajte dodavanje AcroForm polja u učitan PDF u Delphi-ju

Zašto SetFormFieldValue ne nađe polje sa ne-ASCII imenom?

Pre v2.752.1 odgovor je bio enkodiranje: polje je živelo u fajlu pod heksadecimalnim UTF-16BE imenom, a cache imena čuvao je heksadecimalni zapis umesto teksta. ISO 32000-1 §12.7.3.1 definiše delimično ime polja /T kao tekstualni string, a §7.9.2.2 kaže da tekstualni string može biti UTF-16BE sa vodećim FE FF byte order mark-om. Alati za pravljenje formulara rutinski serijalizuju takva imena kao hex stringove po §7.3.4.3, pa polje koje se zove Straße stiže kao <FEFF005300740072006100DF0065>. Unutar HotPDF-a THPDFStringObject.Value drži sirovi heksadecimalni tekst kad god je IsHexadecimal postavljen, što je upravo ono što želite za round trip originalnog rečnika bez gubitka i upravo ono što ne želite kao ključ za pretragu. HPDFLoadedFormTextName razdvaja ta dva interesa. Kada se cache relacija gradi, svaka vrednost /T prolazi kroz nju: ako je string objekat heksadecimalan, HPDFHexToBytes vraća niz bajtova; ako bajtovi počinju sa FE FF i imaju parnu dužinu, payload se dekodira kao UTF-16BE i ponovo enkodira kao UTF-8; rezultat se zatim spaja sa imenom roditelja tačkom da bi se formiralo puno kvalifikovano ime koje §12.7.3.1 opisuje, pa se dete koje se zove City pod roditeljem Address registruje kao Address.City. Ključ cache-a se normalizuje u mala slova, što čini da i SetFormFieldValue('address.city', ...) uspe; to je pogodnost iznad standarda, jer specifikacija tretira imena kao osetljiva na velika i mala slova. Ključno je da se menja samo ključ cache-a. Objekat /T u rečniku polja zadržava svoje heksadecimalno enkodiranje, pa čuvanje dokumenta ne prepisuje identitet polja koje ste samo popunili

Kako HotPDF razrešava ne-ASCII AcroForm imena: HPDFHexToBytes vraća UTF-16BE payload iza heksadecimalnog /T string-a, FE FF byte order mark se dekodira i ponovo enkodira kao UTF-8, a kvalifikovano ime spaja svog roditelja pa Applicant.FullName i polje koje se zove Straße oba sležu u cache za pretragu
Menja se samo ključ cache-a: rečnik polja zadržava svoje heksadecimalno enkodiranje, pretrage se normalizuju u mala slova kao pogodnost iznad standarda, a čuvanje dokumenta nikada ne prepisuje identitet polja koje ste samo popunili
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Kvalifikovana imena se dekodiraju iz UTF-16BE /T stringova i
    // spajaju tačkama, pa se ugnježdena i ne-ASCII imena razrešavaju
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // Vrednosti koje nisu Latin-1 putuju kao FEFF-prefiksirani UTF-16BE hex
    // i upisuju se kao PDF heksadecimalni string
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

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

Šta SetFormFieldValue zapravo upisuje?

Obe overload varijante rade istih pet koraka: lociraj rečnik polja, upiši /V kroz HPDFSetDictFormValue, usaglasi indekse izbora za choice polja, označi rečnik kao prljav, usaglasi stanja izgleda dugmadi, i na kraju zabeleži indeks polja kroz NoteLoadedFormFieldDirty. Taj poslednji korak je važan ako formular nosi računske skripte, jer je skup prljavih upravo ono što overload RecalculateLoadedFormFieldsIncremental bez parametara troši da ponovo pokrene samo izračunavanja koja tranzitivno čitaju izmenjeno polje. Sam HPDFSetDictFormValue pazi na tip objekta koji zamenjuje. Ako je postojeći /V name objekat, što checkbox i radio polja koriste za svoju izvoznu vrednost, nova vrednost se upisuje kao name, nikada kao string, jer su PDF name-ovi po konstrukciji samo ASCII. U suprotnom upisuje string objekat i pregleda vrednost koju ste predali: string koji počinje sa FEFF, ima parnu dužinu i sastoji se isključivo od heksadecimalnih cifara tretira se kao UTF-16BE žičani oblik iz §7.9.2.2 i čuva sa postavljenim IsHexadecimal, pa se serijalizuje kao <FEFF...> a ne kao literal (FEFF...). To je mehanizam na koji se oslanja linija City gore; svaki drugi string se čuva kao literal string sa bajtovima koje ste dali, pa za običan latinični tekst predajete običan tekst

Zašto checkbox zadrži stari kvačicu posle promene vrednosti?

Zato što za dugme polje sama vrednost ne odlučuje šta se crta. ISO 32000-1 §12.7.4.2.3 propisuje da checkbox widget nosi /AS stanje izgleda koje imenuje koji stream u /AP /N je trenutno prikazan, a pregledači crtaju iz /AS, ne iz /V. Ako promenite /V u Yes ali ostavite /AS na Off, fajl je interno protivrečan, i flattening će rado upeći zastareli neoznačeni izgled u stranicu dok podaci formulara kažu označeno. ReconcileLoadedButtonAppearanceStates postoji da zatvori tu prazninu: za polje čiji je /FT Btn, obilazi sam rečnik polja i svaki unos u njegovom /Kids nizu, čita ime uključenog stanja iz /AP /N, i prepisuje /AS na to ime kada se poklapa sa vrednošću polja, ili na Off kada se ne poklapa

Zašto HotPDF checkbox zadrži staru kvačicu kada se promeni samo /V: pregledači crtaju iz /AS stanja izgleda u /AP /N, pa ReconcileLoadedButtonAppearanceStates obilazi polje i svako dete, čita ime uključenog stanja kao prvi ključ osim Off, i prepisuje /AS pri poklapanju ili na Off u suprotnom
Radio grupe porede svako dete sa vrednošću roditelja koju InheritedButtonValue povrati hodom kroz lanac /Parent, pa postavljanje grupe na jednu izvoznu vrednost uključuje tačno taj widget a svaki brat isključuje

Dva detalja sa stvarnih formulara oblikovala su popravku u v2.752.3. Prvo, normal appearance rečnik sme da sadrži samo uključeno stanje; §12.7.4.2.3 imenuje isključeni izgled kao Off, ali alati za pravljenje formulara često izostavljaju njegov stream i puštaju pregledač da ne iscrta ništa. Raniji kod odustajao je kada je rečnik imao manje od dva unosa, pa su ti checkbox-ovi sa jednim stanjem tiho zadržavali staru kvačicu. Provera je sada prosto da rečnik nije prazan, a ime uključenog stanja uzima se kao prvi ključ koji nije Off. Drugo, ime uključenog stanja je šta god je autor odabrao. Pravi formulari koriste 2, Yes, On ili lokalizovanu reč, pa se poređenje vrši prema stvarnom ključu, bez razlikovanja velikih i malih slova, nikada prema hard-kodiranom Yes. Radio dugmad dodaju još jednu nabor, opisanu u §12.7.4.2.4: izbor živi u /V na roditeljskom polju, dok pojedinačna deca poseduju widget-e i obično nemaju sopstveni /V. Ugnježdeni pomoćnik InheritedButtonValue zato ide nagore kroz lanac /Parent, do 64 nivoa, dok ne nađe nepraznu vrednost, pa se svako dete poredi sa vrednošću grupe kojoj pripada. Postavljanje roditelja na izvoznu vrednost jednog deteta uključuje tačno to dete a svaki brat isključuje

// Checkbox: izvozna vrednost mora da se poklopi sa ključem uključenog stanja u /AP /N
// (često 'Yes', ali pravi formulari koriste '2', 'On' ili bilo šta drugo)
Pdf.SetFormFieldValue('Consent', 'Yes');

// Radio grupa: /V se upisuje na roditelja; svaki widget deteta dobija
// /AS postavljen na svoje izvozno ime ili na Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Brisanje checkbox-a: svaka vrednost koja se ne poklapa ni sa jednim uključenim stanjem daje /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Choice polja: držanje /I u koraku sa /V

Za combo box ili list box, /V nije jedino mesto gde se beleži izbor. Tabela 231 u §12.7.4.4 definiše /I kao niz indeksa sa bazom nula u /Opt koji identifikuje izabrane stavke, a pregledač koji nađe da /I pokazuje na opciju 0 dok /V imenuje opciju 3 može da istakne pogrešan red. Od v2.754.1, HPDFReconcileChoiceSelection se izvršava unutar svakog poziva SetFormFieldValue i, kada je nasleđeni /FT jednak Ch, ponovo gradi /I iz nove vrednosti. Redosled operacija je nameran. Lokalni unos /I briše se prvi, bez dodirivanja njegovog sadržaja: ako je stari niz bio indirektni objekat koji deli neko drugo polje, mutiranje na mestu pokvarilo bi izbor tog drugog polja, pa rutina odbacuje referencu i pravi svež direktni niz umesto toga. Zatim razrešava /Opt kroz lanac /Parent, jer opcije izbora mogu biti nasleđene, i skenira unose. Gola string opcija poredi se direktno; par [export display] poredi se po svom izvoznom elementu, a par sa manje od dva elementa se preskače. Obe strane prolaze kroz HPDFLoadedFormTextName, pa se hex UTF-16 opcija poklapa sa hex UTF-16 vrednošću bez toga da ih vi prepišete identično. Pri prvom poklapanju upisuje se jednoelementni /I i skeniranje staje; skalarna vrednost uvek zamenjuje svaki prethodni višestruki izbor, bez obzira na MultiSelect flag

Kako HotPDF održava choice polje konzistentnim: HPDFReconcileChoiceSelection briše lokalni /I niz pre nego što ga dotakne, razrešava /Opt kroz lanac /Parent, poredi izvoznu polovinu svake opcije kroz HPDFLoadedFormTextName, upisuje jednoelementni /I pri prvom poklapanju i ne upisuje ništa kada vrednost editabilnog combo box-a nema indeks
Gola string opcija poredi se direktno a par export display po svom izvoznom elementu, dok vrednost van /Opt ispravno ostavlja bez indeksa — zastareo /I koji pokazuje na pogrešan red bio bi gori od nikakvog

Kada se ništa ne poklapa, /I se uopšte ne upisuje. To je ispravan ishod za editabilni combo box, gde §12.7.4.4 dopušta korisniku da otkuca vrednost van liste opcija; takva vrednost nema indeks, a zastareo indeks bio bi gori od nikakvog. To je i ono što dobijete ako paru opcija predate oznaku za prikaz umesto izvozne vrednosti, pa kada combo box odbije da prikaže vaš izbor, proverite koju ste polovinu para dali

// /Opt je [[US United States] [CA Canada] [MX Mexico]]:
// poklapanje po izvoznoj vrednosti, i /I postaje [1]
Pdf.SetFormFieldValue('Country', 'CA');

// Editabilni combo sa vrednošću van /Opt: /V se upisuje,
// /I se uklanja, i nikakav indeks se ne izmišlja
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Vrednost i izgled su dve odvojene operacije

SetFormFieldValue nikada ne dira appearance stream tekstualnog ili choice polja. Posle poziva /V drži novi tekst dok /AP /N i dalje crta stari, a koje će od toga pregledač prikazati zavisi od toga da li AcroForm rečnik nosi /NeedAppearances true po §12.7.3.3 i da li pregledač to poštuje. Ako želite da fajl iscrta novu vrednost u svakom čitaču, uključujući flattenere i generatore thumbnail-a koji ignorišu flag, pozovite EnsureLoadedFieldAppearanceStream sa indeksom polja. On gradi Form XObject iz nasleđenog /DA string-a, /Q quadding-a, /MaxLen comb rasporeda i vrednosti, razrešava imenovani font kroz AcroForm /DR resurse da bi Type0 font zadržao svoj descendant font umesto da degradira u Helvetica, i vraća True kada je barem jedan widget dobio stream. Overload SetFormFieldValue-a po imenu ne vraća vam indeks, pa ga nabavite kroz GetFormField, koji vraća THPDFLoadedFormField koji vi posedujete i morate da ga oslobodite. Regresioni suite za promenu u v2.752.1 eksplicitan je oko te podele: postavlja vrednost, poziva EnsureLoadedFieldAppearanceStream, zatim renderuje stranicu i proverava da su se pikseli unutar pravougaonika widget-a promenili a pikseli van njega nisu. Provera da se /V promenio ne dokazuje ništa o tome šta će korisnik videti

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // Iscrtaj novu vrednost u /AP da bi je pregledači koji ignorišu
    // /NeedAppearances ipak prikazali
    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;

Ograničenja koja vredi znati pre nego što gradite na ovome

ReconcileLoadedButtonAppearanceStates testira lokalni /FT rečnika koji ste adresirali, pa deluje na radio roditelja ili na checkbox koji nosi sopstveni /FT; widget deteta adresiran samostalno, sa /FT samo na roditelju, ne usaglašava se kroz tu putanju. HPDFReconcileChoiceSelection obrađuje jednu skalarnu vrednost i upisuje najviše jedan indeks; list box-ovi sa višestrukim izborom i nekoliko izabranih unosa su van onoga što SetFormFieldValue modeluje. Ni jedna rutina ne validira vrednost koju predate prema /Opt ili prema ključevima uključenog stanja, pa greška u kucanju daje checkbox na Off ili combo bez indeksa umesto izuzetka. A GetFormFieldValue vraća sačuvani /V tekst onakav kakav stoji u rečniku, što za heksadecimalno kodiranu vrednost znači heksadecimalni zapis, a ne dekodirani tekst

Kada su vrednosti unutra i izgledi iscrtani, dva prirodna sledeća koraka sede sa obe strane ove operacije. Razmena podataka polja sa eksternim sistemima u masi, umesto jedan SetFormFieldValue poziv po poziv, je ono što pokriva uvoz i izvoz XFDF-a u Delphi-ju. A kada je popunjeni formular konačan i više ne treba da bude uređivan, flattening AcroForm i XFA polja u Delphi-ju upeče upravo ona /AS stanja i appearance stream-ove opisane ovde u statičan sadržaj stranice, i zato njihovo usaglašavanje pre flattening-a nije opciono

API za uređivanje učitanih formulara iz ovog članka, uključujući SetFormFieldValue, EnsureLoadedFieldAppearanceStream i graf inkrementalnog preračunavanja, isporučuje se kao deo HotPDF Delphi Component za Delphi i C++Builder