Articol tehnic

Valori de câmp AcroForm moștenite și resetări în Delphi

HotPDF Delphi Component tratează /FT, /Ff, /V și /DV de pe un câmp AcroForm încărcat ca atribute moștenibile, rezolvate parcurgând lanțul /Parent. Din v2.754.3 și v2.754.4, un copil numit a cărui type vine de la părintele lui rămâne adresabil individual, RemoveFormField își lasă frații în pace, iar ResetLoadedFormField copiază implicitul moștenit cu tipul lui original de obiect PDF. Înainte de asta, un număr surprinzător de formulare obișnuite erau citite greșit

Formularul care scoate la iveală totul nu e excentric. O unealtă de autorat construiește un nod de grup group care cară /FT /Ch, flag-urile de câmp și lista de opțiuni o singură dată, și atârnă sub el doi copii numiți a și b, fiecare un dicționar îmbinat de câmp-plus-widget fără altceva decât /T, /Parent, /Rect și propriul lui /V. E un mod perfect legal de a partaja atribute, și e exact cazul pe care secțiunea Limite din setarea valorilor câmpurilor de formular într-un PDF încărcat în Delphi l-a semnalat ca netratat: reconcilierea butoanelor se uita doar la /FT-ul local. Articolul de față preia de acolo unde celălalt s-a oprit, acoperind cum e clasificat arborele de câmpuri, cum se citesc valorile moștenite și ce are voie să scrie un reset pe un singur câmp

Ce intrări AcroForm poate moșteni un câmp de la părintele lui?

ISO 32000-1 §12.7.3.1, Tabelul 220, marchează /FT, /Ff, /V și /DV ca moștenibile, iar Tabelul 229 din §12.7.4.3 face la fel pentru /MaxLen-ul unui câmp de text, deci orice cititor care se uită doar la dicționarul local va raporta tipul greșit, flag-urile greșite și o valoare goală pentru un copil perfect valid. HotPDF varsă toate aceste citiri printr-un singur resolver intern, HPDFLoadedInheritedFieldObject, care verifică dicționarul după cheie, rezolvă o referință indirectă dacă găsește una și, în caz contrar, urmează /Parent pentru cel mult 128 de niveluri, pentru că fișierele malformate pot construi cicluri /Parent care n-au nimic de-a face cu /Kids. Getter-ii publici stau deasupra lui: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue și helper-ii de opțiuni GetLoadedFormFieldOptionCount și GetLoadedFormFieldOptions, care prind și un tablou /Opt stocat pe părinte. O regulă din resolver e ușor de greșit: parcurgerea se oprește la primul dicționar care conține cheia, chiar dacă valoarea de acolo e un șir gol. Un /V () local e o suprascriere deliberată care maschează părintele, nu un gol de umplut de mai sus în arbore

Diagrama atributelor AcroForm moștenite în HotPDF: un nod de grup cară /FT, /Ff și /Opt o singură dată, în timp ce copiii numiți group.a și group.b țin doar /T, /Parent, /Rect și un /V local, arătând pe HPDFLoadedInheritedFieldObject parcurgând /Parent până la 128 de niveluri, unde câștigă primul dicționar care ține cheia, iar o valoare locală goală îl maschează pe părinte
HotPDF rezolvă /FT, /Ff, /V, /DV și /Opt printr-un singur resolver care urcă pe părinte, deci un copil numit rămâne adresabil, în timp ce o valoare locală goală suprascrie deliberat tot ce cară grupul de deasupra lui
var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // 'group' cară /FT /Ch, /Ff 131078 și /Opt; copilul
    // 'group.b' cară doar /T, /Parent, /Rect și propriul lui /V
    Field := Pdf.GetFormField('group.b');
    try
      if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
      begin
        // 131078 = Combo (bitul 18) + NoExport (bitul 3) + Required (bitul 2)
        Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
        Writeln(Pdf.IsFormFieldRequired(Field.Index));    // TRUE
        Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
        Writeln(Pdf.GetFormFieldValue(Field.Index));       // /V-ul local
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

De ce un /FT local e testul greșit pentru un câmp terminal?

Pentru că un părinte poate furniza tipul și să dețină în continuare câmpuri copil numite, deci prezența lui /FT nu spune nimic despre unde se termină arborele de câmpuri. Vechea traversare declara un nod terminal de câte ori avea propriul /FT sau niciun /Kids. În formularul de mai sus, group are și /FT /Ch și /Kids, deci era înregistrat ca un singur câmp numit group cu două widget-uri, iar numele complet calificate group.a și group.b pur și simplu dispăreau. GetFormFieldCount întorcea 1, o căutare după numele copilului eșua, iar SetFormFieldValue putea scrie doar părintele partajat. Testul de înlocuire, HPDFLoadedFieldHasChildFields, se uită la copii în loc de părinte: un copil e un câmp copil dacă are propriul /T, are propriul /Kids sau nu e deloc un dicționar /Subtype /Widget. Doar când niciun copil nu se califică, nodul e terminal, cu copiii lui tratați ca annotațiile lui de widget

