HotPDF Delphi Component completează un câmp AcroForm existent pe un PDF încărcat prin THotPDF.SetFormFieldValue, adresat fie după indexul de câmp bazat pe zero, fie după numele complet calificat al câmpului. Scrierea noii intrări /V este partea ușoară; ce face apelul de încredere pe formularele din lumea reală este că aceeași metodă ține consistente trei bucăți de stare care sunt invizibile până când se strică: identitatea decodată a câmpului, ca un nume non-ASCII să poată fi găsit deloc, starea de aparență /AS pe widget-urile de checkbox și radio, și tabloul de indici de selecție /I pe câmpurile de tip choice. Stream-ul de aparență vizibil este un pas separat, explicit, prin EnsureLoadedFieldAppearanceStream
Scenariul este cel banal: un client vă trimite propriul formular, o declarație fiscală, o cerere de asigurare, o comandă de achiziție pe care cineva a construit-o în Acrobat acum ani de zile, iar aplicația dumneavoastră Delphi trebuie să îl populeze dintr-o bază de date și să întoarcă un fișier care se deschide corect peste tot. Nu aveți niciun control asupra felului în care a fost autorat formularul. Numele de câmp pot fi codate UTF-16, valorile de export ale checkbox-urilor pot fi 2 în loc de Yes, iar casetele combo pot folosi perechi de opțiuni [export display]. Fiecare dintre detaliile acelea are o regulă în ISO 32000-1, iar fiecare regulă este ceva ce SetFormFieldValue tratează acum pentru dumneavoastră. Articolul de față este despre ce face, de ce și unde se oprește. Pentru problema înrudită a creării de câmpuri care nu există încă, vedeți adăugarea de câmpuri AcroForm într-un PDF încărcat în Delphi
De ce nu găsește SetFormFieldValue un câmp cu nume non-ASCII?
Înainte de v2.752.1, răspunsul era codarea: câmpul trăia în fișier sub un nume hexazecimal UTF-16BE, iar cache-ul de nume stoca scrierea hexazecimală în loc de text. ISO 32000-1 §12.7.3.1 definește numele parțial de câmp /T ca șir de text, iar §7.9.2.2 spune că un șir de text poate fi UTF-16BE cu un marcaj de ordine a octeților FE FF în frunte. Uneltele de autorat serializează de regulă astfel de nume ca șiruri hexazecimale conform §7.3.4.3, așa că un câmp numit Straße sosește ca <FEFF005300740072006100DF0065>. În interiorul HotPDF, THPDFStringObject.Value ține textul hexazecimal brut ori de câte ori IsHexadecimal este setat, exact ce vrei pentru un dus-întors fără pierderi al dicționarului original și exact ce nu vrei ca cheie de căutare. HPDFLoadedFormTextName separă cele două preocupări. Când cache-ul de relații este construit, fiecare valoare /T trece prin el: dacă obiectul de tip șir este hexazecimal, HPDFHexToBytes restaurează secvența de octeți; dacă octeții încep cu FE FF și au lungime pară, payload-ul este decodat ca UTF-16BE și recodat ca UTF-8; rezultatul este apoi alăturat numelui părinte cu un punct, pentru a forma numele complet calificat pe care îl descrie §12.7.3.1, așa că un copil numit City sub un părinte numit Address este înregistrat ca Address.City. Cheia de cache este normalizată la litere mici, ceea ce face ca și SetFormFieldValue('address.city', ...) să reușească; este o facilitate peste standard, pentru că specificația tratează numele ca sensibile la majuscule. Esențial este că doar cheia de cache se schimbă. Obiectul /T din dicționarul câmpului își păstrează codarea hexazecimală, așa că salvarea documentului nu rescrie identitatea unui câmp pe care doar l-ați completat
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// Numele calificate sunt decodate din șiruri /T UTF-16BE și
// alăturate cu puncte, așa că numele imbricate și non-ASCII se rezolvă
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');
// Valorile care nu sunt Latin-1 călătoresc ca hex UTF-16BE cu prefix FEFF
// și sunt scrise ca șir hexazecimal PDF
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
Ce scrie de fapt SetFormFieldValue?
Ambele suprascrieri rulează aceiași cinci pași: localizează dicționarul câmpului, scrie /V prin HPDFSetDictFormValue, reconciliază indicii de selecție pentru choice, marchează dicționarul ca murdar, reconciliază stările de aparență ale butoanelor și, în final, înregistrează indexul câmpului prin NoteLoadedFormFieldDirty. Ultimul pas contează dacă formularul cară scripturi de calcul, pentru că setul de câmpuri murdare este ce consumă suprascrierea fără parametri RecalculateLoadedFormFieldsIncremental ca să ruleze din nou doar calculele care citesc tranzitiv un câmp schimbat. HPDFSetDictFormValue însuși este atent la tipul de obiect pe care îl înlocuiește. Dacă /V existent este un obiect de tip name, care este ce folosesc câmpurile de checkbox și radio pentru valoarea lor de export, valoarea nouă este scrisă ca name, niciodată ca șir, pentru că numele PDF sunt prin construcție doar ASCII. Altfel scrie un obiect de tip șir și inspectează valoarea pe care ați transmis-o: un șir care începe cu FEFF, are lungime pară și constă doar din cifre hexazecimale este tratat ca forma de transport UTF-16BE din §7.9.2.2 și stocat cu IsHexadecimal setat, așa că se serializează ca <FEFF...> și nu ca un literal (FEFF...). Acesta este mecanismul pe care se sprijină linia cu City de mai sus; orice alt șir este stocat ca șir literal cu octeții pe care i-ați dat, așa că pentru text latin simplu transmiteți text simplu
De ce păstrează un checkbox bifa veche după ce valoarea se schimbă?
Pentru că la un câmp de tip buton doar valoarea nu decide ce se desenează. ISO 32000-1 §12.7.4.2.3 specifică faptul că un widget de checkbox cară o stare de aparență /AS care numește ce stream din /AP /N este afișat în acel moment, iar vizualizatoarele pictează din /AS, nu din /V. Dacă schimbați /V în Yes, dar lăsați /AS la Off, fișierul este intern contradictoriu, iar aplatizarea va coace bucuros aparența veche nebifată în pagină, în timp ce datele de formular spun bifat. ReconcileLoadedButtonAppearanceStates există ca să închidă golul acela: pentru un câmp al cărui /FT este Btn, vizitează dicționarul câmpului însuși și fiecare intrare din tabloul lui /Kids, citește numele stării de pornit din /AP /N și rescrie /AS la numele acela când se potrivește cu valoarea câmpului, sau la Off când nu se potrivește
Două detalii din formularele reale au dat forma reparației din v2.752.3. Întâi, un dicționar de aparență normal are voie să conțină doar starea de pornit; §12.7.4.2.3 numește aparența de oprit Off, dar uneltele de autorat omit frecvent stream-ul ei și lasă vizualizatorul să nu deseneze nimic. Codul anterior renunța când dicționarul avea mai puțin de două intrări, așa că acele checkbox-uri cu o singură stare își păstrau în tăcere bifa veche. Verificarea este acum pur și simplu că dicționarul este nenul, iar numele stării de pornit este luat drept prima cheie care nu este Off. Al doilea, numele stării de pornit este orice a ales autorul. Formularele reale folosesc 2, Yes, On sau un cuvânt localizat, așa că comparația se face cu cheia reală, fără să țină cont de majuscule, niciodată cu un Yes scris în cod. Butoanele radio adaugă o încrețitură în plus, descrisă în §12.7.4.2.4: selecția trăiește în /V pe câmpul părinte, în timp ce copiii individuali dețin widget-urile și de regulă nu au /V al lor. Helper-ul imbricat InheritedButtonValue urcă deci pe lanțul /Parent, până la 64 de niveluri, până găsește o valoare nenulă, așa că fiecare copil este comparat cu valoarea grupului din care face parte. Setarea părintelui la valoarea de export a unui copil aprinde exact copilul acela și stinge fiecare frate
// Checkbox: valoarea de export trebuie să se potrivească cu cheia stării de pornit din /AP /N
// (deseori 'Yes', dar formularele reale folosesc '2', 'On' sau orice altceva)
Pdf.SetFormFieldValue('Consent', 'Yes');
// Grup radio: /V se scrie pe părinte; fiecare widget copil primește
// /AS pus pe propriul nume de export sau pe Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Golirea unui checkbox: orice valoare care nu se potrivește cu nicio stare de pornit dă /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
Câmpuri choice: cum ținem /I în pas cu /V
Pentru o casetă combo sau o listă, /V nu este singurul loc unde se consemnează o selecție. Tabelul 231 din §12.7.4.4 definește /I ca un tablou de indici bazați pe zero în /Opt, care identifică elementele selectate, iar un vizualizator care găsește /I arătând spre opțiunea 0 în timp ce /V numește opțiunea 3 poate evidenția rândul greșit. Din v2.754.1, HPDFReconcileChoiceSelection rulează în interiorul fiecărui apel SetFormFieldValue și, când /FT-ul moștenit este Ch, reconstruiește /I din valoarea nouă. Ordinea operațiilor este deliberată. Intrarea locală /I este ștearsă mai întâi, fără a-i atinge conținutul: dacă tabloul vechi era un obiect indirect partajat cu alt câmp, mutarea lui în loc ar corupe selecția celuilalt câmp, așa că rutina aruncă referința și creează în schimb un tablou direct nou. Apoi rezolvă /Opt prin lanțul /Parent, pentru că opțiunile de tip choice pot fi moștenite, și scanează intrările. O opțiune care este doar un șir este comparată direct; o pereche [export display] este comparată pe elementul ei de export, iar o pereche cu mai puțin de două elemente este sărită. Ambele părți trec prin HPDFLoadedFormTextName, așa că o opțiune hex UTF-16 se potrivește cu o valoare hex UTF-16 fără să le scrieți identic. La prima potrivire se scrie un /I cu un singur element și scanarea se oprește; o valoare scalară înlocuiește mereu orice multi-selecție anterioară, indiferent de flag-ul MultiSelect
Când nimic nu se potrivește, nu se scrie deloc /I. Acesta este rezultatul corect pentru o casetă combo editabilă, unde §12.7.4.4 permite utilizatorului să tasteze o valoare din afara listei de opțiuni; o astfel de valoare nu are index, iar un index perimat ar fi mai rău decât niciunul. Este și ce obțineți dacă transmiteți o etichetă de afișare în loc de o valoare de export la o listă de opțiuni pereche, așa că atunci când o casetă combo refuză să vă arate selecția, verificați care jumătate a perechii ați furnizat-o
// /Opt is [[US United States] [CA Canada] [MX Mexico]]:
// potrivire pe valoarea de export, iar /I devine [1]
Pdf.SetFormFieldValue('Country', 'CA');
// Combo editabil cu o valoare din afara lui /Opt: /V este scris,
// /I este scos și nu se fabrică niciun index
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
Valoarea și aparența sunt două operații separate
SetFormFieldValue nu atinge niciodată stream-ul de aparență al unui câmp de text sau choice. După apel, /V ține textul nou, în timp ce /AP /N îl pictează încă pe cel vechi, iar care dintre ele este arătat depinde de dacă dicționarul AcroForm cară /NeedAppearances true conform §12.7.3.3 și de dacă vizualizatorul o respectă. Dacă aveți nevoie ca fișierul să randeze valoarea nouă în orice cititor, inclusiv în aplatizatoare și generatoare de miniaturi care ignoră flag-ul, apelați EnsureLoadedFieldAppearanceStream cu indexul câmpului. Construiește un Form XObject din șirul /DA moștenit, alinierea /Q, aspectul comb din /MaxLen și valoarea, rezolvă fontul numit prin resursele /DR ale AcroForm, ca un font Type0 să își păstreze propriul font descendent în loc să degenereze în Helvetica, și întoarce True când cel puțin un widget a primit un stream. Suprascrierea după nume a lui SetFormFieldValue nu vă dă niciun index înapoi, așa că obțineți unul prin GetFormField, care întoarce un THPDFLoadedFormField pe care îl dețineți și pe care trebuie să îl eliberați. Suita de regresie pentru schimbarea din v2.752.1 este explicită în privința acestei separări: setează o valoare, apelează EnsureLoadedFieldAppearanceStream, apoi randează pagina și verifică faptul că pixelii din interiorul dreptunghiului widget-ului s-au schimbat, în timp ce pixelii din afara lui nu. Verificarea faptului că /V s-a schimbat nu dovedește nimic despre ce va vedea un utilizator
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// Pictează valoarea nouă în /AP ca vizualizatoarele care ignoră
// /NeedAppearances să o arate totuși
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;
Limite care merită știute înainte de a construi pe asta
ReconcileLoadedButtonAppearanceStates testează /FT-ul local al dicționarului pe care l-ați adresat, așa că acționează pe părintele de radio sau pe un checkbox care își cară propriul /FT; un widget copil adresat separat, cu /FT doar pe părintele lui, nu este reconciliat prin calea aceea. HPDFReconcileChoiceSelection tratează o singură valoare scalară și scrie cel mult un index; casetele de listă cu selecție multiplă și mai multe intrări alese sunt în afara a ce modelează SetFormFieldValue. Niciuna dintre rutine nu validează valoarea pe care o transmiteți față de /Opt sau față de cheile stărilor de pornit, așa că o greșeală de tastare produce un checkbox Off sau un combo fără index, nu o excepție. Iar GetFormFieldValue întoarce textul /V stocat așa cum stă în dicționar, ceea ce pentru o valoare codată hexazecimal înseamnă scrierea hexazecimală, nu textul decodat
Odată ce valorile sunt introduse și aparențele pictate, următorii doi pași firești stau de o parte și de alta a acestei operații. Schimbul de date de câmp cu sisteme externe în bloc, în loc de un apel SetFormFieldValue pe rând, este ce acoperă importul și exportul XFDF în Delphi. Iar când formularul completat este final și nu mai trebuie să fie editabil, aplatizarea câmpurilor AcroForm și XFA în Delphi coace exact stările /AS și stream-urile de aparență descrise aici în conținut static de pagină, și de aceea obținerea consistenței lor înainte de aplatizare nu este opțională
API-ul de editare a formularelor încărcate din acest articol, inclusiv SetFormFieldValue, EnsureLoadedFieldAppearanceStream și graful de recalculare incrementală, este livrat ca parte din HotPDF Delphi Component pentru Delphi și C++Builder