Műszaki cikk

Örökölt AcroForm mezőértékek és resetek Delphiben

A HotPDF Delphi Component a betöltött AcroForm mező /FT, /Ff, /V és /DV bejegyzéseit örökölhető attribútumoknak tekinti, amiket a /Parent lánc bejárásával old fel. A v2.754.3 és a v2.754.4 óta egy olyan nevesített gyerek, aminek a típusa a szülőjéből jön, egyénileg elérhető marad, a RemoveFormField békén hagyja a testvéreit, a ResetLoadedFormField pedig az örökölt alapértéket az eredeti PDF objektumtípusával másolja. Azelőtt meglepően sok hétköznapi űrlap olvasódott félre

Az az űrlap, ami mindezt felszínre hozza, nem egzotikus. Egy szerkesztőeszköz olyan group csoportcsomópontot épít, ami egyszer hordozza a /FT /Ch-t, a mező flageket és az opciólistát, alá pedig két nevesített gyereket akaszt, a-t és b-t, mindkettő egy összefésült mező-plusz-widget szótár, amiben ott a /T, a /Parent, a /Rect és a saját /V. Ez teljesen legális módja az attribútumok megosztásának, és pontosan az az eset, amit a űrlapmező-értékek beállítása betöltött PDF-ben Delphivel cikk Korlátok szakasza kezeletlenként jelzett: a button-összehangolás csak a helyi /FT-re nézett. Ez a cikk ott veszi fel a fonalat, ahol az abbahagyta: hogyan osztályozódik a mezőfa, hogyan olvasódnak az örökölt értékek, és mit írhat egyetlen mező resetje

Mely AcroForm bejegyzéseket örökölhet egy mező a szülőjétől?

Az ISO 32000-1 §12.7.3.1-e, a 220. táblázat, örökölhetőnek jelöli a /FT, /Ff, /V és /DV bejegyzéseket, a §12.7.4.3 229. táblázata pedig ugyanezt teszi egy szöveges mező /MaxLen-jére, így minden olvasó, ami csak a helyi szótárra néz, rossz típust, rossz flageket és üres értéket fog jelenteni egy teljesen érvényes gyerekre. A HotPDF mindezeket az olvasásokat egyetlen belső feloldón, a HPDFLoadedInheritedFieldObject-en önti át, ami megkeresi a kulcsot a szótárban, indirekt hivatkozás esetén feloldja azt, egyébként legfeljebb 128 szintig követi a /Parent-et, mert a hibás fájlok olyan /Parent ciklusokat építhetnek, amiknek semmi közük a /Kids-hez. A nyilvános getterek rajta ülnek: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue, valamint az opcióhelperek, a GetLoadedFormFieldOptionCount és a GetLoadedFormFieldOptions, amik felkapnak a szülőn tárolt /Opt tömböt is. A feloldó egyik szabálya könnyen elrontható: a bejárás az első, a kulcsot tartalmazó szótárnál áll meg, akkor is, ha az ottani érték üres string. Egy helyi /V () tudatos felülírás, ami elfedi a szülőt, nem pedig rés, amit felfelé töltenének ki

HotPDF örökölt AcroForm attribútumok ábrája: egy csoportcsomópont egyszer hordozza a /FT, /Ff és /Opt bejegyzéseket, miközben a nevesített gyerekek, a group.a és a group.b, csak /T, /Parent, /Rect és egy helyi /V értéket tartanak, bemutatva, hogy a HPDFLoadedInheritedFieldObject legfeljebb 128 szintig járja a /Parent-et, ahol az első kulcsot tartó szótár nyer, és egy üres helyi érték elfedi a szülőt
A HotPDF a /FT, /Ff, /V, /DV és /Opt bejegyzéseket egyetlen szülőbejáró feloldón keresztül oldja fel, így egy nevesített gyerek elérhető marad, miközben egy helyi üres érték tudatosan felülír mindent, amit a fölötte álló csoport hordoz
var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // A 'group' hordozza a /FT /Ch-t, az /Ff 131078-at és az /Opt-ot; a gyerek
    // 'group.b' csak a /T, /Parent, /Rect és a saját /V értékét hordozza
    Field := Pdf.GetFormField('group.b');
    try
      if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
      begin
        // 131078 = Combo (18. bit) + NoExport (3. bit) + Required (2. bit)
        Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
        Writeln(Pdf.IsFormFieldRequired(Field.Index));    // TRUE
        Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
        Writeln(Pdf.GetFormFieldValue(Field.Index));       // a helyi /V
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Miért rossz teszt a helyi /FT egy végmezőre?

Mert egy szülő szolgáltathatja a típust, és közben birtokolhat nevesített gyerekmezőket, így a /FT jelenléte semmit sem mond arról, hol ér véget a mezőfa. A régi bejárás akkor nyilvánított egy csomópontot végcsomópontnak, ha volt saját /FT-je, vagy nem volt /Kids-e. A fenti űrlapon a group-nak egyszerre van /FT /Ch-je és /Kids-e, így egyetlen group nevű mezőként regisztrálták két widgettel, és a teljesen minősített group.a és group.b nevek egyszerűen eltűntek. A GetFormFieldCount 1-et adott vissza, a gyerekneves keresés megbukott, a SetFormFieldValue csak a megosztott szülőt tudta írni. A pótló teszt, a HPDFLoadedFieldHasChildFields, a gyerekekre néz a szülő helyett: egy kid gyerekmező, ha van saját /T-je, van saját /Kids-e, vagy egyáltalán nem /Subtype /Widget szótár. Csak akkor nincs kvalifikált kid, ha a csomópont végcsomópont, a kidjei pedig az ő widget annotációiként kezelődnek

