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
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
// 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
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