Tehnički članak

Postavljanje vrijednosti polja obrasca u učitanom PDF-u

HotPDF Delphi Component puni postojeće AcroForm polje u učitanom PDF-u kroz THotPDF.SetFormFieldValue, adresirano bilo nulto baziranim indeksom polja bilo potpuno kvalificiranim imenom polja. Upisivanje novog unosa /V laki je dio; ono što taj poziv čini pouzdanim na stvarnim obrascima jest da ista metoda održava konzistentnima i tri stanja koja su nevidljiva dok ne pođu po zlu: dekodirani identitet polja, da bi se ne-ASCII ime uopće moglo naći, stanje izgleda /AS na widgetima potvrdnih okvira i radio gumba, i array indeksa odabira /I na poljima izbora. Vidljivi appearance stream zaseban je, izričit korak kroz EnsureLoadedFieldAppearanceStream

Scenarij je onaj svakodnevni: kupac vam pošalje svoj obrazac, poreznu prijavu, zahtjev za osiguranje, narudžbenicu koju je netko sastavio u Acrobatu prije godinama, a vaša Delphi aplikacija mora ga popuniti iz baze i vratiti datoteku koja se ispravno otvara svugdje. Nemate nikakvu kontrolu nad tim kako je obrazac autoriran. Imena polja mogu biti UTF-16 kodirana, izvozne vrijednosti potvrdnih okvira mogu biti 2, a ne Yes, a combo boxovi mogu koristiti parove opcija [export display]. Svaka od tih pojedinosti ima svoje pravilo u ISO 32000-1, i svako pravilo SetFormFieldValue sada rješava umjesto vas. Ovaj članak govori o tome što radi, zašto i gdje staje. Za srodni problem stvaranja polja koja još ne postoje pogledajte dodavanje AcroForm polja u učitanom PDF-u u Delphiju

Zašto SetFormFieldValue ne uspije naći polje s ne-ASCII imenom?

Prije v2.752.1 odgovor je bio kodiranje: polje je u datoteci živjelo pod heksadecimalnim UTF-16BE imenom, a cache imena spremao je heksadecimalni zapis umjesto teksta. ISO 32000-1 §12.7.3.1 definira djelomično ime polja /T kao text string, a §7.9.2.2 kaže da text string može biti UTF-16BE s vodećim FE FF byte order markom. Alati za autoriranje rutinski serijaliziraju takva imena kao hex stringove prema §7.3.4.3, pa polje nazvano Straße stiže kao <FEFF005300740072006100DF0065>. Unutar HotPDF-a THPDFStringObject.Value drži sirovi heksadecimalni tekst kad god je IsHexadecimal postavljen, što je točno ono što želite za round trip izvornog rječnika bez gubitka i točno ono što ne želite kao ključ za pretragu. HPDFLoadedFormTextName razdvaja te dvije brige. Kad se gradi cache relacija, svaka vrijednost /T prolazi kroz njega: ako je string objekt heksadecimalan, HPDFHexToBytes vraća slijed bajtova; ako bajtovi počinju s FE FF i parne su duljine, payload se dekodira kao UTF-16BE i ponovno kodira kao UTF-8; rezultat se zatim spaja s imenom roditelja točkom u potpuno kvalificirano ime koje opisuje §12.7.3.1, pa se dijete nazvano City pod roditeljem nazvanim Address registrira kao Address.City. Ključ cachea normalizira se na mala slova, što čini da i SetFormFieldValue('address.city', ...) uspije; to je pogodnost iznad standarda, jer specifikacija imena tretira kao osjetljiva na velika i mala slova. Ključno je da se mijenja samo ključ cachea. Objekt /T u rječniku polja zadržava svoje heksadecimalno kodiranje, pa spremanje dokumenta ne prepisuje identitet polja koje ste samo ispunili