Cele două cazuri limită care au dat forma regulii vin amândouă din dicționarele îmbinate, pe care §12.7.3.1 le permite când un câmp are un singur widget. Un dicționar îmbinat numit cară /Subtype /Widget și este în continuare un câmp copil, deci subtipul singur nu-l poate trimite în lista anonimă de widget-uri a părintelui; /T-ul câștigă. Inversul se întâmplă și el: unii producători repetă /FT-ul părintelui pe fiecare widget anonim, deci /FT nu poate fi folosit ca dovadă că un widget pornește un câmp nou. Clasificarea e partajată de cache-ul de relații, FormFieldExists și RemoveFormField, iar fiecare dintre parcursurile acelea înregistrează acum dicționarele pe care le-a vizitat deja și se oprește peste 128 de niveluri. Un fișier de regresie al cărui grup se listează pe sine de două ori, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], raportează în continuare exact două câmpuri, fără să recureze la infinit și fără să numere același nod de două ori

Cum evită RemoveFormField ștergerea câmpurilor frate?

RemoveFormField șterge acum doar copilul pe care îl numiți, pentru că descoperirea și ștergerea s-au pus în sfârșit de acord asupra a ceea ce e un câmp terminal. Acordul acela contează mai mult decât pare. Suprascrierea după nume rezolvă un index prin cache-ul de relații și apoi numără câmpurile terminale într-o a doua parcurgere peste /AcroForm /Fields. Odată ce cache-ul a fost reparat să vadă group.a și group.b, o parcurgere de ștergere nereparată ar fi tratat în continuare group ca un singur câmp terminal, iar indexul 0 ar fi eliminat părintele împreună cu fiecare frate și toate widget-urile lor. Parcurgerea de ștergere folosește acum același test HPDFLoadedFieldHasChildFields și același set de vizitate, colecționează annotațiile de widget doar ale copilului eliminat, le scoate din /Annots-ul fiecărei pagini și elimină părintele doar când tabloul lui /Kids rămâne gol. Regresia verifică toate cele trei locuri în care o greșeală ar ieși la iveală: /Kids-ul părintelui, /Annots-ul paginii și valoarea și aparența fratelui supraviețuitor, atât după o rescriere completă, cât și după o actualizare incrementală

Diagrama supraviețuirii fratelui în RemoveFormField din HotPDF: parcurgerea de ștergere refolosește HPDFLoadedFieldHasChildFields și setul de vizitate din descoperire, scoate doar copilul numit group.a din AcroForm /Fields și din /Annots paginii și păstrează părintele partajat cât timp tabloul lui /Kids mai ține fratele group.b supraviețuitor
Descoperirea și ștergerea s-au pus în sfârșit de acord asupra a ceea ce e un câmp terminal, deci eliminarea unui copil numit lasă valoarea și aparența fratelui lui intacte după o rescriere completă sau o actualizare incrementală
// Eliminarea unui copil numit; fratele lui și părintele partajat supraviețuiesc
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Tipul, flag-urile și opțiunile sunt în continuare rezolvate prin părinte
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

Ce scrie ResetLoadedFormField când implicitul e moștenit?

ResetLoadedFormField scrie un /V local care e o copie proaspătă a /DV-ului moștenit, cu același tip de obiect PDF, și validează tot implicitul înainte să atingă câmpul. Tipul de obiect contează pentru că getter-ii scalari aplatizează totul în text. Un implicit de checkbox e un name precum /Yes, un implicit de listă multi-select e un tablou de șiruri, iar un implicit de text poate fi un șir UTF-16 hexazecimal; copierea oricăruia dintre ele prin GetLoadedFormFieldDefaultValue ar transforma name-ul în șir, tabloul în șir gol și șirul hex în cifrele lui literale. Reset-ul se ramifică deci pe tipul moștenit: câmpurile text și choice primesc un obiect de tip șir nou care păstrează flag-ul IsHexadecimal, câmpurile choice cu implicit de tip tablou primesc un tablou nou de șiruri noi, iar butoanele non-pushbutton primesc un obiect de tip name nou. Copierea, în loc să arate spre obiectele părintelui, e deliberată: un /V care ar partaja tabloul /DV al părintelui sau numărul lui de obiect ar schimba implicitul la următoarea editare a valorii. Un implicit de tip greșit, sau un tablou choice care conține altceva decât șiruri, ridică o excepție și lasă /V și /I exact cum erau. Pushbutton-urile, care n-au valoare (Tabelul 226, bitul 17), și câmpurile de semnătură revin la vechea cale doar-șir

