Technisch artikel

Ontbrekende widget-appearances en veldovererving in HotPDF

Een PDF-formulier met veldwaarden maar zonder appearance-streams kan uit een Linux-renderjob komen als lege vlakken, met het verkeerde lettertype of de verkeerde uitlijning, of met een EHPDFHeadlessRenderError met de tekst "Widget field requires a name field type". De headless renderer van HotPDF tekent tijdelijke appearances voor tekst-, knop- en keuzewidgets en erft /FT, /V, /DA en /Q langs de veldboom over

Waar een widget zijn veldinstellingen bewaart

Het grootste deel van de ellende begint bij de splitsing tussen een veld en zijn widgets. De velddictionary bewaart het logische deel van een formulierbesturing: zijn type, waarde, vlaggen en standaardappearance. De widgetannotatie bewaart wat een pagina nodig heeft: /Rect, rand, een verwijzing naar zijn pagina. Een veld met één widget wordt meestal samengevoegd tot één dictionary. Moet dezelfde waarde op meerdere pagina's verschijnen, dan worden de widgets kinderen van het veld en dragen ze vaak niets dan geometrie en een /Parent-verwijzing. ISO 32000-1 §12.7.3.1 maakt /FT, /Ff, /V en /DV overerfbaar langs die keten (Tabel 220), en §12.7.3.3 voegt nog maar twee variabele-tekstattributen toe aan de overerfbare set in Tabel 222: /DA, dat verplicht is maar van een voorouder mag komen, en /Q. De interactieve formulierdictionary (Tabel 218) levert documentbrede standaarden voor /DA en /Q, plus /DR, de standaardresourcedictionary waarin /DA-lettertypenamen worden opgezocht. /DR staat niet in Tabel 222; hij hoort bij het formulier als geheel. Een renderer die alleen de widgetdictionary leest vindt in het bestand hieronder geen type, waarde of /DA en valt terug op zijn eigen standaardlettertype

1 0 obj  % catalogus
<< /Type /Catalog /Pages 2 0 R
   /AcroForm << /Fields [10 0 R] /DA (/Helv 0 Tf 0 g) /Q 0
                /DR << /Font << /Helv 20 0 R /Cour 21 0 R >> >> >> >>
endobj
10 0 obj  % terminaal veld: hier huizen type, waarde en appearance-standaarden
<< /FT /Tx /T (invoice_no) /V (INV-2026-0417)
   /DA (/Cour 10 Tf 0 0 0.6 rg) /Q 2
   /Kids [11 0 R 12 0 R] >>
endobj
11 0 obj  % widget op pagina 1: alleen geometrie, hoeken eerst rechtsboven opgegeven
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj  % widget op pagina 2: alleen geometrie
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj

HotPDF zoekt elke veldentry op de widget en, als die er niet is, klimt hij /Parent één dictionary per keer op; de dichtstbijzijnde waarde wint, en alleen /DA en /Q vallen terug op de AcroForm-dictionary zodra de keten oprakt. Verwacht voor het bestand hierboven: beide widgets tonen INV-2026-0417 rechtsuitgelijnd in Courier 10 punten, donkerblauw, en de documentbrede /Helv 0 Tf komt nooit in beeld want object 10 heeft een eigen /DA. Komt een veld in plaats daarvan in zwart Helvetica 12 punten naar buiten, dan is de voor de hand liggende verdachte een Courier-lettertype dat op de Linux-machine ontbreekt. De grootte verraadt het. Dat is wat HotPDF tekent wanneer er geen /DA opduikt op de widget, op een voorouder of in de AcroForm-dictionary. Dump dus de widget, volg zijn /Parent met de hand en vind waar de producent de /DA neerzette, bijvoorbeeld op een velddictionary waar de widget nooit naar terugwijst

AcroForm-objectgrafiek voor headless rendering met HotPDF in Delphi: widgets met alleen geometrie klimmen langs /Parent naar het veld dat /FT, /V, /DA en /Q bewaart
de dichtstbijzijnde waarde langs de /Parent-keten wint, en alleen /DA en /Q vallen terug op de AcroForm-dictionary zodra de keten oprakt