Kako HotPDF razrješava ne-ASCII AcroForm imena: HPDFHexToBytes vraća UTF-16BE payload iza heksadecimalnog /T stringa, FE FF byte order mark dekodira se i ponovno kodira kao UTF-8, a kvalificirano ime spaja se s roditeljem pa Applicant.FullName i polje nazvano Straße oba dospijevaju u cache za pretragu
Mijenja se samo ključ cachea: rječnik polja zadržava svoje heksadecimalno kodiranje, pretrage se normaliziraju na mala slova kao pogodnost iznad standarda, a spremanje dokumenta nikad ne prepisuje identitet polja koje ste samo ispunili
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Kvalificirana imena dekodiraju se iz UTF-16BE /T stringova i
    // spajaju točkama, pa se ugniježđena i ne-ASCII imena razrješavaju
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

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

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

Što SetFormFieldValue zapravo piše?

Oba overloada rade istih pet koraka: lociraju rječnik polja, upisuju /V kroz HPDFSetDictFormValue, usklađuju indekse odabira kod polja izbora, označavaju rječnik prljavim, usklađuju stanja izgleda gumba i konačno zabilježe indeks polja kroz NoteLoadedFormFieldDirty. Taj posljednji korak važan je ako obrazac nosi skripte za izračun, jer prljavi skup ono je što bezargumentni overload RecalculateLoadedFormFieldsIncremental troši da ponovno pokrene samo izračune koji tranzitivno čitaju promijenjeno polje. Sam HPDFSetDictFormValue pažljiv je oko tipa objekta koji zamjenjuje. Ako je postojeći /V name objekt, što potvrdni okviri i radio polja koriste za svoju izvoznu vrijednost, nova vrijednost zapisuje se kao name, nikad kao string, jer su PDF imena po konstrukciji samo ASCII. Inače zapisuje string objekt i pregledava vrijednost koju ste predali: string koji počinje s FEFF, parne je duljine i sastoji se isključivo od hex znamenki tretira se kao UTF-16BE wire oblik iz §7.9.2.2 i sprema s postavljenim IsHexadecimal, pa se serijalizira kao <FEFF...>, a ne kao literal (FEFF...). To je mehanizam na koji se oslanja gornji redak s City; svaki drugi string sprema se kao literal string s bajtovima koje ste dali, pa za običan latinični tekst predajete običan tekst

Zašto potvrdni okvir zadrži staru kvačicu nakon promjene vrijednosti?

Zato što kod polja gumba sama vrijednost ne određuje što se crta. ISO 32000-1 §12.7.4.2.3 propisuje da widget potvrdnog okvira nosi stanje izgleda /AS koje imenuje koji je stream u /AP /N trenutačno prikazan, a preglednici crtaju iz /AS, ne iz /V. Ako promijenite /V u Yes, ali ostavite /AS na Off, datoteka je interno proturječna, a flattening će rado upeći zastarjeli neoznačeni izgled u stranicu dok podaci obrasca govore da je označeno. ReconcileLoadedButtonAppearanceStates postoji da zatvori tu prazninu: za polje čiji je /FT Btn, obilazi sam rječnik polja i svaki unos u njegovu nizu /Kids, čita ime uključenog stanja iz /AP /N i prepisuje /AS u to ime kad odgovara vrijednosti polja, ili u Off kad ne odgovara

Zašto HotPDF potvrdni okvir zadržava staru kvačicu kad se promijeni samo /V: preglednici crtaju iz stanja izgleda /AS u /AP /N, pa ReconcileLoadedButtonAppearanceStates obilazi polje i svako dijete, čita ime uključenog stanja kao prvi ključ osim Off i prepisuje /AS na podudaranje ili u Off inače
Radio grupe uspoređuju svako dijete s vrijednošću roditelja koju InheritedButtonValue vraća hodajući lancima /Parent, pa postavljanje grupe na jednu izvoznu vrijednost uključuje točno taj widget, a svaki brat isključuje

