Technisch artikel

Geërfde AcroForm-veldwaarden en resets in Delphi

Een enquêteformulier toont twee dropdown-lijsten in Acrobat, group.a en group.b. Wandelt u /AcroForm /Fields met code die elke node met een eigen /FT als een afgerond veld behandelt, dan komt de telling op 1 uit: één veld genaamd group met twee widgets. Vraagt u group.b op bij naam, dan faalt de opzoeking. Open de dictionary van het kind en er zit helemaal geen /FT in, geen /Ff, geen optielijst. Er is niets kapot. Het type, de vlaggen en de opties zitten één niveau hoger op de ouder, en de PDF-specificatie staat expliciet toe dat ze daar leven

De layout is legaal en zuinig. Een authoringtool schrijft één keer een groupnode met /FT /Ch, /Ff 131078 en een /Opt-array, en hangt er daaronder twee benoemde kinderen aan, elk een samengevoegde veld-plus-widget-dictionary met alleen /T, /Parent, /Rect en zijn eigen /V. Zo'n formulier correct lezen vergt drie dingen: voor elke overerbare sleutel /Parent volgen, bepalen waar de veldboom echt eindigt, en zorgen dat een reset een waarde van het juiste PDF-type wegschrijft. Dit stuk pakt het op waar formulierveldwaarden zetten in een geladen PDF met Delphi stopt

Welke AcroForm-entries kan een veld van zijn ouder erven?

ISO 32000-1 §12.7.3.1 (Tabel 220) markeert /FT, /Ff, /V en /DV als overerfbaar, en Tabel 229 in §12.7.4.3 doet hetzelfde voor de /MaxLen van een tekstveld. Optiearrays krijgen hier dezelfde behandeling, want producenten die een keuzelijst over kinderen delen zetten /Opt op de group. Een lezer die alleen de lokale dictionary controleert meldt daarom voor een volkomen geldig kind het verkeerde type, vlaggen 0 en een lege waarde

HotPDF Delphi Component beantwoordt elk van die lezingen op dezelfde manier. Hij zoekt de sleutel in de eigen dictionary van het veld en, als de sleutel er staat, lost hij de indirecte verwijzing op en stopt. Anders volgt hij /Parent en probeert opnieuw, tot maximaal 128 niveaus. Dat plafond is geen sier. /Parent-aanwijzers in beschadigde bestanden kunnen een lus vormen die niets met /Kids te maken heeft, en een lezer die ze onvoorwaardelijk vertrouwt hangt bij het eerste zulke bestand

Eén regel in die wandeling is makkelijk verkeerd op te vatten: de eerste dictionary die de sleutel bevat wint, ook wanneer de daar opgeslagen waarde een lege string is. Een lokale /V () op group.b is de auteur die zegt "dit kind is leeg", en de wandeling stopt daar in plaats van verder te klimmen naar een waarde die de ouder misschien heeft. Leeg als "ontbrekend" behandelen zou een gewist kind de selectie van zijn ouder laten tonen, het omgekeerde van wat de auteur schreef

Diagram van geërfde AcroForm-attributen in HotPDF: een groupnode draagt /FT, /Ff en /Opt één keer terwijl de benoemde kinderen group.a en group.b alleen /T, /Parent, /Rect en een lokale /V houden, met de opzoeking van geërfde attributen die /Parent tot 128 niveaus volgt, waar de eerste dictionary met de sleutel wint en een lege lokale waarde de ouder maskeert
HotPDF lost /FT, /Ff, /V, /DV en /Opt op via één resolver die over ouders wandelt, dus een benoemd kind blijft adresseerbaar terwijl een lokale lege waarde bewust alles overrulet wat de groep erboven draagt

De publieke getters zitten allemaal op die opzoeking: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue, en de optiehelpers GetLoadedFormFieldOptionCount en GetLoadedFormFieldOptions. Het kind van de enquetegroep lezen vraagt dus geen speciale code:

