Avkrysningsbokser og radioknapper flates som uavkrysset fordi utseendetilstanden /AS aldri ble synkronisert med feltverdien /V. PDFium Component, den PDFium-baserte VCL- og LCL-komponenten for Delphi, C++Builder, og Lazarus, leser nå den verdien med FPDFAnnot_GetFormFieldValue, som løser opp den overordnede feltordboken i stedet for widget-annotasjonen
Feilrapporten som ledet hit er den typen du mistror først. En kunde flater ut et signert samtykkeskjema, åpner resultatet, og hver avkrysningsboks er tom. Åpne kildefilen i Acrobat og boksene er synlig avkrysset. Les kildefilen tilbake gjennom samme komponent og feltverdiene er korrekte. Bare den flatede utdataen mister dem, og bare for avkrysningsbokser og radioknapper: tekstfelt på samme side kommer ut fint
Hvorfor er avkrysningsbokser uavkrysset etter flating?
Fordi flating aldri ser på /V. FPDFPage_Flatten baker widget-utseendestrømmen inn i sideinnholdet, og utseendet den velger er det navngitt av /AS. Hvis /AS fortsatt sier /Off mens feltverdien sier boksen er på, baker flating lydig det avslåtte utseendet. Verdien gikk aldri tapt; den ble aldri konsultert
ISO 32000-1 §12.5.5 definerer utseendeordboken /AP med tre mulige oppføringer, /N, /R, og /D. For en avkrysningsboks eller radioknapp er /N-oppføringen ikke en strøm men en underordbok hvis nøkler er utseendetilstandsnavn, og §12.5.2 gjør /AS til den påkrevde velgeren når /N er en underordbok. Så en avkrysningsboks bærer to ferdigbygde utseender og én peker. Få pekeren feil og renderingen er feil på en måte ingen mengde korrekt /V vil reparere. Dette er også hvorfor feilmodusen skiller seg fra tekstfelt, som ikke har noe ferdigbygget utseende å velge i det hele tatt: et tekstfelt /N er en enkelt strøm som må regenereres fra bunnen av etter at verdien endres, så GenerateFormAppearances håndterer de to tilfellene gjennom helt separate kodeveier og bare knapp-veien var ødelagt
Hvor bor egentlig avkrysningsboks-verdien?
På feltordboken, ikke på widget-en. ISO 32000-1 §12.7.5.2 beskriver avkrysningsbokser og radioknapper som knappefelt hvis /V er et navneobjekt som navngir den gjeldende utseendetilstanden, og §12.7.3.1 plasserer /V blant oppføringene felles for alle feltordbøker. Widget-annotasjonen definert i §12.5.6.19 bidrar med /AS og /AP. Ingenting i spesifikasjonen forplikter en widget til å 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. Kontrakten dens er akkurat det navnet sier: hent en strengoppføring fra annotasjonsordboken du ga den. Å spørre den om /V på objekt 13 returnerer ingenting fordi objekt 13 genuint ikke har noe /V. Defekten var i kalleren, som antok en flat objektmodell ISO 32000-1 aldri lovet
Når deler felt og widget én ordbok?
Når som helst et felt har nøyaktig én widget. §12.5.6.19 tillater at feltordboken og dens enkelte widget-annotasjon slås sammen til ett objekt, og de fleste forfatterverktøy tar den snarveien. I et sammenslått objekt sitter /FT, /T, /V, /AS, og /AP alle side om side, så en widget-nivå-lesing av /V lykkes og hele feilen forblir usynlig
I det øyeblikket et felt eier to eller flere widgets er sammenslåingen umulig, og §12.7.3.1 krever at widgetene blir /Kids av en separat feltordbok. Hver radiogruppe er i denne formen by construction. Det er også samtykke-avkrysningsbokser gjentatt i en topptekst og en bunntekst, og ethvert felt et forfatterverktøy har kopiert til en andre side. Det er hele forklaringen på hvorfor defekten overlevde en regresjonssuite: testkorpuset var fullt av enkelt-widget-skjemaer og kundefilene var det ikke. Hvis du går gjennom widgets selv fremfor å stole på komponenten, dukker samme asymmetri opp i opplistningsrekkefølge, og notatene om PDF-skjemafeltnavigasjon med PDFium Component dekker hvordan en side-nivå-annotasjonsgjennomgang forholder seg til det dokument-nivå-felttreet
Å lese verdien slik PDFium mener det
FPDFAnnot_GetFormFieldValue er den korrekte API-en, og den hadde vært koblet inn i komponenten en stund uten at avkrysningsboks-veien brukte den. Den tar skjemahåndtaket i tillegg til annotasjonen, som er signalet som betyr noe: med skjema-fyll-miljøet tilgjengelig, løser PDFium opp annotasjonen til sin skjemakontroll og leser verdien fra feltobjektet, så den returnerer riktig svar for både sammenslåtte og delte oppsett
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 utdraget er lette å få feil. Den returnerte lengden er et byteantall for UTF-16-tekst inkludert terminatoren, så tegnantallet er buflen div 2 - 1 og en verdi på 2 betyr en tom streng. Vakten buflen >= 4 betyr derfor minst ett ekte tegn, som er det som hindrer et felt uten noe /V i det hele tatt fra å få sin /AS overskrevet med et tomt navn
Hva /AS og /AP /N egentlig blir enige om
De blir enige om et navn, og navnet er valgt av den som produserte filen. §12.7.5.2 krever at av-tilstanden kalles /Off, og overlater på-tilstanden helt til produsenten. /Yes er en konvensjon, ikke en regel. Acrobat skriver /Yes, men mange generatorer skriver /On, /1, /Choice1, eller et lokalisert ord, og en radiogruppe gir normalt hver kid et distinkt på-tilstand-navn slik at gruppen kan uttrykke hvilken knapp som er valgt. Dette er nøyaktig hvorfor å kopiere /V ordrett inn i /AS er den riktige operasjonen fremfor en hack: for en avkrysset kontroll rapporterer PDFium på-tilstand-navnet filen selv definerer, og for en uavkrysset rapporterer den Off, så verdien du skriver inn i /AS er garantert å være en nøkkel som finnes i den widgetens /AP /N-underordbok. Å hardkode /Yes ville fungert på Acrobat-utdata og stille ødelagt overalt ellers
Operasjonsrekkefølge, og hvor det fortsatt trenger omsorg
Sekvensen er fast og uforsonlig: aktiver skjema-fyll, tildel verdier, regenerer utseender, flat ut, lagre deretter. Hopp over regenereringssteget og FPDFPage_Flatten finner tomme eller foreldede utseendestrømmer og baker dem uten klage, som er et stille datatap fremfor en feilretur
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 grenser gjenstår. For det første skriver synkroniseringen feltverdien inn i /AS for hver widget av det feltet, som er korrekt for avkrysningsbokser men omtrentlig for radiogrupper hvis kids hver definerer sitt eget på-tilstand-navn; en kid hvis /AP /N ikke har noen oppføring som matcher den skrevne /AS har ikke noe utseende å velge under §12.5.5, så en uvalgt knapp kan flates til ingenting i stedet for en tom sirkel. Å revidere en radiogruppe med FPDFAnnot_GetFormControlIndex før flating er verdt de få linjene. For det andre gjelder ingenting av dette for XFA, hvor verdien bor i en XML-datapakke fremfor i AcroForm-ordbøkene, et skille dekket i notatene om XFA-feltredigeringer som ikke persisteres. Den generelle lærdommen er verdt å holde forbi denne ene fiksen: når som helst en API tar skjemahåndtaket i tillegg til annotasjonen, forteller den deg at den vil løse opp felthierarkiet for deg, og når som helst den bare tar annotasjonen vil den lese akkurat det objektet du sendte. Det skillet styrer også datautveksling, siden eksport og import av XFDF-skjemadata arbeider i fullt kvalifiserte feltnavn, aldri i widget-posisjoner
Skjemaflating er en av de funksjonene som ser ut som ett enkelt API-kall og viser seg å være en kontrakt mellom tre ordbøker. Hvis du heller vil jobbe mot en komponent som allerede koder inn den kontrakten, leverer PDFium Component for Delphi og C++Builder utseende-regenerering, flating, og skjemafelt-tilgang beskrevet her som ordinære egenskaper og metoder