Checkboxes en keuzerondjes flattenen als niet aangevinkt omdat de weergavestatus /AS nooit werd gesynchroniseerd met de veldwaarde /V. PDFium Component, de op PDFium gebaseerde VCL- en LCL-component voor Delphi, C++Builder en Lazarus, leest die waarde nu met FPDFAnnot_GetFormFieldValue, wat het bovenliggende veldwoordenboek oplost in plaats van de widget-annotatie
Het bugrapport dat hiertoe leidde is het soort dat je in eerste instantie niet vertrouwt. Een klant flattent een ondertekend toestemmingsformulier, opent het resultaat, en elke checkbox is leeg. Open het bronbestand in Acrobat en de vakjes zijn zichtbaar aangevinkt. Lees het bronbestand terug via dezelfde component en de veldwaarden zijn correct. Alleen de geflattende uitvoer verliest ze, en alleen voor checkboxes en keuzerondjes: tekstvelden op dezelfde pagina komen prima tevoorschijn
Waarom staan checkboxes na het flattenen niet aangevinkt
Omdat flattenen nooit naar /V kijkt. FPDFPage_Flatten bakt de weergavestream van de widget in de pagina-inhoud, en de weergave die het kiest is degene die door /AS wordt aangeduid. Als /AS nog steeds /Off zegt terwijl de veldwaarde zegt dat het vakje aan staat, bakt het flattenen trouw de uit-weergave. De waarde is nooit verloren gegaan; ze is nooit geraadpleegd
ISO 32000-1 §12.5.5 definieert het weergavewoordenboek /AP met drie mogelijke ingangen, /N, /R en /D. Voor een checkbox of keuzerondje is de /N-ingang geen stream maar een subwoordenboek waarvan de sleutels weergavestatusnamen zijn, en §12.5.2 maakt /AS de verplichte selector wanneer /N een subwoordenboek is. Een checkbox draagt dus twee vooraf opgebouwde weergaven en één pointer. Als de pointer verkeerd is, is de weergave op een manier fout die geen enkele correcte /V kan herstellen. Dit is ook waarom het faalpatroon verschilt van tekstvelden, die helemaal geen vooraf opgebouwde weergave hebben om te selecteren: een tekstveld-/N is een enkele stream die vanaf nul moet worden geregenereerd nadat de waarde is veranderd, dus GenerateFormAppearances behandelt de twee gevallen via volledig gescheiden codepaden en alleen het knoppad was defect
Waar leeft de checkboxwaarde eigenlijk
Op het veldwoordenboek, niet op de widget. ISO 32000-1 §12.7.5.2 beschrijft checkboxes en keuzerondjes als knopvelden waarvan /V een naamobject is dat de huidige weergavestatus benoemt, en §12.7.3.1 plaatst /V onder de ingangen die alle veldwoordenboeken gemeen hebben. De widget-annotatie gedefinieerd in §12.5.6.19 levert /AS en /AP aan. Niets in de specificatie verplicht een widget om /V te dragen
// 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 is niet defect. Het contract ervan is precies wat de naam zegt: haal een tekenreeksingang op uit het annotatiewoordenboek dat je hebt meegegeven. Ernaar vragen om /V op object 13 levert niets op omdat object 13 werkelijk geen /V heeft. Het defect zat bij de aanroeper, die een plat objectmodel veronderstelde dat ISO 32000-1 nooit heeft beloofd
Wanneer delen veld en widget één woordenboek
Wanneer een veld precies één widget heeft. §12.5.6.19 staat toe dat het veldwoordenboek en zijn enkele widget-annotatie tot één object worden samengevoegd, en de meeste authoring-tools nemen die kortere weg. In een samengevoegd object staan /FT, /T, /V, /AS en /AP allemaal naast elkaar, dus een lezing van /V op widgetniveau slaagt en de hele bug blijft onzichtbaar
Zodra een veld twee of meer widgets bezit, is de samenvoeging onmogelijk, en §12.7.3.1 vereist dat de widgets /Kids worden van een afzonderlijk veldwoordenboek. Elke keuzerondjegroep heeft deze vorm van nature. Zo ook toestemmingscheckboxes die herhaald worden in een koptekst en een voettekst, en elk veld dat een authoring-tool naar een tweede pagina heeft gekopieerd. Dat is de volledige verklaring waarom het defect een regressiesuite overleefde: het testcorpus zat vol met formulieren met één widget en de klantbestanden niet. Als je zelf widgets doorloopt in plaats van op de component te vertrouwen, duikt dezelfde asymmetrie op in de opsommingsvolgorde, en de aantekeningen over PDF-formuliervveldnavigatie met PDFium Component behandelen hoe een paginaniveau-annotatiewandeling zich verhoudt tot de documentniveau-veldboom
De waarde lezen zoals PDFium het bedoelt
FPDFAnnot_GetFormFieldValue is de juiste API, en die was al enige tijd in de component gebonden zonder dat het checkboxpad hem gebruikte. Ze neemt naast de annotatie ook de formulierhandle mee, wat het signaal is dat ertoe doet: met de form-fill-omgeving beschikbaar lost PDFium de annotatie op naar zijn formulierbesturingselement en leest de waarde uit het veldobject, zodat ze voor zowel samengevoegde als gesplitste indelingen het juiste antwoord geeft
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;
Twee details in dat fragment zijn makkelijk verkeerd te doen. De teruggegeven lengte is een bytetelling voor UTF-16-tekst inclusief de terminator, dus het aantal tekens is buflen div 2 - 1 en een waarde van 2 betekent een lege tekenreeks. De bewaking buflen >= 4 betekent daarom minstens één echt teken, wat voorkomt dat een veld zonder enige /V zijn /AS overschreven krijgt met een lege naam
Waar komen /AS en /AP /N eigenlijk overeen
Ze komen overeen op een naam, en de naam wordt gekozen door wie het bestand heeft geproduceerd. §12.7.5.2 vereist dat de uit-status /Off heet, en laat de aan-status volledig aan de producent over. /Yes is een conventie, geen regel. Acrobat schrijft /Yes, maar heel wat generators schrijven /On, /1, /Choice1, of een gelokaliseerd woord, en een keuzerondjegroep geeft elk kind normaal een aparte aan-status-naam zodat de groep kan uitdrukken welke knop is geselecteerd. Dit is precies waarom /V letterlijk overnemen in /AS de juiste bewerking is en geen truc: voor een aangevinkt besturingselement rapporteert PDFium de aan-status-naam die het bestand zelf definieert, en voor een niet-aangevinkt rapporteert het Off, dus de waarde die je in /AS schrijft is gegarandeerd een sleutel die in dat widget-/AP/N-subwoordenboek bestaat. /Yes hardcoderen zou werken op Acrobat-uitvoer en stilzwijgend overal elders breken
Volgorde van bewerkingen, en waar nog voorzichtigheid nodig is
De volgorde is vast en onvergevingsgezind: formulierinvulling inschakelen, waarden toewijzen, weergaven regenereren, flattenen, dan opslaan. Sla de regeneratiestap over en FPDFPage_Flatten vindt lege of verouderde weergavestreams en bakt ze zonder klagen, wat stil gegevensverlies is in plaats van een foutmelding
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');
Twee eerlijke beperkingen blijven bestaan. Ten eerste schrijft de synchronisatie de veldwaarde in de /AS van elke widget van dat veld, wat correct is voor checkboxes maar benaderend voor keuzerondjegroepen waarvan elk kind zijn eigen aan-status-naam definieert; een kind wiens /AP/N geen ingang heeft die overeenkomt met de geschreven /AS heeft geen weergave om te selecteren onder §12.5.5, zodat een niet-geselecteerde knop kan flattenen naar niets in plaats van een lege cirkel. Een keuzerondjegroep controleren met FPDFAnnot_GetFormControlIndex vóór het flattenen is de paar regels waard. Ten tweede is niets hiervan van toepassing op XFA, waar de waarde leeft in een XML-gegevenspakket in plaats van in de AcroForm-woordenboeken, een scheiding die wordt behandeld in de aantekeningen over XFA-veldbewerkingen die niet worden bewaard. De algemene les blijft de moeite waard om te onthouden na deze ene fix: wanneer een API naast de annotatie ook de formulierhandle meeneemt, vertelt het je dat het de veldhiërarchie voor je zal oplossen, en wanneer het alleen de annotatie meeneemt, zal het precies het object lezen dat je hebt doorgegeven. Dat onderscheid regelt ook gegevensuitwisseling, want het exporteren en importeren van XFDF-formuliergegevens werkt met volledig gekwalificeerde veldnamen, nooit met widgetposities
Formulierflattening is een van die functies die eruitziet als een enkele API-aanroep en blijkt een contract tussen drie woordenboeken te zijn. Als je liever werkt tegen een component die dat contract al vastlegt, dan brengt de PDFium Component voor Delphi en C++Builder de weergaveregeneratie, het flattenen en de toegang tot formuliervelden die hier zijn beschreven als gewone eigenschappen en methoden