var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // 'group' draagt /FT /Ch, /Ff 131078 en /Opt; het kind
    // 'group.b' draagt alleen /T, /Parent, /Rect en zijn eigen /V
    Field := Pdf.GetFormField('group.b');
    try
      if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
      begin
        // 131078 = Combo (bit 18) + NoExport (bit 3) + Required (bit 2)
        Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
        Writeln(Pdf.IsFormFieldRequired(Field.Index));    // TRUE
        Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
        Writeln(Pdf.GetFormFieldValue(Field.Index));       // de lokale /V
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

GetFormField geeft elke keer een nieuwe wrapper THPDFLoadedFormField terug en gooit een exception bij een onbekende naam: houd de aanroep buiten het try-blok van de wrapper en geef de wrapper zelf vrij

Zoeken waar de veldboom eindigt

Een lokale /FT is de verkeerde test voor een terminaal veld, want een ouder kan het type leveren en toch benoemde kindvelden bezitten. De verleidelijke traversal stopt met afdalen zodra een node een eigen /FT heeft of geen /Kids. In het enqueteformulier heeft group allebei, /FT /Ch en /Kids, dus die traversal registreert hem als één veld met twee widget-annotaties. De namen group.a en group.b verdwijnen. Het veldaantal is 1. Een opzoeking op kindnaam faalt, en een waarde kan alleen op de gedeelde ouder terechtkomen

HotPDF kijkt in plaats daarvan naar de kids. Een kid telt als kindveld als het een eigen /T heeft, een eigen /Kids heeft, of geen /Subtype /Widget-dictionary is. Alleen als geen kid kwalificeert is de node terminaal, en zijn kids worden dan als zijn widgets behandeld. Beide randgevallen achter die regel komen uit samengevoegde dictionaries, die §12.7.3.1 toestaat wanneer een veld precies één widget heeft:

  • Een benoemde samengevoegde dictionary draagt /Subtype /Widget en is toch een kindveld. Het subtype alleen kan hem niet in de anonieme widgetlijst van de ouder drukken; de /T beslist
  • Sommige producenten herhalen de /FT van de ouder op elke anonieme widget. Dus ook /FT op een widget kan niet worden gelezen als "hier begint een nieuw veld"

Dezelfde classificatie stuurt de veldindex, FormFieldExists en RemoveFormField aan. Elk van die wandelingen houdt ook de dictionaries bij die hij al heeft bezocht en geeft het op na 128 niveaus. Stel dat de groep object 5 is en zichzelf tweemaal tussen zijn eigen kids noemt: /Kids [5 0 R 5 0 R 6 0 R 7 0 R]. Hij meldt nog steeds precies twee velden, de kinderen 6 en 7. Geen oneindige recursie, en geen spookachtig derde veld uit de zelfverwijzing

Hoe voorkomt RemoveFormField dat sibling-velden worden verwijderd?

RemoveFormField verwijdert alleen het kind dat u noemt, omdat ontdekking en verwijdering één definitie van een terminaal veld delen. De overload op naam lost een veldindex op uit de veldlijst en wandelt daarna /AcroForm /Fields een tweede keer, terminale velden tellend, om de te verwijderen dictionary te vinden. Zou de tweede wandeling de verleidelijke /FT-regel volgen terwijl de eerste de kid-gebaseerde regel gebruikte, dan zouden de indexen uit elkaar lopen: de verwijderwandeling zou group als veld 0 zien, en group.a verwijderen zou de ouder, de sibling en beide widgets meenemen

Dus past de verwijderwandeling dezelfde kindveldtest en dezelfde bezochten-set toe als de ontdekking. Hij verzamelt de widget-annotaties die bij het verwijderde kind horen, stript alleen die van de /Annots van elke pagina en verwijdert de ouder pas als zijn /Kids-array leeg uitkomt. Met group.b nog vermeld blijft de ouder staan, en blijven het type, de vlaggen en de opties die group.b erft bij hem

Diagram van sibling-overleving bij HotPDF RemoveFormField: de verwijderwandeling gebruikt dezelfde kindveldtest en bezochten-set als de ontdekking, stript alleen het benoemde kind group.a uit AcroForm /Fields en de pagina-/Annots, en houdt de gedeelde ouder aan terwijl zijn /Kids-array nog het overlevende group.b bevat
Ontdekking en verwijdering zijn het eens over wat een terminaal veld is, dus het verwijderen van één benoemd kind laat de waarde en appearance van zijn sibling intact na een volledige herschrijving of een incrementele update
// Verwijder één benoemd kind; zijn sibling en de gedeelde ouder overleven
Pdf.RemoveFormField('group.a');

