Technisch artikel

Atomisch batchgewijs PDF-formulieren vullen in Delphi

PDFium Component for Delphi vult een AcroForm atomair met TPdf.SetFormFieldValues: je geeft 1 tot 256 getypeerde aanvragen mee, geadresseerd op pagina en widget, en de component past ofwel elke waarde toe in één documentvervanging, ofwel geen enkele. Als één aanvraag ongeldig is, benoemt TPdfFormValuesWriteReport.FailedRequest de aanvraag die de batch liet zinken

Die garantie telt het zwaarst in de klussen die niemand in de gaten houdt. Een nachtelijke import pompt CRM-rijen in een claimformulier, of een printservice legt polisgegevens over een voorgedrukte template heen. Voer die per veld uit en de eerste slechte waarde — een code die geen geldige radio-export is, een string langer dan de MaxLen van het veld — laat je achter met een PDF die voor de helft oude en voor de helft nieuwe data bevat. Er is niets gecrasht, dus niemand wordt gewaarschuwd. De batch-API bestaat zodat een mislukking een nette weigering is in plaats van een half weggeschreven document

Waarom is per-veld-formulieren vullen gevaarlijk voor data-import?

Per veld vullen is gevaarlijk omdat elke schrijfactie voor zichzelf commit, dus een fout midden in de lus laat een document achter dat matcht met het bronrecord noch met de originele template. Het klassieke patroon is een lus over FormField[Index]-toewijzingen, die elk door de native form-fill-omgeving van PDFium gaan, één widget per keer. Waarde 1 tot en met 17 landen, waarde 18 gooit een exception, en de aanroeper moet nu precies weten welke waarden erin zijn gegaan om ze ongedaan te maken

De meeste aanroepers schrijven die ongedaan-maakcode nooit, en wie het wel doet, krijgt het doorgaans subtiel fout: een gedeeld veld is al gewijzigd op elke pagina waar het opduikt en checkbox-statussen zijn al omgeklapt. Draait de opslag in een finally-blok, dan gaat het half gevulde bestand naar de schijf terwijl het compleet lijkt. In een overdrukklus is dat erger dan geen bestand, want het wordt geprint en iemand zet er een handtekening onder

Het andere stille probleem is focus. De klassieke setter bedient de interactieve editor van PDFium: hij verbreekt bestaande focus, zet focus op de doelwidget, selecteert alle tekst, vervangt haar en verbreekt de focus opnieuw. Prima voor een headless klus, maar in een viewer beëindigt hij ook de editsessie die de gebruiker nog open had in een ander veld. Het getypeerde schrijfpad dat in PDFium Component 3.231.0 kwam, loopt helemaal niet door native veldfocus, en 3.233.0 breidde het uit van losse velden naar cross-page batches

Hoe SetFormFieldValues een batch in één vervanging toepast

TPdf.SetFormFieldValues zet elke aanvraag klaar tegen één vastgelegde snapshot van het document, schrijft alle veld-dictionaries in één keer weg en wisselt het afgeronde resultaat in met één vervanging. Het originele document blijft levend tot de allerlaatste stap, dus elke fout vóór die stap laat het onaangetast

De vijfstappen-atomaire batch-formfill-volgorde in PDFium Component voor Delphi, van preflight tot verifiëren en wisselen met alles-of-niets-uitkomsten
elke aanvraag wordt klaargezet tegen één snapshot en het levende document wordt pas aangeraakt bij de definitieve geverifieerde wissel

De volgorde verklaart zowel de garantie als de prijs:

  1. Preflight-controles draaien eerst: het document moet actief zijn, het aantal aanvragen moet 1..256 zijn, er mag geen page-object- of edit-journal-transactie open staan, geen native veld mag focus hebben en het document moet fill-forms- of modify-permissie verlenen
  2. Elke aanvraag wordt gevalideerd op paginabereik en invoerbudget, en FailedRequest volgt de index die wordt gecontroleerd
  3. De component legt een edit-snapshot vast, normaliseert de AcroForm in die kopie en past elke aanvraag op volgorde toe, waarbij /V, /I voor keuzeselecties en /AS voor elke widget die het veld deelt, wordt weggeschreven
  4. De wijzigingen worden als één sparse incrementele update in de zin van ISO 32000-1 §7.5.6 aan de snapshot toegevoegd, dus ongerelateerde objecten worden byte voor byte overgezet
  5. Een aparte verifierinstantie laadt eerst de kandidaatbytes; pas nadat die netjes opent, ontlaadt de component het levende document en laadt het nieuwe, met terugval op de originele snapshot als die load faalt

