Teknisk artikel

PDF-kryssrutebugg vid platta: fältvärde vs widget i Delphi

Kryssrutor och radioknappar plattas ur som avmarkerade eftersom utseendetillståndet /AS aldrig synkroniserades med fältvärdet /V. PDFium Component, den PDFium-baserade VCL- och LCL-komponenten för Delphi, C++Builder och Lazarus, läser nu det värdet med FPDFAnnot_GetFormFieldValue, som löser föräldrafältets ordlista istället för widget-annoteringen

Felrapporten som ledde hit är den sort man misstror först. En kund plattar ur ett signerat samtyckesformulär, öppnar resultatet, och varje kryssruta är tom. Öppna källfilen i Acrobat och rutorna är synligt ikryssade. Läs källfilen tillbaka genom samma komponent och fältvärdena är korrekta. Bara den utplattade utdatan förlorar dem, och bara för kryssrutor och radioknappar: textfält på samma sida kommer ut fint

Varför är kryssrutor avmarkerade efter utplattning?

Därför att utplattning aldrig tittar på /V. FPDFPage_Flatten bakar in widgetens utseendeström i sidinnehållet, och det utseende den väljer är det som namnges av /AS. Om /AS fortfarande säger /Off medan fältvärdet säger att rutan är på, bakar utplattningen troget in av-utseendet. Värdet gick aldrig förlorat; det konsulterades aldrig

ISO 32000-1 §12.5.5 definierar utseendeordlistan /AP med tre möjliga poster, /N, /R och /D. För en kryssruta eller radioknapp är /N-posten inte en ström utan en underordlista vars nycklar är utseendetillståndsnamn, och §12.5.2 gör /AS till den obligatoriska väljaren när /N är en underordlista. Så en kryssruta bär två förbyggda utseenden och en pekare. Få pekaren fel och renderingen är fel på ett sätt som ingen mängd korrekt /V kan reparera. Det är också varför felläget skiljer sig från textfält, som inte har något förbyggt utseende att välja alls: ett textfält-/N är en enda ström som måste regenereras från grunden efter att värdet ändrats, så GenerateFormAppearances hanterar de två fallen genom helt separata kodvägar och bara knappvägen var trasig

Var bor kryssrutans värde egentligen?

På fältordlistan, inte på widgeten. ISO 32000-1 §12.7.5.2 beskriver kryssrutor och radioknappar som knappfält vars /V är ett namnobjekt som namnger det aktuella utseendetillståndet, och §12.7.3.1 placerar /V bland posterna gemensamma för alla fältordlistor. Widget-annoteringen definierad i §12.5.6.19 bidrar med /AS och /AP. Ingenting i specifikationen ålägger en widget att bära /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 är inte defekt. Dess kontrakt är exakt vad namnet säger: hämta en strängpost från annoteringsordlistan du gav den. Att fråga den om /V på objekt 13 returnerar ingenting eftersom objekt 13 genuint inte har något /V. Defekten låg hos anroparen, som antog en platt objektmodell som ISO 32000-1 aldrig utlovade

När delar fält och widget en ordlista?

Närhelst ett fält har exakt en widget. §12.5.6.19 tillåter att fältordlistan och dess enda widget-annotering slås samman till ett objekt, och de flesta författarverktyg tar den genvägen. I ett sammanslaget objekt sitter /FT, /T, /V, /AS och /AP alla sida vid sida, så en widget-nivå-läsning av /V lyckas och hela buggen förblir osynlig

I samma stund ett fält äger två eller fler widgetar är sammanslagningen omöjlig, och §12.7.3.1 kräver att widgetarna blir /Kids till en separat fältordlista. Varje radiogrupp är i den här formen per konstruktion. Det är även samtyckeskryssrutor upprepade i ett sidhuvud och en sidfot, och vilket fält som helst ett författarverktyg har kopierat till en andra sida. Det är hela förklaringen till varför defekten överlevde en regressionssvit: testkorpuset var fullt av enwidget-formulär och kundfilerna var det inte. Om du går igenom widgetar själv snarare än att förlita dig på komponenten, dyker samma asymmetri upp i uppräkningsordning, och anteckningarna om PDF-formulärfältnavigering med PDFium Component täcker hur en sidnivå-annoteringsgenomgång relaterar till dokumentnivå-fältträdet

