Technisch artikel

AcroForm-velden toevoegen aan een geladen PDF in Delphi

Je hebt een factuursjabloon van een derde partij, of een gearchiveerd contract dat iemand jaren geleden heeft gegenereerd in software die niemand meer kan vinden, en de eis is om het interactief te maken: zet een handtekeningvak in de hoek, voeg een paar tekstvelden toe, maak van een vlakke checklist echte selectievakjes. De moeilijkheid is dat je deze PDF niet vanaf nul maakt. Hij bestaat al, hij heeft al pagina's en contentstreams en fonts die je niet beheert, en je moet AcroForm-widgets op dat objectgraph enten zonder hem opnieuw op te bouwen. Dat is een ander probleem dan een formulier op een nieuw document maken, en het deel waar mensen over struikelen blijft onzichtbaar totdat je het resultaat in een viewer opent en de velden die je net hebt geschreven nergens op de pagina verschijnen

HotPDF is een native VCL PDF-component voor Delphi en C++Builder, en vanaf v2.247.0 biedt het een speciale reeks methoden precies hiervoor: alle zes standaardveldtypen rechtstreeks opbouwen op een document dat met LoadFromFile is geladen. Dit artikel laat zien wat die methoden doen, welke ISO 32000-1-dictionary ze opbouwen, en welke ene vlag ervoor zorgt dat het hele proces zonder waarschuwing een leeg ogend bestand oplevert

Waarom veldcreatie op een geladen document een eigen codepad is

Als je een PDF vanaf nul bouwt, beheert HotPDF het hele objectmodel. Elke pagina is een beschrijfbare THPDFPage-wrapper, en een tekstveld toevoegen via AddTextField koppelt de nieuwe widget aan het annotatieobject van de pagina, het paginaobject en de veldcollectie van het formulier, en genereert daarna een appearance stream uit de fontresources van het document. De appearance stream is het zichtbare oppervlak van de widget, dus het vak, de rand en eventuele standaardtekst, gerenderd als PDF-tekenopdrachten die de viewer letterlijk afbeeldt

Een geladen document geeft je van dat alles geen van de onderliggende lagen. De pagina's zijn binnengekomen als ruwe dictionaries; er is geen beschrijfbare THPDFPage-wrapper om een widget aan te hangen, en nog belangrijker, er staat geen fontresource-pijplijn klaar om appearance streams te tekenen. Het geladen pad kiest daarom een andere route. Het schrijft de field dictionaries rechtstreeks op de geparseerde objectgraph en adresseert pagina's via een zero-based index in plaats van via een page object. De veldtypen en de vlagbits komen exact overeen met het vanaf-nul-pad, dus een Text field is in beide gevallen een Text field; wat verandert is de onderliggende plumbing en, cruciaal, hoe het oppervlak van de widget wordt getekend

De /NeedAppearances-vlag is hier niet optioneel

Dit is het ene feit dat bepaalt of je werk zichtbaar wordt. Omdat het geladen pad geen appearance streams genereert, arriveert een pas toegevoegde widget bij de viewer zonder /AP-entry: een veld zonder beschreven oppervlak. Veel viewers tekenen dan helemaal niets wanneer ze een widget moeten renderen die geen appearance heeft en geen instructie om er een te bouwen. Het veld staat wel in het bestand, is structureel geldig, adresseerbaar door een formulierinvultool en totaal onzichtbaar voor een mens

De uitweg staat in ISO 32000-1 §12.7.3: de AcroForm-dictionary draagt een /NeedAppearances-boolean, en wanneer die true is moet een conforme reader de ontbrekende appearance streams zelf opbouwen uit elke field's /DA-string en waarde. HotPDF zet dit voor je. De eerste keer dat je een veld aan een geladen document toevoegt, draait EnsureLoadedAcroForm: als de catalog geen /AcroForm heeft maakt het er een, als er geen /Fields-array is maakt het die aan, en het forceert /NeedAppearances true. Je roept het niet rechtstreeks aan, maar weten dat het bestaat verklaart het gedrag. Het verklaart ook een implementatiekanttekening die je gewoon moet noemen: een handvol minimale of niet-conforme viewers negeert /NeedAppearances en tekent nog steeds niets. Voor gangbare readers doet de vlag zijn werk, maar als je doelgroep een ongebruikelijke embedded renderer gebruikt, test daar dan eerst

De zes veldtypen toevoegen

Elke methode volgt dezelfde vorm. Je geeft de zero-based pagina-index, de vier hoeken van de widget-rectangel in PDF user-space-coördinaten, de veldnaam en de extra argumenten die dat type nodig heeft. De rechthoek is X1, Y1, X2, Y2 met de PDF-oorsprong linksonder op de pagina, dus grotere Y-waarden staan hoger; dat is de coördinatenconventie van het bestandsformaat, niet de schermconventie met de oorsprong linksboven, en daar de boel mee omdraaien is de tweede meest gemaakte fout na het vergeten van de vlag. Elke aanroep geeft de zero-based index van het nieuwe veld terug, of -1 als de pagina-index buiten bereik was of het page object niet kon worden opgelost

De derde en vierde stringargumenten van het tekstveld zijn de veldnaam en de eerste /V-waarde; het gehele getal is /MaxLen, en wordt alleen geschreven als het groter is dan nul. HotPDF geeft elk bewerkbaar veld een standaard appearance-string van /Helv 12 Tf 0 0 0 rg, wat de /NeedAppearances-bewuste viewer leest om te bepalen welk font en welke kleur voor de waarde worden gebruikt. Het selectievakje neemt een exportwaarde, de string die het formulier verzendt wanneer het vakje is aangevinkt, plus een boolean voor de beginstatus; intern schrijft het de bijbehorende naamobjecten /V, /AS en /DV zodat de aan-uitstatus al consistent is op het moment dat het bestand wordt geopend. Een lege exportwaarde valt terug op Yes, de gebruikelijke "aan"-naam van een selectievakje

