Teknisk artikkel

AcroForm arvede feltverdier og nullstillinger i Delphi

HotPDF Delphi Component behandler /FT, /Ff, /V og /DV på et lastet AcroForm-felt som arvbare attributter, løst ved å gå gjennom /Parent-kjeden. Siden v2.754.3 og v2.754.4 forblir et navngitt barn hvis type kommer fra forelderen, individuelt adresserbart, RemoveFormField lar søskenene være i fred, og ResetLoadedFormField kopierer den arvede standardverdien med sin opprinnelige PDF-objekttype. Før det ble en overraskende mengde vanlige skjemaer feillest

Skjemaet som avslører alt dette, er ikke eksotisk. Et forfatterverktøy bygger en gruppenode group som bærer /FT /Ch, feltflaggene og alternativlisten én gang, og henger to navngitte barn a og b under den, hvert et sammenslått felt-pluss-widget-ordbok med ingenting annet enn /T, /Parent, /Rect og sin egen /V. Det er en fullstendig lovlig måte å dele attributter på, og det er nøyaktig tilfellet Limits-avsnittet i å sette skjemafeltverdier i en lastet PDF med Delphi flagget som uhandtert: knappeavstemmingen så bare på lokal /FT. Denne artikkelen plukker opp der den sluttet, og dekker hvordan felttreet klassifiseres, hvordan arvede verdier leses, og hva en enkeltsfelt-nullstilling har lov til å skrive

Hvilke AcroForm-oppføringer kan et felt arve fra forelderen sin?

ISO 32000-1 §12.7.3.1, tabell 220, merker /FT, /Ff, /V og /DV som arvbare, og tabell 229 i §12.7.4.3 gjør det samme for et tekstfelts /MaxLen, så enhver leser som bare ser på den lokale ordboken, rapporterer feil type, feil flagg og en tom verdi for et fullt gyldig barn. HotPDF leder alle disse lesingene gjennom én intern resolver, HPDFLoadedInheritedFieldObject, som sjekker ordboken for nøkkelen, løser en indirekte referanse hvis den finner én, og ellers følger /Parent i høyst 128 nivåer, fordi misdannede filer kan bygge /Parent-sykluser som ikke har noe med /Kids å gjøre. De offentlige getterne ligger oppå den: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue og alternativhjelperne GetLoadedFormFieldOptionCount og GetLoadedFormFieldOptions, som også plukker opp en /Opt-array lagret på forelderen. Én regel i resolveren er lett å få galt: gjennomgangen stopper ved den første ordboken som inneholder nøkkelen, selv om verdien der er en tom streng. En lokal /V () er en bevisst overstyring som maskerer forelderen, ikke et hull som skal fylles lenger opp i treet

Diagram over HotPDF arvede AcroForm-attributter: en gruppenode bærer /FT, /Ff og /Opt én gang mens navngitte barn group.a og group.b holder bare /T, /Parent, /Rect og en lokal /V, som viser HPDFLoadedInheritedFieldObject gå gjennom /Parent opptil 128 nivåer der den første ordboken som holder en nøkkel vinner, og en tom lokal verdi maskerer forelderen
HotPDF løser /FT, /Ff, /V, /DV og /Opt gjennom én forelder-gående resolver, så et navngitt barn forblir adresserbart mens en lokal tom verdi bevisst overstyrer alt 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; barnet
    // 'group.b' bærer bare /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 feil test for et terminalfelt?

Fordi en forelder kan levere typen og fortsatt eie navngitte barnefelt, så tilstedeværelsen av /FT sier ingenting om hvor felttreet slutter. Den gamle traverseringen erklærte en node som terminal når den hadde sin egen /FT eller ingen /Kids. I skjemaet over har group både /FT /Ch og /Kids, så den ble registrert som ett felt kalt group med to widgeter, og de fullt kvalifiserte navnene group.a og group.b forsvant rett og slett. GetFormFieldCount returnerte 1, et oppslag etter barnenavn feilet, og SetFormFieldValue kunne bare skrive den delte forelderen. Erstatningstesten, HPDFLoadedFieldHasChildFields, ser på barna i stedet for forelderen: et barn er et barnefelt hvis det har sin egen /T, har sine egne /Kids, eller ikke er en /Subtype /Widget-ordbok i det hele tatt. Bare når ingen barn kvalifiserer, er noden terminal, med barna sine behandlet som widget-annoteringene sine

