Teknisk artikel

Nedarvede AcroForm-feltværdier og resets i Delphi

HotPDF Delphi Component behandler /FT, /Ff, /V og /DV på et loadet AcroForm-felt som nedarvede attributter, resolver ved at gå /Parent-kæden igennem. Siden v2.754.3 og v2.754.4 forbliver et navngivet child, hvis type kommer fra sin parent, individuelt adresserbart, RemoveFormField lader dets søskende i fred, og ResetLoadedFormField kopierer den nedarvede default med sin originale PDF-objekttype. Før det blev en overraskende mængde almindelige formularer mislæst

Formularen, der udstiller alt dette, er ikke eksotisk. Et authoring-værktøj bygger en gruppe-node group, der bærer /FT /Ch, feltflagene og optionslisten én gang, og hænger to navngivne children a og b under den, hver én en merged field-plus-widget-dictionary med intet andet end /T, /Parent, /Rect og sin egen /V. Det er en fuldt lovlig måde at dele attributter på, og det er præcis det tilfælde, Limits-sektionen i at sætte formularfeltværdier i en loadet PDF med Delphi flaggede som ubehandlet: button-reconciliation kiggede kun på den lokale /FT. Denne artikel tager der, hvor den slap, og dækker, hvordan felttræet klassificeres, hvordan nedarvede værdier læses, og hvad et single-field reset må skrive

Hvilke AcroForm-entries kan et felt arve fra sin parent?

ISO 32000-1 §12.7.3.1, Tabel 220, markerer /FT, /Ff, /V og /DV som nedarvelige, og Tabel 229 i §12.7.4.3 gør det samme for et tekstfelts /MaxLen, så enhver reader, der kun kigger i den lokale dictionary, rapporterer den forkerte type, de forkerte flag og en tom værdi for et fuldt gyldigt child. HotPDF leder alle disse læsninger gennem én intern resolver, HPDFLoadedInheritedFieldObject, som tjekker dictionaryen for nøglen, resolver en indirekte reference, hvis den finder én, og ellers følger /Parent i højst 128 niveauer, fordi misdannede filer kan bygge /Parent-cykler, der ikke har noget med /Kids at gøre. De offentlige getters ligger ovenpå den: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue og option-helpersne GetLoadedFormFieldOptionCount og GetLoadedFormFieldOptions, som også samler et /Opt-array, der ligger på parenten. Én regel i resolveren er let at tage fejl af: gennemløbet stopper ved den første dictionary, der indeholder nøglen, selvom værdien dér er en tom streng. En lokal /V () er en bevidst override, der maskerer parenten, ikke et hul, der skal fyldes oppefra i træet

HotPDF-diagram over nedarvede AcroForm-attributter: en gruppe-node bærer /FT, /Ff og /Opt én gang, mens de navngivne children group.a og group.b kun holder /T, /Parent, /Rect og en lokal /V, hvilket viser HPDFLoadedInheritedFieldObject gå /Parent op til 128 niveauer, hvor den første dictionary med en nøgle vinder, og en tom lokal værdi maskerer parenten
HotPDF resolver /FT, /Ff, /V, /DV og /Opt gennem én parent-gående resolver, så et navngivet child forbliver adresserbart, mens en lokal tom værdi bevidst override'er alt, hvad gruppen over den bærer
var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // 'group' bærer /FT /Ch, /Ff 131078 og /Opt; childet
    // 'group.b' bærer kun /T, /Parent, /Rect og sin egen /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));       // den lokale /V
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Hvorfor er en lokal /FT den forkerte test for et terminal-felt?

Fordi en parent kan levere typen og stadig eje navngivne child-felter, så tilstedeværelsen af /FT siger intet om, hvor felttræet slutter. Den gamle traversal erklærede en node terminal, når den havde sin egen /FT eller ingen /Kids. I formularen ovenfor har group både /FT /Ch og /Kids, så den blev registreret som ét felt kaldet group med to widgets, og de fuldt kvalificerede navne group.a og group.b forsvandt simpelthen. GetFormFieldCount returnerede 1, et opslag efter child-navn fejlede, og SetFormFieldValue kunne kun skrive til den delte parent. Erstatningstesten, HPDFLoadedFieldHasChildFields, kigger på kids i stedet for parenten: et kid er et child-felt, hvis det har sin egen /T, har sine egne /Kids eller slet ikke er en /Subtype /Widget-dictionary. Først når intet kid kvalificerer, er noden terminal, med dens kids behandlet som dens widget-annotationer