A két határeset, ami azt a szabályt formálta, mindkettő összefésült szótárakból jön, amiket a §12.7.3.1 megenged, amikor egy mezőnek egyetlen widgetje van. Egy nevesített összefésült szótár hordozza a /Subtype /Widget-et, és mégis gyerekmező, így a szubtípus önmagában nem küldheti a szülő névtelen widgetlistájába; a /T nyer. A fordítottja is előfordul: egyes producertek minden névtelen widgetre ráírják a szülő /FT-jét, így a /FT nem használható bizonyítékként arra sem, hogy egy widget új mezőt nyit. Az osztályozást a relációcache, a FormFieldExists és a RemoveFormField közösen használja, és mindegyik bejárás most feljegyzi a már meglátogatott szótárakat, és 128 szinten túl megáll. Egy regressziós fájl, aminek a csoportja kétszer listázza önmagát, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], továbbra is pontosan két mezőt jelent a végtelen rekurzió vagy a dupla számlálás helyett

Hogyan kerüli el a RemoveFormField a testvérmezők törlését?

A RemoveFormField mostantól csak azt a gyereket törli, amit megnevezel, mert a felderítés és a törlés végre egyetértenek abban, mi az a végmező. Ez az egyetértés többet ér, mint amennyire látszik. A név szerinti overload a relációcache-en keresztül old fel egy indexet, majd egy második, az /AcroForm /Fields-en átmenő bejárásban számolja a végmezőket. Miután a cache-t kijavították, hogy lássa a group.a-t és a group.b-t, egy javítatlan törlési bejárás továbbra is egyetlen végmezőnek tekintette volna a group-ot, és a 0. index a szülőt együtt távolította volna el minden testvérével és azok összes widgetjével. A törlési bejárás most ugyanazt a HPDFLoadedFieldHasChildFields tesztet és ugyanazt a meglátogatott halmazt használja, csak a törölt gyerek widget annotációit gyűjti össze, azokat kiszedi minden oldal /Annots-ából, és a szülőt csak akkor távolítja el, ha a /Kids tömbje üresre fogy. A regresszió mindhárom helyet ellenőrzi, ahol egy hiba előbukkanhatna: a szülő /Kids-ét, az oldal /Annots-át, és a megmaradó testvér értékét és megjelenését, teljes újraírás után és inkrementális frissítés után egyaránt

HotPDF RemoveFormField testvértúlélési ábra: a törlési bejárás újrahasznosítja a HPDFLoadedFieldHasChildFields tesztet és a felderítés meglátogatott halmazát, csak a nevesített group.a gyereket szedi ki az AcroForm /Fields-ből és az oldal /Annots-ából, és megtartja a megosztott szülőt, amíg a /Kids tömbje még tartja a túlélt group.b-t
A felderítés és a törlés végre egyetértenek abban, mi az a végmező, így egy nevesített gyerek eltávolítása a testvér értékét és megjelenését érintetlenül hagyja teljes újraírás után és inkrementális frissítés után egyaránt
// Egy nevesített gyerek törlése; a testvére és a megosztott szülő túléli
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// A típus, a flagek és az opciók továbbra is a szülőn át oldódnak fel
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

Mit ír a ResetLoadedFormField, amikor az alapérték örökölt?

A ResetLoadedFormField olyan helyi /V-t ír, ami az örökölt /DV friss másolata ugyanazzal a PDF objektumtípussal, és az egész alapértéket validálja, mielőtt a mezőhöz nyúl. Az objektumtípus azért számít, mert a skalár getterek mindent szöveggé lapítanak. Egy checkbox alapértéke olyan név, mint a /Yes, egy multi-select list box alapértéke stringek tömbje, egy szöveges alapérték pedig lehet hexadecimális UTF-16 string; bármelyik másolása a GetLoadedFormFieldDefaultValue-n át a nevet stringgé, a tömböt üres stringgé, a hex stringet pedig a szó szerinti számjegyeivé tenné. A reset ezért az örökölt típus szerint ágazik: a szöveges és choice mezők új string objektumot kapnak, ami megtartja az IsHexadecimal flaget, a tömb alapértékű choice mezők új stringek új tömbjét kapják, a nem-pushbutton gombok pedig új névobjektumot. A másolás, ahelyett hogy a szülő objektumaira mutatna, szándékos: egy /V, ami megosztaná a szülő /DV tömbjét vagy objektumszámát, megváltoztatná az alapértéket, legközelebb, amikor bárki szerkeszti az értéket. Rossz típusú alapérték, vagy bármilyen nem-stringet tartalmazó choice tömb kivételt dob, és a /V-t és az /I-t pontosan úgy hagyja, ahogy voltak. A pushbuttonok, amiknek nincs értékük (226. táblázat, 17. bit), és az aláírásmezők a régebbi, csak-stringes útra térnek vissza