De interne vervangstap is dezelfde machinerie die de component voor zijn edit-journal-snapshots gebruikt, en daarom kunnen het undo- en crashherstel-journal en een batchschrijfactie niet overlappen: rond de journaltransactie eerst af of annuleer haar. Automatische lifecycle-acties worden onderdrukt zolang het document wordt gewisseld, dus een document-open-script kan geen velden herschrijven of focus grijpen midden in de vervanging

Een getypeerde aanvraagbatch bouwen in Delphi

Een batch is een dynamische array van TPdfFormValueRequest-records, elk met een 1-based Page, een 0-based paginalokaal FieldIndex en een getypeerde TPdfFormValue. Het waarderecord draagt een Kind van fvkText, fvkChoice, fvkCheckBox of fvkRadioButton, plus de members Text, ChoiceIndices en Checked die volgens dat soort worden gebruikt

Importdata komen meestal binnen met veldnamen als sleutel, dus de eerste klus is namen vertalen naar pagina- en widgetadressen. FieldIndex telt widgetannotaties op een pagina in dezelfde volgorde als FormFieldCount en FormFieldInfo, dus een simpele scan werkt. De helper neemt de eerste widget die hij voor een naam vindt, wat telt voor gedeelde velden, zoals de volgende sectie uitlegt

type
  TFieldAddress = record
    Page, FieldIndex: Integer;
  end;

function FindFieldAddress(Pdf: TPdf; const FieldName: WString;
  out Address: TFieldAddress): Boolean;
var
  P, I: Integer;
begin
  Result := False;
  for P := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := P;
    for I := 0 to Pdf.FormFieldCount - 1 do
      if Pdf.FormFieldInfo[I].Name = FieldName then
      begin
        Address.Page := P;
        Address.FieldIndex := I;
        Exit(True);      // alleen de eerste widget: één aanvraag per veld
      end;
  end;
end;

procedure AddRequest(var Requests: TPdfFormValueRequests;
  const FieldName: WString; Pdf: TPdf; const Value: TPdfFormValue);
var
  Address: TFieldAddress;
  N: Integer;
begin
  if not FindFieldAddress(Pdf, FieldName, Address) then
    raise Exception.CreateFmt('Template has no field "%s"', [FieldName]);
  N := Length(Requests);
  SetLength(Requests, N + 1);
  Requests[N].Page := Address.Page;
  Requests[N].FieldIndex := Address.FieldIndex;
  Requests[N].Value := Value;
end;

Merk op dat AddRequest een exception gooit bij een onbekende naam in plaats van haar over te slaan. De batch valideert adressen, niet jouw mapping, dus een ontbrekend templateveld moet hier falen of het faalt nooit. Met de adressen opgelost is het bouwen van de waarden en het committeren kort