Lettertypenamen uit /DA resolven tegen een /DR op de widget of een ouderend veld, de AcroForm-/DR en de lettertyperesources van de pagina, in die volgorde. Een /DR op veldniveau valt buiten Tabel 222, maar hij is eenduidig zodra hij er staat, dus HotPDF accepteert hem. De klim is begrensd: een lus in de boom stopt met "Widget field inheritance contains a cycle", een /Parent die geen dictionary is met "Widget Parent requires a dictionary", en een keten dieper dan MaxDepth in de renderopties (standaard 32) met "Widget field inheritance depth budget exceeded". De VCL-component past dezelfde overerving toe wanneer hij overerfde AcroForm-waarden leest en velden reset

Hoe beslist de headless renderer wat hij tekent?

HPDFRenderHeadlessPagePNG en zijn bestandsvariant draaien de fallback alleen voor een zichtbare widget waarvan /AP ontbreekt of null is; ISO 32000-1 §7.3.7 zegt dat een null-waarde in een dictionary hetzelfde is als een ontbrekende entry, en de renderer behandelt /AP null zo. Een aanwezige normale appearance wint altijd, wat /NeedAppearances ook zegt, dus een verouderde appearance van een eerdere invulling blijft zichtbaar tot iets hem opnieuw genereert. Het geladen document wordt nooit gewijzigd. Wat er getekend wordt hangt af van het geresolveerde veldtype en de vlaggen:

VeldGesynthetiseerde appearance
Eénregelige tekst, /FT /TxDe string /V, gezet met de geresolveerde /DA en /Q binnen /Rect
Checkbox, /FT /BtnRechthoekige rand met hoekradii uit /Border, vulling uit /MK /BG, en een vectorvinkje als contourlijn wanneer aan
Radioknop, Btn met bit 16Ronde rand met een gevulde stip wanneer aan
Drukknop, Btn met bit 17Rechthoekige rand met het /MK /CA-bijschrift gecentreerd
Combobox, /FT /Ch met bit 18De weergavetekst van de gekozen /Opt-entry als gecentreerd bijschrift
Keuzelijst, Ch met bit 18 gewistEén rij per optie vanaf /TI, met een markeerbalk achter elke geselecteerde rij

Het vinkje en de radiostip zijn vectorpaden in plaats van ZapfDingbats-glyphs, zoals veel generatoren ze tekenen, want een slanke Linux-container heeft mogelijk geen ZapfDingbats geïnstalleerd. De gezichten van drukknop en combobox matchen wat de VCL-appearancegenerator van HotPDF voor hetzelfde veld tekent, dus het formulier oogt gelijk welke van de twee de afbeelding ook produceerde. Een bijschrift haalt zijn lettertype, grootte en kleur uit /DA; omdat de bijschriftlayout niet automatisch meeschaalt, wordt een /DA-grootte van 0 tot 70 procent van de hoogte van het vlak, met een plafond van 12 punten. De status van checkbox en radio komt uit de eigen /AS van de widget, met terugval op de overerfde /V, en elke status anders dan Off tekent het merkje. Geef elke radiokind zijn eigen /AS. De /V van de ouder benoemt de gekozen status voor de hele groep en kan het ene kind niet van het andere onderscheiden

Beslisstroom in de headless renderer van HotPDF voor Delphi: een aanwezige /AP wint altijd, anders kiest het geresolveerde veldtype een gesynthetiseerd gezicht
tijdelijke appearances worden alleen getekend wanneer /AP ontbreekt of null is, en het geladen document zelf wordt nooit gewijzigd
uses
  SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;

function RenderFormPage(const FileName, PngName: string; PageIndex: Integer;
  out Diagnostic: string): Boolean;
var
  Doc: THPDFHeadlessDocument;
  Options: THPDFHeadlessRenderOptions;
  Info: THPDFHeadlessRenderResult;