HotPDF típusos reset ábra: a ResetLoadedFormField az örökölt /DV objektumtípusa szerint ágazik, friss névobjektumot ír checkboxra, új stringek új tömbjét multi-select choice-re, IsHexadecimal-t megtartó stringet hex szövegre, üres stringet vagy /Off-ot, ha nincs /DV, és típuseltérésnél kivételt dob /V vagy /I megérintése nélkül
A másolás, nem a szülő objektumaira mutatás, megakadályozza, hogy egy későbbi értékszerkesztés csendben megváltoztassa az alapértéket, a pushbuttonok és az aláírásmezők pedig a régebbi, csak-stringes útra térnek vissza

Amikor a láncon sehol sem létezik /DV, a metódus a törlési szerződését tartja meg úgy, hogy helyi üres stringet ír, checkbox vagy radio mezőnél pedig /Off-ot. A helyi /V törlése rendezettebbnek tűnne, és rossz lenne: a szülő tarthat aktuális értéket, és a gyerek felülírásának eltávolítása csendben visszahozná azt az értéket. Ezért nem az a ResetForm akció a §12.7.5.3 szerinti egyetlen mező resetje sem, amit a megjelenítő mezők halmazán futtat, amikor a felhasználó gombot nyom, ahogy azt a AcroForm mezők és akciók építése a HotPDF-fel írja. A ResetLoadedFormField egy betöltött mezőn végzett szerkesztési művelet, saját szabállyal az alapérték nélküli esetre, és a NoteLoadedFormFieldDirty-n keresztül jegyzi a mezőt, hogy az inkrementális újraszámolás lássa a változást

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // A szülő /DV [(b) (r)] értéket tart egy MultiSelect list boxon: a group.a
    // megkapja a saját /V [(b) (r)] értékét és egy friss /I [0 2]-t; a szülő érintetlen
    Pdf.ResetLoadedFormField(Field.Index);
    // A skalár getterek nem tudják ábrázolni a tömb alapértéket
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // üres
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

A /V, /I és /AS összhangban tartása

Egy reset csak akkor helyes, ha a kiválasztási index és a megjelenési állapot követi az értéket, ezért a ResetLoadedFormField ugyanazokkal a két összehangolóval fejeződik be, mint a SetFormFieldValue. A HPDFReconcileChoiceSelection mostantól tömbértéket is elfogad: törli a helyi /I-t anélkül, hogy mutálná, minden értéket az egyes /Opt bejegyzések export feléhez hasonlít, és egy új, rendezett /I-t ír, így a [(b) (r)]-re való reset a b, g, r opciók mellett /I [0 2]-t ad. A ReconcileLoadedButtonAppearanceStates mostantól az örökölt típust kéri, így egy gyerek checkbox, aminek a /FT /Btn-je a szülőn lakik, végre megkapja az /AS beállítását. Az író oldalon a SetFormFieldValue és a SetLoadedFormFieldDefaultValue névobjektumot tárol örökölt nem-pushbutton gombra akkor is, amikor a gyereknek nincs helyi bejegyzése, amiből a típust másolná. És amikor az EnsureLoadedFieldAppearanceStream újraépíti a button megjelenéseket, /AS /Off-ot ír, hacsak az érték nem egyezik az on állapottal, és minden állapotstreamnek szabályos /Type /XObject-et, /Subtype /Form-ot és /BBox-ot ad; a v2.754.4 előtt a megjelenés újragenerálása reset után újra bepipálhatta a dobozt, mielőtt a fájl mentődött volna

Korlátok, amiket érdemes ismerni, mielőtt erre építesz

A skalár getterek skalárok maradnak. A GetFormFieldValue és a GetLoadedFormFieldDefaultValue üres stringet ad vissza tömbértékre, a számokat és a booleannot 42-ként vagy true-ként stringesíti, a hex-kódolt stringet pedig hexadecimális írásmódjában jelenti. Egy /Parent ciklus kivétel nélkül fejezi be a bejárást, így egy olyan mező, aminek a típusa egy ciklusban veszik el, lfftUnknown-t és 0 flageket jelent a megbukás helyett. A SetFormFieldValue és a ResetLoadedFormField mindig azt a gyereket írja, amit címez, és soha nem léptet értéket fel a megosztott szülőre, ami a független gyerekeknek megfelelő, de azt jelenti, hogy a radio csoportokat a kiválasztást birtokló mezőn keresztül kell címezni. És minden hívás a saját mezőjét egyedileg rögzíti; semmi itt nem teszi tranzakcióssá a resetek egy tételét

Az örökölhető attribútumok feloldása, az egységesített mezőfa-osztályozás és az itt leírt típusos reset a betöltött űrlapos API része a Delphihez és C++Builderhez készült HotPDF Delphi Component-ban, az AcroForm mezők hozzáadása betöltött PDF-hez Delphiben által tárgyalt mezőlétrehozás mellett