Att läsa värdet som PDFium avser

FPDFAnnot_GetFormFieldValue är det korrekta API:et, och det hade varit bundet i komponenten en tid utan att kryssrutevägen använde det. Det tar formulärhandtaget såväl som annoteringen, vilket är signalen som spelar roll: med formulärifyllningsmiljön tillgänglig löser PDFium annoteringen till dess formulärkontroll och läser värdet från fältobjektet, så det returnerar rätt svar för både sammanslagna och uppdelade layouter

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;

Två detaljer i det utdraget är lätta att göra fel. Den returnerade längden är ett byteantal för UTF-16-text inklusive avslutaren, så teckenantalet är buflen div 2 - 1 och ett värde på 2 betyder en tom sträng. Vakten buflen >= 4 betyder därför minst ett riktigt tecken, vilket är det som hindrar ett fält utan /V alls från att få sitt /AS överskrivet med ett tomt namn

Vad /AS och /AP /N egentligen kommer överens om

De kommer överens om ett namn, och namnet väljs av vem som än producerade filen. §12.7.5.2 kräver att av-tillståndet kallas /Off, och lämnar på-tillståndet helt åt producenten. /Yes är en konvention, inte en regel. Acrobat skriver /Yes, men gott om generatorer skriver /On, /1, /Choice1, eller ett lokaliserat ord, och en radiogrupp ger normalt varje kid ett distinkt på-tillståndsnamn så gruppen kan uttrycka vilken knapp som är vald. Det är precis varför att kopiera /V ordagrant in i /AS är den rätta operationen snarare än en hack: för en ikryssad kontroll rapporterar PDFium på-tillståndsnamnet som filen själv definierar, och för en oikryssad rapporterar den Off, så värdet du skriver in i /AS är garanterat att vara en nyckel som finns i den widgetens /AP /N-underordlista. Att hårdkoda /Yes skulle fungera på Acrobat-utdata och tyst haverera överallt annars

Operationsordning, och var det fortfarande behövs försiktighet

Sekvensen är fast och oförlåtande: aktivera formulärifyllning, tilldela värden, regenerera utseenden, platta, sedan spara. Hoppa över regenereringssteget och FPDFPage_Flatten hittar tomma eller föråldrade utseendeströmmar och bakar in dem utan klagomål, vilket är en tyst dataförlust snarare än ett felretur

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');

Två ärliga begränsningar kvarstår. För det första skriver synkroniseringen fältvärdet in i /AS för varje widget för det fältet, vilket är korrekt för kryssrutor men ungefärligt för radiogrupper vars kids var och en definierar sitt eget på-tillståndsnamn; ett kid vars /AP /N inte har någon post som matchar det skrivna /AS har inget utseende att välja under §12.5.5, så en ovald knapp kan plattas till ingenting istället för en tom cirkel. Att granska en radiogrupp med FPDFAnnot_GetFormControlIndex innan utplattning är värt de få raderna. För det andra gäller inget av detta för XFA, där värdet bor i ett XML-datapaket snarare än AcroForm-ordlistorna, en separation täckt i anteckningarna om XFA-fältredigeringar som inte sparas. Den allmänna lärdomen är värd att behålla bortom den här enskilda fixen: närhelst ett API tar formulärhandtaget utöver annoteringen, talar det om för dig att det kommer att lösa fälthierarkin åt dig, och närhelst det bara tar annoteringen kommer det att läsa exakt det objekt du gav det. Den distinktionen styr också datautbyte, eftersom export och import av XFDF-formulärdata arbetar med fullt kvalificerade fältnamn, aldrig med widgetpositioner

Formulärutplattning är en av de funktioner som ser ut som ett enda API-anrop och visar sig vara ett kontrakt mellan tre ordlistor. Om du hellre vill arbeta mot en komponent som redan kodar det kontraktet, levererar PDFium Component för Delphi och C++Builder utseenderegenerering, utplattning och formulärfältåtkomst beskriven här som vanliga egenskaper och metoder