Keuzevelden en de /Ff-bitflags

ComboBox en ListBox zijn allebei choice fields, veldtype /Ch in ISO 32000-1 §12.7.4. Het verschil tussen een dropdown en een scrollende lijst is één bit in de field-flags-integer /Ff: bit 18, de Combo-vlag, waarde $40000. HotPDF zet die bit voor AddLoadedComboBox en laat hem uit voor AddLoadedListBox; verder zijn de twee identiek, en beide nemen hun keuzes als een open array van strings die naar de /Opt-entry worden geschreven

Twee opmerkingen over de optielijst. HotPDF schrijft elke /Opt-entry als een gewone string, waarbij de exportwaarde en het getoonde label dezelfde tekst zijn. ISO 32000-1 §12.7.4.4 staat ook de tweedelige [export display]-vorm toe wanneer je wilt dat de verzonden waarde afwijkt van wat de gebruiker leest; de loaded-creatiemethoden gebruiken de eenvoudigere eendelige vorm, dus als je verschillende export- en displaywaarden nodig hebt, zet je die zelf in de resulterende dictionary. En de waarde die je als huidige selectie doorgeeft, moet een van de opties zijn die je hebt opgegeven, omdat de viewer die tegen de lijst afzet

De drukknop is het andere geval dat door een vlag wordt bepaald: veldtype /Btn met bit 17, de PushButton-vlag, waarde $10000. Die bit maakt het verschil tussen een klikbare knop en een selectievakje, dat ook een /Btn-veld is maar zonder die bit. De caption die je doorgeeft wordt in de appearance-characteristics-dictionary /MK geschreven als de normale caption /CA. Belangrijk om hier eerlijk te zijn over de scope: de knop wordt aangemaakt met label en rechthoek, maar de loaded-creatiemethode koppelt geen actie, dus op zichzelf is het een knop die er goed uitziet en niets doet wanneer erop wordt geklikt. Submit-, reset- of JavaScript-acties koppelen is een apart onderwerp; voor het authoring-gedeelte vanaf nul wordt de veld-plus-actie-workflow behandeld in AcroForm-velden en acties bouwen in Delphi, en dat is het juiste vergelijkingspunt voor wat het loaded pad bewust achterwege laat

De dictionary die elk veld deelt

Onder alle zes methoden zit één gedeelde builder die de widget-annotatie opbouwt en deze op twee plaatsen registreert. Die schrijft /Type /Annot en /Subtype /Widget, de /Rect-array uit je vier coördinaten, de annotatievlag /F 4 die de Print-bit zet zodat het veld ook op papier verschijnt, de veldnaam /T, het veldtype /FT, de vlaggen /Ff en een /P-terugverwijzing naar het paginaobject. Daarna voegt het het nieuwe veld toe aan de /Fields-array van de AcroForm en aan de /Annots-array van die pagina, waarbij indirecte referenties onderweg worden opgelost zodat de echte arrays worden uitgebreid in plaats van de widget te verweesd achter te laten

Die dubbele registratie is belangrijk, omdat een widget die maar in een van beide lijsten leeft op een subtiele manier stuk is. Een veld dat wel in /Fields staat maar ontbreekt in de /Annots van de pagina, is wel bekend bij het formulier maar wordt nooit getekend; de omgekeerde situatie wordt wel getekend maar is onbekend voor de form-logica. HotPDF houdt beide bij elke toevoeging synchroon, wat precies het soort administratie is dat je anders handmatig exact goed moet krijgen volgens de specificatie

Een paar eerlijke beperkingen

Zet verwachtingen recht voordat je hier een workflow op bouwt. Het flatten-and-regenerate-gedrag hangt ervan af dat de viewer /NeedAppearances respecteert, wat Acrobat, moderne browser-PDF-engines en de gangbare desktopreaders dekt, maar geen harde garantie is voor elke renderer in het wild. Als je een bestand moet produceren waarvan de velden overal identiek renderen, ook in viewers die de vlag negeren, dan zit je in appearance-stream-territorium en is het vanaf-nul-authoringpad dat /AP voor je tekent de betere keuze. Het handtekeningveld wordt net zo goed aangemaakt als een leeg signature widget dat klaar is om te worden ondertekend; het veld plaatsen is niet hetzelfde als een cryptografische handtekening toepassen

Voor het wijzigen van wat al bestaat in plaats van er iets aan toe te voegen, is de verwante operatie form flattening, waarbij je interactieve velden terugbakent in statische page content zodat de waarden permanent en niet meer bewerkbaar worden; die ronde, inclusief hoe XFA-gebaseerde formulieren worden behandeld, wordt besproken in XFA- en AcroForm-velden flattenen in Delphi. Velden toevoegen en velden flattenen zijn twee uiteinden van dezelfde lifecycle: dit artikel laat zien hoe je interactie op een document zet dat die niet had, en flattening is hoe je die er weer afhaalt zodra het formulier zijn werk heeft gedaan

De loaded-document form-API die hier is getoond, wordt meegeleverd als onderdeel van de standaard HotPDF Component voor Delphi en C++Builder, samen met de volledige referentie voor field flags, appearance-handling en de rest van het AcroForm-model

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;
// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');