begin
  Result := False;
  Diagnostic := '';
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(FileName);
    Options := THPDFHeadlessRenderOptions.Default;  // RenderAnnotations = True, MaxDepth = 32
    Options.Usage := hruPrint;  // zoals het afdrukken van een viewer: widgets zonder de Print-vlag worden overgeslagen
    try
      // PageIndex is nulgebaseerd
      Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
        PngName, Options);
      Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
      Result := True;
    except
      on E: EHPDFHeadlessRenderError do
        // bijv. 'Widget field requires a name field type'; schakel bij een ingevuld formulier niet
        // opnieuw met RenderAnnotations := False, want dat gooit elke widget en zijn waarde weg
        Diagnostic := E.Message;
    end;
  finally
    Doc.Free;
  end;
end;

De tekstveld-fallback tekent éénregelige velden die een stringwaarde bevatten. Voor rijkere tekstlayout genereert u de appearance bij het invullen op de Windows-kant, waar EnsureLoadedFieldAppearanceStream de ontbrekende /AP-stream bouwt voordat het bestand wordt afgevlakt of verstuurd; de Linux-job ontvangt dan echte appearance-streams, en de headless renderer benoemt elk veld dat hij tegenkomt en er nog een nodig heeft. Beoordeel Linux-uitvoer echter op de Linux-renderer. RenderLoadedPageToBitmap tekent via het Windows GDI-backend, dat deze fallback nooit draait, dus een pagina die er daar goed uitziet vertelt u niets over wat het Cairo-apparaat uit hetzelfde bestand zal maken

Veldvlagbits worden vanaf één genummerd

ISO 32000-1 §12.7.3.1 stelt dat bitposities in het /Ff-woord lopen van 1 aan het laagwaardige uiteinde tot 32, en Tabel 226, 228 en 230 gebruiken allemaal die nummering. In Delphi is bit N dus 1 shl (N - 1). Schuift u met het bitnummer zelf, dan schuift elke test één positie op: 1 shl 16 geschreven voor "radio, bit 16" toetst in werkelijkheid de drukknopvlag, en 1 shl 18 geschreven voor "combo, bit 18" toetst Edit. Beide verkeerde maskers zijn echte vlaggen. De code compileert, draait, en sorteert velden in de verkeerde soort

PDF-veldvlaggen nummeren bits vanaf één in Delphi: de liniaal toont waarom radio 1 shl 15 is en 1 shl 16 bij de drukknop uitkomt
bit N van het /Ff-woord neemt het Delphi-masker 1 shl N minus één, en elk masker dat er één naast zit is een echte vlag van de naburige bit
VlagTabelBitDelphi-masker
Multiline228 (tekst)131 shl 12 = 4096
Password228 (tekst)141 shl 13 = 8192
Comb228 (tekst)251 shl 24 = 16777216
RichText228 (tekst)261 shl 25 = 33554432
Radio226 (knop)161 shl 15 = 32768
Pushbutton226 (knop)171 shl 16 = 65536
Combo230 (keuze)181 shl 17 = 131072
uses
  Classes, SysUtils, HPDFHeadlessDocument;

const
  // ISO 32000-1 nummert Ff-bits vanaf 1 (laagwaardig), dus bit N is 1 shl (N - 1)
  TxMultiline   = 1 shl 12;  // bit 13
  TxPassword    = 1 shl 13;  // bit 14
  TxFileSelect  = 1 shl 20;  // bit 21
  TxDoNotScroll = 1 shl 23;  // bit 24
  TxComb        = 1 shl 24;  // bit 25
  TxRichText    = 1 shl 25;  // bit 26
  TxProducerLayout = TxMultiline or TxPassword or TxFileSelect or
    TxDoNotScroll or TxComb or TxRichText;

procedure ListTextFieldsForProducerAppearance(const FileName: string;
  Report: TStrings);
var
  Doc: THPDFHeadlessDocument;
  Field: THPDFHeadlessFormField;
  I: Integer;
begin
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(FileName);
    for I := 0 to Doc.FormFieldCount - 1 do
    begin
      // FieldType, Flags en MaxLength komen al overerfd vanuit voorouders binnen
      Field := Doc.GetFormField(I);
      if Field.FieldType <> 'Tx' then
        Continue;
      if (Field.Flags and TxProducerLayout) <> 0 then
        Report.Add(Format('%s: flags $%.8x, needs a producer /AP',
          [Field.Name, Field.Flags]))
      else if Field.MaxLength > 0 then
        Report.Add(Format('%s: /MaxLen %d, needs a producer /AP',
          [Field.Name, Field.MaxLength]))
      else if Field.Value = '' then
        Report.Add(Format('%s: blank, fill it or give it an /AP', [Field.Name]));
    end;
  finally
    Doc.Free;
  end;