// Bewakingen die u zelf zou toevoegen, geen deel van de bibliotheek
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Type, vlaggen en opties worden nog steeds via de ouder opgelost
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);

Pdf.SaveLoadedDocument('survey-trimmed.pdf');

Wanneer u een verwijdering op uw eigen formulieren nakijkt, kijk dan op drie plaatsen, want elk vangt een andere fout: de /Kids van de ouder, de pagina-/Annots, en de waarde en appearance van het overlevende sibling. Doe dat één keer na SaveLoadedDocument en één keer na SaveIncrementalUpdate, want een incrementele opslag voegt alleen de veranderde objecten toe en de /Kids van de ouder moeten daaronder zitten

Wat ResetLoadedFormField uit een geërfde /DV kopieert

ResetLoadedFormField schrijft een lokale /V die een verse kopie is van de geërfde /DV, met hetzelfde PDF-objecttype, en hij valideert de complete default voordat hij het veld aanraakt. De default van een checkbox is een naam zoals /Yes. De default van een multi-select listbox is een array van strings. Een tekstdefault kan een hexadecimale UTF-16-string zijn zoals <FEFF...>. Stuur een van die door de combinatie GetLoadedFormFieldDefaultValue en SetFormFieldValue en u krijgt een string waar een naam hoort, een lege string waar een array was, of kale hexcijfers

De reset splitst daarom op wat de geërfde /DV werkelijk is:

  • Tekst- en keuzevelden met een stringdefault krijgen een nieuw stringobject dat de hexadecimale vlag van het origineel bewaart
  • Keuzevelden met een arraydefault krijgen een nieuwe array van nieuwe strings; een array die iets anders dan strings bevat gooit een exception
  • Checkboxes en radioknoppen krijgen een nieuw naamobject
  • Een default van elk ander type gooit een exception, en /V en /I blijven exact zoals ze waren
Diagram van de getypeerde reset in HotPDF: ResetLoadedFormField splitst op het objecttype van de geërfde /DV en schrijft een vers naamobject voor een checkbox, een nieuwe array van nieuwe strings voor een multi-select keuze, een string die IsHexadecimal bewaart voor hex-tekst, een lege string of /Off als er geen /DV is, en gooit zonder /V of /I aan te raken een exception bij een typemismatch
Kopiëren in plaats van wijzen naar de objecten van de ouder voorkomt dat een latere waardebewerking stilletjes de default verandert, en pushbuttons plus signature-velden volgen het kale stringpad

Kopiëren in plaats van refereren is bewust, en het is de enige veilige optie. Een kind-/V die naar de /DV-array van de ouder wees zou één object delen tussen "huidige waarde" en "defaultwaarde"; de volgende SetFormFieldValue op dat kind zou stilletjes de default voor elke sibling herschrijven. Een paar extra bytes per resetveld is een goedkope prijs om de twee uit elkaar te houden. Pushbuttons, die geen waarde hebben (Tabel 226, bit 17), en signature-velden volgen een kaal pad dat een stringkopie van hun eigen /DV wegschrijft

Bestaat er hoog in de keten nergens een /DV, dan schrijft de methode een lokale lege string weg, of /Off bij een checkbox- of radioveld. De /V van het kind verwijderen zou netter lijken en het verkeerde antwoord geven. Houdt de ouder een actuele waarde vast, dan haalt het weghalen van de kind-override die waarde recht terug, en toont het "geresette" veld iets dat de gebruiker er nooit in heeft getypt

var
  Field: THPDFLoadedFormField;