var
  Pdf: TPdf;
  Requests: TPdfFormValueRequests;
  Report: TPdfFormValuesWriteReport;
  V: TPdfFormValue;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'claim-template.pdf';
    Pdf.Active := True;

    V := Default(TPdfFormValue);
    V.Kind := fvkText;
    V.Text := 'Maria Lopez';
    AddRequest(Requests, 'Claimant.Name', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkCheckBox;
    V.Checked := True;
    AddRequest(Requests, 'Consent', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkRadioButton;
    V.Text := 'Express';                // exportwaarde van de te selecteren knop
    AddRequest(Requests, 'Shipping', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkChoice;
    SetLength(V.ChoiceIndices, 1);
    V.ChoiceIndices[0] := 2;            // 0-based index in de Opt-array van het veld
    AddRequest(Requests, 'Region', Pdf, V);

    if Pdf.SetFormFieldValues(Requests, Report) then
      Pdf.SaveAs('claim-filled.pdf')
    else
      LogBatchFailure(Requests, Report);
  finally
    Pdf.Free;
  end;
end;

De getypeerde soorten worden tegen het veldtype getoetst voordat er iets wordt weggeschreven. Een tekstwaarde gericht op een checkbox, een radio-waarde gericht op een gewone checkbox of een schrijfactie naar een push-knop worden allemaal geweigerd. Keuze-indexen moeten geldig, uniek en oplopend zijn; meer dan één index vraagt om de MultiSelect-vlag en vrije tekst in een keuzeveld wordt alleen geaccepteerd door een bewerkbare combo box. Een tekstwaarde langer dan de MaxLen van het veld wordt geweigerd in plaats van afgekapt en read-only-velden worden ronduit geweigerd. Bij radiogroepen met de NoToggleToOff-vlag wordt ook een lege exportwaarde, bedoeld als "maak de groep leeg", geweigerd

Waarom laat het dubbel wegschrijven van een gedeeld veld de hele batch falen?

TPdf.SetFormFieldValues weigert twee aanvragen die naar hetzelfde terminale veld resolven, omdat ze allebei een enkele waarde zouden wegschrijven, en de component staat niet toe dat de ene aanvraag stilletjes de andere overschrijft. De controle draait op de veldeigenaar, niet op de widget, dus twee verschillende widgets van één veld op twee verschillende pagina's tellen nog steeds als duplicaat

De reden is het veldmodel uit ISO 32000-1 §12.7.3.1. Een terminaal veld bezit de waarde; zijn widgetannotaties zijn slechts plekken waar die waarde wordt getoond. Een veld genaamd PolicyNumber dat in de kop van elke pagina staat, is één veld met meerdere widgets, en elke widget daarvan toont dezelfde /V. Een radiogroep werkt hetzelfde: elke knop is een widget, de groep is het veld. PolicyNumber wegschrijven via de widget op pagina 1 en opnieuw via de widget op pagina 9 is één veld dat tweemaal wordt geschreven, en elke regel die een winnaar aanwijst verbergt een bug in je mapping

De batch behandelt het dus als fout en benoemt de tweede aanvraag in FailedRequest. In de praktijk zit de fix stroomopwaarts: los elke veldnaam op naar precies één adres, zoals FindFieldAddress doet, en laat de component de waarde naar de andere widgets doorgeven. Dat doet hij als onderdeel van de schrijfactie, waarbij /AS op elke widget van een gedeelde checkbox of radiogroep wordt gezet zodat de appearance-status met de veldwaarde klopt — dezelfde splitsing die wordt behandeld in het artikel over checkbox-veldwaarden versus widgetstatussen

Eén terminaal veld bezit de waarde, widgets op twee pagina's tonen haar en een dubbele aanvraag laat de batch falen in PDFium Component voor Delphi
één aanvraag per terminaal veld — de component schrijft /V één keer en zet /AS op elke widget die hem deelt

Het faalrapport lezen

Een mislukte batch geeft False terug, gooit geen exception en laat het originele document en elke veldwaarde precies zoals ze waren. TPdfFormValuesWriteReport meldt dan Succeeded = False, nul voor zowel UpdatedFields als UpdatedWidgets, de 0-based index van de overtredende aanvraag in FailedRequest en een voor mensen leesbare reden in ErrorMessage

procedure LogBatchFailure(const Requests: TPdfFormValueRequests;
  const Report: TPdfFormValuesWriteReport);
var
  R: Integer;
begin
  R := Report.FailedRequest;
  if R >= 0 then
    Log(Format('Request %d (page %d, widget %d) rejected: %s',
      [R, Requests[R].Page, Requests[R].FieldIndex, Report.ErrorMessage]))
  else
    Log('Batch rejected as a whole: ' + Report.ErrorMessage);
end;

Een FailedRequest van -1 betekent dat de weigering bij de transactie hoort in plaats van bij een aanvraag: native focus was actief, het document is ondertekend, het formulier is XFA of een permissiecontrole faalde. Een niet-negatieve waarde wijst naar één entry. Bij een overschrijding van het totale budget wijst hij naar de aanvraag die de batch over de grens duwde, en dat is niet per se de aanvraag die fout is. Houd een parallelle array met bronkolomnamen naast Requests bij en je kunt index 14 rechtstreeks omzetten naar "rij 2087, kolom ShipDate" in het importlog

Bij succes is UpdatedFields gelijk aan het aantal aanvragen en telt UpdatedWidgets elke aangeraakte widget, gedeelde incluis, een goedkope gezondheidscontrole tegen de template

Voorwaarden en grenzen waar je tegenaan loopt

De meest voorkomende weigering in een viewerapplicatie is native focus: typt een gebruiker in een veld, dan weigert SetFormFieldValues nog voordat hij de snapshot vastlegt, en de wachtende editorbuffer overleeft het intact. De component laat de focus niet stilletjes vallen, want dat zou tekst die de gebruiker nog niet afrondde committeren of verliezen. Roep bewust ClearFormFieldFocus aan, wat de editsessie beëindigt zoals een klik buiten het veld dat zou doen, en probeer het daarna opnieuw

De budgetten staan vast en zijn gedocumenteerd, dus je kunt imports ervoor afmeten:

  • 1 tot 256 aanvragen per batch; splits grotere records over meerdere batches en accepteer dat atomiciteit dan per batch geldt
  • Hooguit 262.144 UTF-16-units tekst en 4.096 keuze-indexen per aanvraag
  • Hooguit 1.048.576 UTF-16-units tekst en 16.384 keuze-indexen over de hele batch
  • De gebruikelijke documentbudgetten voor de snapshotparser gelden nog steeds, waaronder een bron van 64 MiB en 65.536 objecten

Een geslaagde vervanging maakt ook native handles, zoekopdrachten en selecties die vóór de aanroep zijn verkregen ongeldig, want het document eronder is nieuw. Pagina- en viewactivatie en logische veldcursoren blijven behouden; al het andere dat je cachte, zoals annotatie- of page-object-handles, moet opnieuw worden opgehaald

Hoe verschilt dit van XFDF-import?

TPdf.ImportXFDF en TPdf.SetFormFieldValues stoppen allebei data in velden, maar XFDF-import is een toegeeflijke merge terwijl de batch een strenge transactie is. XFDF-import matcht waarden op naam naar velden, slaat namen over die hij niet vindt, negeert duplicaten en geeft terug hoeveel entries hij toepaste. Dat is precies goed voor het mergen van de opmerkingen van een reviewer, waar een gedeeltelijk resultaat nog steeds nuttig is

De batch doet op elk punt het omgekeerde. Ze adresseert widgets expliciet, weigert onbekende of verkeerd getypeerde targets, behandelt duplicaten als fouten en kent geen gedeeltelijke uitkomst. Gebruik XFDF wanneer de data een interchange-bestand uit een andere tool is; gebruik SetFormFieldValues wanneer je eigen code de mapping bezit en een half toegepast record een defect zou zijn

Beslissingsmatrix die XFDF-import afzet tegen de atomaire SetFormFieldValues-batch in PDFium Component voor Delphi: targeting, onbekende en dubbele targets, uitkomst en beste inzet
XFDF merged interchange-bestanden toegeeflijk terwijl de batch onbekende namen, duplicaten en gedeeltelijke uitkomsten weigert

Wat de batchschrijfactie niet dekt

De batch is een begrensd AcroForm-profiel en zegt buiten dat profiel nee in plaats van te gokken. XFA-formulieren worden geweigerd en vragen om hun eigen editor. Ondertekende documenten worden geweigerd, want veld-dictionaries herschrijven onder een bestaande handtekening is een handtekeningworkflow, geen vuloperatie; versleutelde snapshots vallen ook buiten het profiel. Misvormde of dubbelzinnige knop-exportarrays worden geweigerd en radio-exportwaarden die bij meer dan één knop horen worden geweigerd tenzij de groep RadiosInUnison aanzet. De transactie normaliseert ook directe veld-dictionaries vóór het schrijven, het geval achter veldwaarden die lijken terug te vallen

Appearances verdienen één zin zorg. Bij gewijzigde tekst- en keuzewidgets laat de batch de verouderde /AP vallen in plaats van een appearance mee te sturen die de oude waarde toont, en hij zet NeedAppearances uit zodat PDFium niet elke ongerelateerde widget in het formulier opnieuw opbouwt. PDFium genereert de ontbrekende appearances opnieuw bij het renderen. Gaat de gevulde PDF naar een strenge printpipeline die nooit appearances synthetiseert, render of flatten hem dan eerst via PDFium en test dat pad met je eigen templates

SetFormFieldValues en de getypeerde waarderecords worden meegeleverd met de PDFium Component for Delphi and Lazarus, waar de productpagina de volledige form-filling- en AcroForm-API opsomt naast rendering-, bewerkings- en viewercontrols