Dvije pojedinosti sa stvarnih obrazaca oblikovale su popravak v2.752.3. Prvo, normal appearance rječnik smije sadržavati samo uključeno stanje; §12.7.4.2.3 imenuje isključeni izgled Off, ali alati za autoriranje često izostavljaju njegov stream i puštaju da preglednik ne nacrta ništa. Raniji kod odustajao je kad je rječnik držao manje od dva unosa, pa su ti potvrdni okviri s jednim stanjem tiho zadržavali staru kvačicu. Provjera je sada jednostavno to da rječnik nije prazan, a ime uključenog stanja uzima se kao prvi ključ koji nije Off. Drugo, ime uključenog stanja je što god je autor odabrao. Stvarni obrasci koriste 2, Yes, On ili lokaliziranu riječ, pa se usporedba radi prema stvarnom ključu, neosjetljivo na velika i mala slova, nikad prema tvrdo kodiranom Yes. Radio gumbi dodaju još jedan nabor, opisan u §12.7.4.2.4: odabir živi u /V na roditeljskom polju, dok pojedina djeca posjeduju widgete i obično nemaju vlastiti /V. Ugniježđeni pomoćnik InheritedButtonValue zato hoda lancem /Parent prema gore, do 64 razine, dok ne nađe nepraznu vrijednost, pa se svako dijete uspoređuje s vrijednošću grupe kojoj pripada. Postavljanje roditelja na izvoznu vrijednost jednog djeteta uključuje točno to dijete, a isključuje svakog brata

// Potvrdni okvir: izvozna vrijednost mora odgovarati ključu uključenog stanja u /AP /N
// (često 'Yes', ali stvarni obrasci koriste '2', 'On' ili bilo što drugo)
Pdf.SetFormFieldValue('Consent', 'Yes');

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

// Brisanje potvrdnog okvira: svaka vrijednost koja ne odgovara nijednom uključenom stanju daje /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Polja izbora: držanje /I u koraku s /V

Za combo box ili list box /V nije jedino mjesto gdje se bilježi odabir. Tablica 231 u §12.7.4.4 definira /I kao niz nulto baziranih indeksa u /Opt koji identificira odabrane stavke, a preglednik koji nađe /I kako pokazuje na opciju 0 dok /V imenuje opciju 3 može istaknuti pogrešan redak. Od v2.754.1 HPDFReconcileChoiceSelection vrti se unutar svakog poziva SetFormFieldValue i, kad je naslijeđeni /FT jednak Ch, ponovno gradi /I iz nove vrijednosti. Redoslijed operacija je namjeran. Lokalni unos /I briše se prvi, bez diranja njegova sadržaja: ako je stari niz bio neizravni objekt dijeljen s drugim poljem, mijenjanje na mjestu pokvarilo bi odabir tog drugog polja, pa rutina odbacuje referencu i stvara svježi izravni niz. Zatim razrješava /Opt kroz lanac /Parent, jer se opcije izbora mogu naslijediti, i skenira unose. Gola string opcija uspoređuje se izravno; par [export display] uspoređuje se po svom izvoznom elementu, a par s manje od dva elementa preskače se. Obje strane prolaze kroz HPDFLoadedFormTextName, pa hex UTF-16 opcija odgovara hex UTF-16 vrijednosti bez da ih vi ispisujete identično. Na prvom podudaranju upisuje se /I s jednim elementom i skeniranje staje; skalarna vrijednost uvijek zamjenjuje svaki prethodni višestruki odabir, bez obzira na MultiSelect zastavicu

Kako HotPDF drži polje izbora konzistentnim: HPDFReconcileChoiceSelection briše lokalni /I niz prije nego ga dotakne, razrješava /Opt kroz lanac /Parent, uspoređuje izvoznu polovicu svake opcije kroz HPDFLoadedFormTextName, upisuje /I s jednim elementom na prvom podudaranju i ne upisuje ništa kad vrijednost editabilnog comba nema indeks
Gola string opcija uspoređuje se izravno, a par export display po svom izvoznom elementu, dok vrijednost izvan /Opt ispravno ne ostavlja indeks — zastarjeli /I koji pokazuje na pogrešan redak bio bi gori od nikakvog