Diagrama reset-ului tipizat în HotPDF: ResetLoadedFormField se ramifică pe tipul de obiect al /DV-ului moștenit, scriind un obiect de tip name proaspăt pentru un checkbox, un tablou nou de șiruri noi pentru un choice multi-select, un șir care păstrează IsHexadecimal pentru text hex, un șir gol sau /Off când nu există /DV, și ridicând excepție fără să atingă /V sau /I la o nepotrivire de tip
Copierea, în loc să arate spre obiectele părintelui, împiedică o editare ulterioară a valorii să schimbe în tăcere implicitul, iar pushbutton-urile și câmpurile de semnătură revin la vechea cale doar-șir

Când nu există niciun /DV nicăieri pe lanț, metoda își păstrează contractul de golire scriind un șir gol local, sau /Off pentru un câmp checkbox sau radio. Ștergerea /V-ului local ar arăta mai îngrijit și ar fi greșită: părintele poate ține o valoare curentă, iar scoaterea suprascrierii copilului ar aduce acea valoare înapoi în tăcere. Din același motiv, un reset pe un singur câmp nu e acțiunea ResetForm din §12.7.5.3, pe care un vizualizator o rulează peste un set de câmpuri când utilizatorul apasă un buton, așa cum descrie construirea de câmpuri și acțiuni AcroForm cu HotPDF. ResetLoadedFormField e o operație de editare pe un singur câmp încărcat, cu propria lui regulă pentru cazul fără implicit, și înregistrează câmpul prin NoteLoadedFormFieldDirty ca recalcularea incrementală să vadă schimbarea

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // Părintele ține /DV [(b) (r)] pe o listă MultiSelect: group.a primește
    // propriul /V [(b) (r)] și un /I [0 2] proaspăt; părintele e neatins
    Pdf.ResetLoadedFormField(Field.Index);
    // Getter-ii scalari nu pot reprezenta implicitul de tip tablou
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // gol
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

Menținerea acordului între /V, /I și /AS

Un reset e corect doar dacă indexul de selecție și starea de aparență urmează valoarea, deci ResetLoadedFormField se termină cu aceiași doi reconciliatori ca SetFormFieldValue. HPDFReconcileChoiceSelection acceptă acum o valoare de tip tablou: șterge /I-ul local fără să-l mute, potrivește fiecare valoare cu jumătatea de export a fiecărei intrări /Opt și scrie un /I sortat nou, deci un reset la [(b) (r)] împotriva opțiunilor b, g, r dă /I [0 2]. ReconcileLoadedButtonAppearanceStates cere acum tipul moștenit, deci un checkbox copil a cărui /FT /Btn trăiește pe părinte primește în sfârșit /AS-ul setat. Pe partea de scriere, SetFormFieldValue și SetLoadedFormFieldDefaultValue stochează un obiect de tip name pentru un buton non-pushbutton moștenit chiar și când copilul n-are nicio intrare locală de la care să copieze tipul. Iar când EnsureLoadedFieldAppearanceStream reconstruiește aparențele butoanelor, scrie /AS /Off dacă valoarea nu se potrivește cu starea de pornit și dă fiecărui stream de stare un /Type /XObject, un /Subtype /Form și un /BBox cu adevărat; înainte de v2.754.4, regenerarea aparenței după un reset putea bifa din nou căsuța înainte ca fișierul să fie salvat

Limite care merită știute înainte să construiți pe asta

Getter-ii scalari rămân scalari. GetFormFieldValue și GetLoadedFormFieldDefaultValue întorc un șir gol pentru o valoare de tip tablou, stringifică numerele și booleenii ca 42 sau true și raportează un șir codat hex în scrierea lui hexazecimală. Un ciclu /Parent termină parcurgerea fără excepție, deci un câmp al cărui tip e pierdut într-un ciclu raportează lfftUnknown și flag-uri 0 în loc să eșueze. SetFormFieldValue și ResetLoadedFormField scriu întotdeauna copilul pe care îl adresați și nu promovează niciodată o valoare spre părintele partajat, ceea ce e corect pentru copiii independenți, dar înseamnă că grupurile de radio ar trebui adresate prin câmpul care deține selecția. Și fiecare apel comite un singur câmp de unul singur; nimic din aici nu face un lot de reset-uri tranzacțional

Rezolvarea atributelor moștenite, clasificarea unificată a arborelui de câmpuri și reset-ul tipizat descrise aici fac parte din API-ul de formulare încărcate din HotPDF Delphi Component pentru Delphi și C++Builder, alături de crearea de câmpuri acoperită în adăugarea de câmpuri AcroForm într-un PDF încărcat în Delphi