De to kanttilfellene som formet den regelen, kommer begge fra sammenslåtte ordbøker, som §12.7.3.1 tillater når et felt har én enkelt widget. En navngitt sammenslått ordbok bærer /Subtype /Widget og er likevel et barnefelt, så subtypen alene kan ikke sende den inn i forelderens anonyme widgetliste; /T vinner. Det omvendte skjer også: noen produsenter gjentar forelderens /FT på hver anonym widget, så /FT kan ikke brukes som bevis for at en widget starter et nytt felt heller. Klassifiseringen deles av relasjonscachen, FormFieldExists og RemoveFormField, og hver av de gjennomgangene registrerer nå ordbøkene den allerede har besøkt, og stopper etter 128 nivåer. En regressjonsfil hvis gruppe lister seg selv to ganger, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], rapporterer fortsatt nøyaktig to felt i stedet for å rekursere i evighet eller telle samme node to ganger

Hvordan unngår RemoveFormField å slette søskenfelt?

RemoveFormField sletter nå bare barnet du navngir, fordi oppdagelse og sletting endelig er enige om hva et terminalfelt er. Den enigheten betyr mer enn det ser ut som. Overloaden etter navn løser en indeks gjennom relasjonscachen og teller deretter terminalfelt i en andre gjennomgang over /AcroForm /Fields. Så snart cachen var fikset til å se group.a og group.b, ville en ufikset slettingsgjennomgang fortsatt ha behandlet group som ett enkelt terminalfelt, og indeks 0 ville ha fjernet forelderen sammen med hvert søsken og alle deres widgeter. Slettingsgjennomgangen bruker nå samme HPDFLoadedFieldHasChildFields-test og samme besøkt-sett, samler widget-annoteringene til det fjernede barnet alene, stripper de fra hver sides /Annots, og fjerner forelderen bare når /Kids-arrayen dens ender tom. Regresjonen sjekker alle tre stedene en feil ville vise seg: forelderens /Kids, side-/Annots, og det overlevende søsknens verdi og utseende, både etter en full omskriving og etter en inkrementell oppdatering

Diagram over HotPDF RemoveFormField søskenoverlevelse: slettingsgjennomgangen gjenbruker HPDFLoadedFieldHasChildFields og det besøkte settet fra oppdagelsen, stripper bare det navngitte barnet group.a fra AcroForm /Fields og side-/Annots, og beholder den delte forelderen mens /Kids-arrayen dens fortsatt holder det overlevende group.b
Oppdagelse og sletting er endelig enige om hva et terminalfelt er, så å fjerne ett navngitt barn lar søsknens verdi og utseende være intakt etter en full omskriving eller en inkrementell oppdatering
// Fjern ett navngitt barn; dets søsken og den delte forelderen overlever
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Type, flagg og alternativer løses fortsatt gjennom forelderen
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

Hva skriver ResetLoadedFormField når standardverdien er arvet?

ResetLoadedFormField skriver en lokal /V som er en fersk kopi av den arvede /DV med samme PDF-objekttype, og den validerer hele standardverdien før den rører feltet. Objekttypen betyr noe fordi skalar-getterne flater alt ut til tekst. En avkrysningsboks-standard er et navn som /Yes, en multi-select listeboks-standard er en array av strenger, og en tekststandard kan være en heksadesimal UTF-16-streng; å kopiere noen av dem gjennom GetLoadedFormFieldDefaultValue ville gjøre navnet om til en streng, arrayen til en tom streng og heksstrengen til sine bokstavelige siffer. Nullstillingen forgrener seg derfor på den arvede typen: tekst- og valgfelt får et nytt strengobjekt som beholder IsHexadecimal-flagget, valgfelt med array-standard får en ny array av nye strenger, og ikke-pushbutton-knapper får et nytt navneobjekt. Å kopiere, i stedet for å peke på forelderens objekter, er bevisst: en /V som delte forelderens /DV-array eller objektnummeret dens, ville endre standardverdien neste gang noen redigerte verdien. En standard av feil type, eller en valg-array som inneholder noe annet enn strenger, reiser et unntak og lar /V og /I være nøyaktig som de var. Pushbuttons, som ikke har noen verdi (tabell 226, bit 17), og signaturfelt faller tilbake til den eldre bare-streng-stien