begin
  // Ouder 'palette' is een multi-select listbox met
  // /Opt [(b) (g) (r)] en /DV [(b) (r)]; het kind houdt alleen /T en /V
  Field := Pdf.GetFormField('palette.primary');
  try
    try
      Pdf.ResetLoadedFormField(Field.Index);
      // Het kind heeft nu zijn eigen /V [(b) (r)] en /I [0 2];
      // de /DV-array van de ouder wordt niet aangeraakt
    except
      on E: Exception do
        // Misvormde default (verkeerd type, niet-string array-item):
        // /V en /I bleven zoals ze waren
        Writeln('Reset skipped: ', E.Message);
    end;
    // De scalaire getter kan een arraydefault niet weergeven
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // leeg
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

Een reset van één veld is ook iets anders dan de ResetForm-actie van §12.7.5.3, die een viewer over een set velden draait wanneer de gebruiker op een knop klikt; die actie staat beschreven in AcroForm-velden en acties bouwen met HotPDF. ResetLoadedFormField bewerkt één geladen veld, volgt zijn eigen regel voor het geval zonder default en markeert het veld als veranderd zodat incrementele herberekening hem oppikt

/V, /I en /AS met elkaar in overeenstemming houden

Een reset is alleen correct als de selectie-index en de appearance-status de waarde volgen, en ResetLoadedFormField sluit af met dezelfde twee vervolgstappen als SetFormFieldValue. Bij keuzevelden verwijdert hij de lokale /I van het kind, matcht elke waarde tegen de exporthelft van elke /Opt-entry en schrijft een nieuwe gesorteerde /I. Resetten naar [(b) (r)] tegen de opties b, g, r levert /I [0 2] op. De geërfde /I van de ouder, als die er is, blijft onaangetast

Knopen werken vanuit het geërfde type. Een kind-checkbox wiens /FT /Btn op de ouder staat krijgt toch zijn /AS bijgewerkt. Op de schrijfkant bewaren SetFormFieldValue en SetLoadedFormFieldDefaultValue een naamobject voor een geërfde niet-pushbutton-knop, zelfs wanneer het kind geen lokale entry heeft om het type uit te kopiëren. En wanneer EnsureLoadedFieldAppearanceStream knop-appearances herbouwt, schrijft hij /AS /Off tenzij de waarde met de on-status overeenkomt, en geeft hij elke statusstream /Type /XObject, /Subtype /Form en een /BBox. Zonder die /Off-controle zou het hergenereren van appearances na een reset het vakje al weer aangevinkt tonen voordat het bestand zelfs maar is opgeslagen

Scalaire getters en andere randgevallen om rekening mee te houden

De stringgetters blijven scalair, en code die hun uitvoer vergelijkt moet weten wat ze afplatten. GetFormFieldValue en GetLoadedFormFieldDefaultValue geven voor een arraywaarde een lege string terug, maken van getallen en booleans 42 of true, en melden een hex-gecodeerde string in zijn hexadecimale spelling. Wilt u weten of een listboxdefault twee selecties bevat of geen, dan kan de defaultgetter het u niet vertellen: allebei komen terug als lege string

Een /Parent-cyclus beëindigt de opzoeking zonder exception, dus een veld wiens type in een cyclus verdwijnt meldt lfftUnknown en vlaggen 0. Behandel lfftUnknown op een benoemd veld als een teken van een beschadigd bestand dat het loggen waard is. SetFormFieldValue en ResetLoadedFormField schrijven altijd het kind dat u adresseert en promoveren nooit een waarde naar de gedeelde ouder. Dat klopt voor onafhankelijke kinderen zoals de twee enquête-dropdowns. Radiogroepen zijn anders: de selectie hoort bij het veld dat de groep bezit, dus adresseer dat veld in plaats van een van zijn kids

Elke aanroep commit ook één veld op zich. Een lus van resets over twintig velden is twintig onafhankelijke bewerkingen; faalt de vijfde op een misvormde default, dan zijn de eerste vier al toegepast. Voor batchwerk leest u eerst het type en de default van elk doel, beslist welke velden u aanraakt, en begint pas daarna met schrijven

Geërfde attributenopzoeking, de kid-gebaseerde veldboomclassificatie en de getypeerde reset horen allemaal bij de loaded-form API van HotPDF Delphi Component, die ook Delphi- en C++Builder-projecten dekt die nieuwe velden aan een bestaand document moeten toevoegen, zoals getoond in AcroForm-velden toevoegen aan een geladen PDF in Delphi