end;

De scan leest vlaggen op veldniveau, want daar zetten producenten ze neer; de renderer neemt ook een /Ff op een aparte widget in acht, dus behandel de lijst als planningshulp en de renderdiagnose als het laatste woord. De leegtecontrole doet ertoe omdat een tekstveld zonder /V en zonder /AP niets heeft te synthetiseren: de render stopt met "A text-field widget without a value has no appearance to synthesize", en het middel is een waarde of een appearance voordat het bestand de renderer bereikt

Wat betekent "Degenerate appearance geometry"?

Het betekent dat de rechthoek of het appearance-vlak geen oppervlak heeft, en een omgekeerde /Rect is niet de oorzaak. ISO 32000-1 §7.9.5 laat een rechthoek elke twee diagonaal tegenover elkaar liggende hoeken benoemen, en zijn noot heet verwerkingssoftware zich voor te bereiden om ze te normaliseren. Een naïeve renderer die de breedte als x2 - x1 berekent krijgt uit [300 720 160 700] een negatief getal en verwerpt de annotatie of spiegelt zijn appearance. HotPDF wisselt de coördinaten vóór elke groottecontrole naar de volgorde linksonder en rechtsboven, voor gesynthetiseerde gezichten en bestaande /AP-streams in gelijke mate, dus widget 11 hierboven rendert alsof zijn hoeken conventioneel waren. De fout wordt alleen geworpen voor een annotatie met een appearance-stream wanneer, na normalisatie, de /Rect nul breedte of hoogte heeft, of de /BBox van de stream onder zijn /Matrix inkrimpt tot een lijn of een punt. Verschijnt hij, dump dan de /Rect van de widget samen met de /BBox en de /Matrix van de stream: een van de drie heeft een uitgestrektheid van nul

Een widget zonder /AP en met een /Rect zonder oppervlak neemt een andere route: de fallback slaat hem over en gaat met de pagina verder. ISO 32000-2 §12.5.2 vrijwaart alleen Popup-, Projection- en Link-annotaties en een rechthoek die inkrimpt tot één punt van de appearance-eis. Een rechthoek die slechts een lijn is blijft ook onder PDF 2.0 een appearance verschuldigd; de renderer slaat die ook over, puur omdat er geen oppervlak is om te verven

Wanneer het veldtype niet gevonden kan worden

"Widget field requires a name field type" betekent dat de renderer vanaf de widget is geklommen tot waar /Parent-verwijzingen ophouden en geen /FT vond. De renderer begint bij de /Annots-array van de pagina, dus de opwaartse /Parent-verwijzing is zijn enige route naar het veld. Het laden loopt de andere kant op, van de AcroForm-/Fields-array omlaag door /Kids, wat de reden is dat LoadFromFile kan slagen op een bestand waarvan de pagina later faalt te renderen. Een typische oorzaak is een widget die in de /Kids van een veld staat maar niet terugwijst. ISO 32000-1 Tabel 188 geeft daar enige dekking: die vereist /Parent op een widget alleen wanneer de widget een van meerdere kinderen is en zegt "absent otherwise", dus een writer die hem letterlijk leest laat de verwijzing vallen bij een veld met één aparte widget. ISO 32000-2 versoepelde dezelfde entry naar "optional otherwise". Wij zouden toch op elke widget die niet met zijn veld is samengevoegd /Parent schrijven. ISO 32000-2 staat het toe, en het is de enige manier waarop een paginagedreven lezer, de onze of die van een ander, het veld kan bereiken. Kunt u de producent niet aanpassen, dan geeft het samenvoegen van een solitaire widget in zijn velddictionary hetzelfde resultaat

Een widget die naar zijn ouder wijst en ergens op de weg omhoog een /DA vindt rendert correct zonder enige /AP in de headless renderer van de HotPDF Delphi component, wat de veldboom, meer nog dan de appearance-streams, het eerste maakt om goed te krijgen in een Linux-formulierpijplijn