Afkrydsningsfelter og radioknapper flatter som uafkrydset, fordi udseendetilstanden /AS aldrig blev synkroniseret med feltværdien /V. PDFium Component, den PDFium-baserede VCL- og LCL-komponent til Delphi, C++Builder og Lazarus, læser nu den værdi med FPDFAnnot_GetFormFieldValue, som opløser forældre-felt-dictionaryet frem for widget-annotationen
Fejlrapporten, der førte hertil, er den slags man mistror først. En kunde flatter en signeret samtykkeformular, åbner resultatet, og hvert afkrydsningsfelt er tomt. Åbn kildefilen i Acrobat, og boksene er synligt afkrydsede. Læs kildefilen tilbage gennem samme komponent, og feltværdierne er korrekte. Kun det flatede output taber dem, og kun for afkrydsningsfelter og radioknapper: tekstfelter på samme side kommer ud fint
Hvorfor er afkrydsningsfelter uafkrydsede efter flatning?
Fordi flatning aldrig kigger på /V. FPDFPage_Flatten bager widget-udseendestrømmen ind i sideindholdet, og det udseende, den vælger, er det navngivet af /AS. Hvis /AS stadig siger /Off, mens feltværdien siger, at boksen er tændt, bager flatning trofast off-udseendet ind. Værdien gik aldrig tabt; den blev aldrig konsulteret
ISO 32000-1 §12.5.5 definerer udseende-dictionaryet /AP med tre mulige poster, /N, /R og /D. For et afkrydsningsfelt eller en radioknap er /N-posten ikke en strøm, men et under-dictionary, hvis nøgler er udseendetilstandsnavne, og §12.5.2 gør /AS til den påkrævede vælger, når /N er et under-dictionary. Så et afkrydsningsfelt bærer to forudbyggede udseender og én pointer. Få pointeren forkert, og renderingen er forkert på en måde, ingen mængde korrekt /V vil reparere. Dette er også hvorfor fejltilstanden adskiller sig fra tekstfelter, som slet ikke har et forudbygget udseende at vælge: en tekstfelt-/N er en enkelt strøm, der skal genereres fra bunden, efter værdien ændres, så GenerateFormAppearances håndterer de to tilfælde gennem helt separate kodestier, og kun knap-stien var i stykker
Hvor bor afkrydsningsfelt-værdien egentlig?
På felt-dictionaryet, ikke på widgeten. ISO 32000-1 §12.7.5.2 beskriver afkrydsningsfelter og radioknapper som knap-felter, hvis /V er et navneobjekt, der navngiver den aktuelle udseendetilstand, og §12.7.3.1 placerer /V blandt posterne fælles for alle felt-dictionaries. Widget-annotationen defineret i §12.5.6.19 bidrager med /AS og /AP. Intet i specifikationen forpligter en widget til at bære /V
// Wrong: reads the widget annotation dictionary directly
buflen := FPDFAnnot_GetStringValue(Annot, 'V', nil, 0);
// For most real forms buflen comes back as 2 (an empty UTF-16 string),
// so /AS is never written and the box flattens as Off
{ What the two objects look like when the field has several widgets:
12 0 obj % field dictionary (the parent)
<< /FT /Btn /T (Consent) /V /On
/Kids [ 13 0 R 14 0 R ] >>
endobj
13 0 obj % widget annotation (a kid)
<< /Type /Annot /Subtype /Widget /Parent 12 0 R
/AS /Off
/AP << /N << /On 20 0 R /Off 21 0 R >> >> >>
endobj }
FPDFAnnot_GetStringValue er ikke defekt. Dens kontrakt er præcis hvad dens navn siger: hent en strengpost fra det annotations-dictionary, man overgav den. At spørge den om /V på objekt 13 returnerer intet, fordi objekt 13 reelt ikke har noget /V. Defekten var i kalderen, som antog en flad objektmodel, ISO 32000-1 aldrig lovede
Hvornår deler felt og widget ét dictionary?
Når som helst et felt har præcis én widget. §12.5.6.19 tillader felt-dictionaryet og dets enkelte widget-annotation at blive flettet ind i ét objekt, og de fleste forfatterværktøjer tager den genvej. I et flettet objekt sidder /FT, /T, /V, /AS og /AP alle side om side, så en widget-niveau-læsning af /V lykkes, og hele fejlen forbliver usynlig
I det øjeblik et felt ejer to eller flere widgets, er flettet umulig, og §12.7.3.1 kræver at widgetsene bliver /Kids af et separat felt-dictionary. Hver radiogruppe er i denne form af konstruktion. Det er også samtykke-afkrydsningsfelter gentaget i en header og en footer, og ethvert felt et forfatterværktøj har kopieret til en anden side. Det er hele forklaringen på hvorfor defekten overlevede en regressionssuite: testkorpuset var fuldt af enkelt-widget-formularer, og kundefilerne var ikke. Hvis man gennemgår widgets selv frem for at stole på komponenten, viser den samme asymmetri sig i opremsningsrækkefølge, og noterne om PDF-formularfelt-navigation med PDFium Component dækker hvordan en side-niveau-annotationsgennemgang forholder sig til dokument-niveau-felt-træet
At læse værdien på den måde PDFium tiltænker
FPDFAnnot_GetFormFieldValue er den korrekte API, og den havde været bundet i komponenten i nogen tid uden at afkrydsningsfelt-stien brugte den. Den tager formularhåndtaget såvel som annotationen, hvilket er signalet der betyder noget: med formular-udfyldningsmiljøet tilgængeligt, opløser PDFium annotationen til dens formularkontrol og læser værdien fra feltobjektet, så den returnerer det rigtige svar for både flettede og splittede layouts
FPDF_FORMFIELD_CHECKBOX, FPDF_FORMFIELD_RADIOBUTTON:
begin
// /AP is prebuilt per state; only /AS has to be synchronised with /V.
// FPDFAnnot_GetFormFieldValue resolves the parent field dictionary,
// which is where ISO 32000-1 12.7.5.2 keeps the value.
buflen := FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, nil, 0);
if buflen >= 4 then
begin
SetLength(OrigVal, buflen div 2 - 1);
FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, PWideChar(OrigVal), buflen);
FPDFAnnot_SetStringValue(Annot, 'AS', Pointer(OrigVal));
end;
end;
To detaljer i det uddrag er lette at få forkert. Den returnerede længde er en byte-tælling for UTF-16-tekst inklusive terminatoren, så tegn-tællingen er buflen div 2 - 1, og en værdi på 2 betyder en tom streng. Vagten buflen >= 4 betyder derfor mindst ét rigtigt tegn, hvilket er hvad der forhindrer et felt uden noget /V overhovedet i at få sin /AS overskrevet med et tomt navn
Hvad /AS og /AP /N egentlig er enige om
De er enige om et navn, og navnet vælges af hvem end der producerede filen. §12.7.5.2 kræver, at off-tilstanden kaldes /Off, og lader on-tilstanden helt være op til producenten. /Yes er en konvention, ikke en regel. Acrobat skriver /Yes, men mange generatorer skriver /On, /1, /Choice1, eller et lokaliseret ord, og en radiogruppe giver normalt hvert barn et distinkt on-tilstandsnavn, så gruppen kan udtrykke hvilken knap der er valgt. Det er præcis hvorfor at kopiere /V bogstaveligt ind i /AS er den rigtige operation frem for et hack: for en afkrydset kontrol rapporterer PDFium det on-tilstandsnavn, filen selv definerer, og for en uafkrydset rapporterer den Off, så værdien man skriver ind i /AS er garanteret at være en nøgle, der findes i den widgets /AP /N-under-dictionary. At hårdkode /Yes ville virke på Acrobat-output og stille bryde alle andre steder
Operationsrækkefølge, og hvor der stadig kræves omhu
Sekvensen er fast og ubarmhjertig: aktivér form-fill, tildel værdier, genopbyg udseender, flat, gem så. Spring genopbygningstrinet over, og FPDFPage_Flatten finder tomme eller forældede udseendestrømme og bager dem uden klage, hvilket er stille datatab frem for en fejl-return
Pdf.FileName := FormPath;
Pdf.FormFill := True; // required: FormHandle must exist
Pdf.Active := True;
Pdf.FormField[0] := 'On'; // writes /V only
Pdf.GenerateFormAppearances; // syncs /AS for buttons, rebuilds /AP for text
if Pdf.FlattenAllPages(FLAT_PRINT) then
Pdf.SaveAs('consent-flat.pdf');
To ærlige grænser forbliver. For det første skriver synkroniseringen feltværdien ind i /AS på hver widget af det felt, hvilket er korrekt for afkrydsningsfelter, men approksimativt for radiogrupper, hvis børn hver definerer deres eget on-tilstandsnavn; et barn, hvis /AP /N ingen post har matchende den skrevne /AS, har intet udseende at vælge under §12.5.5, så en ikke-valgt knap kan flatte til intet frem for en tom cirkel. At revidere en radiogruppe med FPDFAnnot_GetFormControlIndex før flatning er de få linjer værd. For det andet gælder intet af dette for XFA, hvor værdien bor i en XML-datapakke frem for i AcroForm-dictionaries, en adskillelse dækket i noterne om XFA-feltredigeringer, der ikke persisteres. Den generelle lektie er værd at holde fast i efter denne rettelse: når som helst en API tager formularhåndtaget ud over annotationen, fortæller den dig, at den vil opløse felt-hierarkiet for dig, og når som helst den kun tager annotationen, vil den læse præcis det objekt, man overgav. Den skelnen styrer også dataudveksling, siden eksport og import af XFDF-formulardata arbejder i fuldt kvalificerede feltnavne, aldrig i widget-positioner
Formularflatning er en af de funktioner, der ser ud som ét enkelt API-kald og viser sig at være en kontrakt mellem tre dictionaries. Hvis man hellere vil arbejde mod en komponent, der allerede indkoder den kontrakt, leverer PDFium Component til Delphi og C++Builder udseende-genopbygning, flatning og formularfelt-adgang beskrevet her som almindelige egenskaber og metoder