De to edge cases, der formede den regel, kommer begge fra merged dictionaries, som §12.7.3.1 tillader, når et felt har en enkelt widget. En navngivet merged dictionary bærer /Subtype /Widget og er stadig et child-felt, så subtypen alene kan ikke sende den ind i parentens anonyme widget-liste; /T vinder. Det omvendte sker også: nogle producenter gentager parentens /FT på hver anonym widget, så /FT heller ikke kan bruges som bevis for, at en widget starter et nyt felt. Klassificeringen deles af relationscachen, FormFieldExists og RemoveFormField, og hvert af de gennemløb registrerer nu de dictionaries, det allerede har besøgt, og stopper forbi 128 niveauer. En regressionsfil, hvis gruppe lister sig selv to gange, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], rapporterer stadig præcis to felter i stedet for at rekurrere i al evighed eller tælle samme node to gange

Hvordan undgår RemoveFormField at slette søskende-felter?

RemoveFormField sletter nu kun det child, du navngiver, fordi discovery og sletning endelig er enige om, hvad et terminal-felt er. Den enighed betyder mere, end den ser ud til. By-name-overloadet resolver et indeks gennem relationscachen og tæller derefter terminal-felter i et andet gennemløb af /AcroForm /Fields. Så snart cachen var fikset til at se group.a og group.b, ville et ufikset slette-gennemløb stadig have behandlet group som ét enkelt terminal-felt, og indeks 0 ville have fjernet parenten sammen med hver søskende og alle deres widgets. Slette-gennemløbet bruger nu samme HPDFLoadedFieldHasChildFields-test og samme visited-sæt, samler kun det fjernede childs widget-annotationer, stripper dem fra hver sides /Annots og fjerner parenten kun, når dens /Kids-array ender tomt. Regressionen tjekker alle tre steder, en fejl ville vise sig: parentens /Kids, sidens /Annots og den overlevende søskendes værdi og udseende, både efter en fuld omskrivning og efter en inkrementel opdatering

HotPDF RemoveFormField-diagram over søskende-overlevelse: slette-gennemløbet genbruger HPDFLoadedFieldHasChildFields og det visitede sæt fra discovery, stripper kun det navngivne child group.a fra AcroForm /Fields og sidens /Annots og beholder den delte parent, mens dens /Kids-array stadig holder den overlevende group.b
Discovery og sletning er endelig enige om, hvad et terminal-felt er, så at fjerne ét navngivet child efterlader dets søskendes værdi og udseende intakt efter en fuld omskrivning eller en inkrementel opdatering
// Fjern ét navngivet child; dets søskende og den delte parent overlever
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Type, flags og options resolveres stadig gennem parenten
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

Hvad skriver ResetLoadedFormField, når defaulten er nedarvet?

ResetLoadedFormField skriver en lokal /V, der er en frisk kopi af den nedarvede /DV med samme PDF-objekttype, og den validerer hele defaulten, før feltet røres. Objekttypen betyder noget, for de skalære getters flader alt ud til tekst. En checkbox-default er et name som /Yes, en multi-select list box-default er et array af strenge, og en tekst-default kan være en hexadecimal UTF-16-streng; at kopiere nogen af dem gennem GetLoadedFormFieldDefaultValue ville gøre et name til en streng, arrayet til en tom streng og hex-strengen til sine literale cifre. Resettet forgrener sig derfor på den nedarvede type: tekst- og choice-felter får et nyt strengobjekt, der bevarer IsHexadecimal-flaget, choice-felter med et array-default får et nyt array af nye strenge, og ikke-pushbutton-knapper får et nyt name-objekt. At kopiere i stedet for at pege på parentens objekter er bevidst: en /V, der delte parentens /DV-array eller dets objektnummer, ville ændre defaulten, næste gang nogen redigerede værdien. En default af forkert type, eller et choice-array, der indeholder andet end strenge, rejser en exception og efterlader /V og /I præcis, som de var. Pushbuttons, som ikke har nogen værdi (Tabel 226, bit 17), og signaturfelter falder tilbage til den ældre kun-strenge-sti