Kad se ništa ne podudara, /I se uopće ne upisuje. To je ispravan ishod za editabilni combo box, gdje §12.7.4.4 dopušta korisniku da upiše vrijednost izvan popisa opcija; takva vrijednost nema indeks, a zastarjeli indeks bio bi gori od nikakvog. To je i ono što dobivate ako paru opcija predate prikaznu oznaku umjesto izvozne vrijednosti, pa kad combo box odbije pokazati vaš odabir, provjerite koju ste polovicu para predali

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

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

Vrijednost i izgled dvije su odvojene operacije

SetFormFieldValue nikad ne dira appearance stream tekstualnog polja ili polja izbora. Nakon poziva /V drži novi tekst, dok /AP /N još crta stari, a koje će od to dvoje preglednik pokazati ovisi o tome nosi li AcroForm rječnik /NeedAppearances true prema §12.7.3.3 i poštuje li ga preglednik. Ako trebate da datoteka renderira novu vrijednost u svakom čitaču, uključujući flattenere i generatore thumbnaila koji ignoriraju zastavicu, pozovite EnsureLoadedFieldAppearanceStream s indeksom polja. On gradi Form XObject iz naslijeđenog /DA stringa, /Q quaddinga, /MaxLen comb layouta i vrijednosti, razrješava imenovani font kroz AcroForm resurse /DR da Type0 font zadrži svoj descendant font umjesto da degradira u Helvetica, i vraća True kad je barem jedan widget dobio stream. Overload SetFormFieldValue po imenu ne vraća vam indeks, pa ga dohvatite kroz GetFormField, koji vraća THPDFLoadedFormField koji posjedujete i morate ga osloboditi. Regresijski suite za promjenu v2.752.1 izričit je oko te podjele: postavlja vrijednost, poziva EnsureLoadedFieldAppearanceStream, zatim renderira stranicu i provjerava da su se pikseli unutar pravokutnika widgeta promijenili, a pikseli izvan njega nisu. Provjera da se /V promijenio ne dokazuje ništa o tome što će korisnik vidjeti

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // Upisi novu vrijednost u /AP da je preglednici koji ignoriraju
    // /NeedAppearances ipak prikažu
    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 vrijedi znati prije nego na ovome gradite

ReconcileLoadedButtonAppearanceStates testira lokalni /FT rječnika koji ste adresirali, pa djeluje na roditelja radio grupe ili na potvrdni okvir koji nosi vlastiti /FT; dječji widget adresiran samostalno, s /FT samo na svom roditelju, ne usklađuje se tim putem. HPDFReconcileChoiceSelection rukuje jednom skalarnom vrijednošću i upisuje najviše jedan indeks; list boxovi s višestrukim odabirom i nekoliko odabranih unosa izvan su onoga što SetFormFieldValue modelira. Nijedna rutina ne validira vrijednost koju predajete prema /Opt ili prema ključevima uključenog stanja, pa tipfeler daje Off potvrdni okvir ili combo bez indeksa umjesto iznimke. A GetFormFieldValue vraća spremljeni tekst /V onakav kakav stoji u rječniku, što za heksadecimalno kodiranu vrijednost znači heksadecimalni zapis, a ne dekodirani tekst

Kad su vrijednosti unutra i izgledi iscrtani, dva prirodna sljedeća koraka stoje s obje strane ove operacije. Razmjena podataka polja s vanjskim sustavima u skupnom obliku, umjesto jednog poziva SetFormFieldValue po putu, ono je što pokriva uvoz i izvoz XFDF-a u Delphiju. A kad je ispunjeni obrazac konačan i više ne treba biti uređiv, flattening AcroForm i XFA polja u Delphiju upeče upravo ovdje opisana stanja /AS i appearance streamove u statični sadržaj stranice, zato njihovo usklađivanje prije flatteninga nije opcionalno

API za uređivanje učitanih obrazaca iz ovog članka, uključujući SetFormFieldValue, EnsureLoadedFieldAppearanceStream i graf inkrementalnog ponovnog izračuna, isporučuje se kao dio HotPDF Delphi Component za Delphi i C++Builder