Diagram over HotPDF typet nullstilling: ResetLoadedFormField forgrener seg på den arvede /DV-objekttypen, skriver et ferskt navneobjekt for en avkrysningsboks, en ny array av nye strenger for et multi-select valg, en streng som beholder IsHexadecimal for heks tekst, en tom streng eller /Off når ingen /DV finnes, og reiser uten å røre /V eller /I ved typemismatch
Å kopiere i stedet for å peke på forelderens objekter hindrer en senere verdireduering i å stille endre standardverdien, og pushbuttons pluss signaturfelt faller tilbake til den eldre bare-streng-stien

Når ingen /DV finnes noe sted oppover kjeden, beholder metoden sin tømmingskontrakt ved å skrive en lokal tom streng, eller /Off for et avkrysnings- eller radiofelt. Å slette den lokale /V ville sett penere ut og vært feil: forelderen kan holde en gjeldende verdi, og å fjerne barnets overstyring ville stille bringe den verdien tilbake. Dette er også grunnen til at en enkeltsfelt-nullstilling ikke er ResetForm-handlingen fra §12.7.5.3, som et visningsprogram kjører over et sett med felt når brukeren klikker en knapp, som beskrevet i å bygge AcroForm-felt og -handlinger med HotPDF. ResetLoadedFormField er en redigeringsoperasjon på ett lastet felt, med sin egen regel for tilfellet uten standardverdi, og den registrerer feltet gjennom NoteLoadedFormFieldDirty slik at inkrementell omberegning ser endringen

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // Forelderen holder /DV [(b) (r)] på en MultiSelect listeboks: group.a får
    // sin egen /V [(b) (r)] og en fersk /I [0 2]; forelderen er urørt
    Pdf.ResetLoadedFormField(Field.Index);
    // Skalar-gettere kan ikke representere array-standarden
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // tom
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

Å holde /V, /I og /AS i samsvar

En nullstilling er bare korrekt hvis valgindeksen og utseendetilstanden følger verdien, så ResetLoadedFormField avslutter med de samme to avstemmerne som SetFormFieldValue. HPDFReconcileChoiceSelection godtar nå en arrayverdi: den sletter den lokale /I uten å mutere den, matcher hver verdi mot eksporthalvdelen av hver /Opt-oppføring, og skriver én ny sortert /I, så en nullstilling til [(b) (r)] mot alternativene b, g, r gir /I [0 2]. ReconcileLoadedButtonAppearanceStates spør nå etter den arvede typen, så et barn-avkrysningsfelt hvis /FT /Btn bor på forelderen, får endelig /AS satt. På skrivesiden lagrer SetFormFieldValue og SetLoadedFormFieldDefaultValue et navneobjekt for en arvet ikke-pushbutton-knapp selv når barnet ikke har noen lokal oppføring å kopiere typen fra. Og når EnsureLoadedFieldAppearanceStream bygger knappeutseender på nytt, skriver den /AS /Off med mindre verdien matcher på-tilstanden, og gir hver tilstandsstrøm en ordentlig /Type /XObject, /Subtype /Form og /BBox; før v2.754.4 kunne regenerering av utseendet etter en nullstilling hake av boksen igjen før filen ble lagret

Grenser verdt å kjenne før du bygger på dette

Skalar-getterne forblir skalarer. GetFormFieldValue og GetLoadedFormFieldDefaultValue returnerer en tom streng for en arrayverdi, strengifiserer tall og boolske verdier som 42 eller true, og rapporterer en hekskodet streng i sin heksadesimale stavemåte. En /Parent-syklus avslutter gjennomgangen uten unntak, så et felt hvis type går tapt i en syklus rapporterer lfftUnknown og flagg 0 i stedet for å feile. SetFormFieldValue og ResetLoadedFormField skriver alltid barnet du adresserer, og promoverer aldri en verdi til den delte forelderen, noe som er riktig for uavhengige barn, men betyr at radiogrupper bør adresseres gjennom feltet som eier valget. Og hvert kall committer ett felt på egen hånd; ingenting her gjør en bunke nullstillinger transaksjonell

Den arvede attributtløsningen, den forente felttre-klassifiseringen og den typede nullstillingen beskrevet her, er del av det lastede skjema-API-et i HotPDF Delphi Component for Delphi og C++Builder, ved siden av feltopprettelsen dekket i å legge til AcroForm-felt i en lastet PDF i Delphi