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 volgorde verklaart zowel de garantie als de prijs:
- 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
- Elke aanvraag wordt gevalideerd op paginabereik en invoerbudget, en
FailedRequestvolgt de index die wordt gecontroleerd - De component legt een edit-snapshot vast, normaliseert de AcroForm in die kopie en past elke aanvraag op volgorde toe, waarbij
/V,/Ivoor keuzeselecties en/ASvoor elke widget die het veld deelt, wordt weggeschreven - 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
- 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
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
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