HotPDF-diagram over typet reset: ResetLoadedFormField forgrener sig på den nedarvede /DV-objekttype og skriver et friskt name-objekt til en checkbox, et nyt array af nye strenge til et multi-select choice, en streng, der bevarer IsHexadecimal, til hex-tekst, en tom streng eller /Off, når ingen /DV findes, og rejser uden at røre /V eller /I ved en type-mismatch
At kopiere i stedet for at pege på parentens objekter holder en senere værdi-redigering fra i stilhed at ændre defaulten, og pushbuttons plus signaturfelter falder tilbage til den ældre kun-strenge-sti

Når der ingen /DV findes noget oppe ad kæden, fastholder metoden sin clearing-kontrakt ved at skrive en lokal tom streng, eller /Off for et checkbox- eller radio-felt. At slette den lokale /V ville se pænere ud og være forkert: parenten kan holde en aktuel værdi, og at fjerne childets override ville i stilhed bringe den værdi tilbage. Det er også derfor, et single-field reset ikke er ResetForm-actionen fra §12.7.5.3, som en viewer kører over et sæt felter, når brugeren klikker på en knap, som beskrevet i at bygge AcroForm-felter og -actions med HotPDF. ResetLoadedFormField er en redigeringsoperation på ét loadet felt, med sin egen regel for intet-default-tilfældet, og den registrerer feltet gennem NoteLoadedFormFieldDirty, så inkrementel genberegning ser ændringen

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // Parent holder /DV [(b) (r)] på en MultiSelect list box: group.a får
    // sin egen /V [(b) (r)] og et friskt /I [0 2]; parenten er urørt
    Pdf.ResetLoadedFormField(Field.Index);
    // Skalære getters kan ikke repræsentere array-defaulten
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // tom
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

At holde /V, /I og /AS i overensstemmelse

Et reset er kun korrekt, hvis valgindeks og appearance-tilstand følger værdien, så ResetLoadedFormField slutter med de samme to reconcilers som SetFormFieldValue. HPDFReconcileChoiceSelection accepterer nu en array-værdi: den sletter den lokale /I uden at mutere den, matcher hver værdi mod eksporthalvdelen af hver /Opt-entry og skriver ét nyt sorteret /I, så et reset til [(b) (r)] mod options b, g, r giver /I [0 2]. ReconcileLoadedButtonAppearanceStates spørger nu efter den nedarvede type, så et child-checkbox, hvis /FT /Btn bor på parenten, endelig får sin /AS sat. På skrivesiden gemmer SetFormFieldValue og SetLoadedFormFieldDefaultValue et name-objekt for en nedarvet ikke-pushbutton-knap, selv når childet ikke har nogen lokal entry at kopiere typen fra. Og når EnsureLoadedFieldAppearanceStream genopbygger button-udseender, skriver den /AS /Off, medmindre værdien matcher on-tilstanden, og giver hver tilstands-stream en ordentlig /Type /XObject, /Subtype /Form og /BBox; før v2.754.4 kunne en regenerering af udseendet efter et reset flueben af boksen igen, før filen blev gemt

Grænser, der er værd at kende, før du bygger videre på dette

De skalære getters forbliver skalære. GetFormFieldValue og GetLoadedFormFieldDefaultValue returnerer en tom streng for en array-værdi, strengificerer tal og booleans som 42 eller true og rapporterer en hex-encoded streng i dens hexadecimale stavemåde. En /Parent-cykel afslutter gennemløbet uden en exception, så et felt, hvis type er tabt i en cykel, rapporterer lfftUnknown og flag på 0 i stedet for at fejle. SetFormFieldValue og ResetLoadedFormField skriver altid det child, du adresserer, og promoverer aldrig en værdi til den delte parent, hvilket er rigtigt for uafhængige children, men betyder, at radio-grupper skal adresseres gennem feltet, der ejer valget. Og hvert kald committer ét felt ad gangen; intet her gør en batch af resets transaktionel

Den nedarvede attribut-resolution, den forenede felttræ-klassificering og den typede reset, der er beskrevet her, er en del af loaded-form-API'en i HotPDF Delphi Component til Delphi og C++Builder, sammen med felt-oprettelsen, der dækkes i at tilføje AcroForm-felter til en loadet PDF i Delphi