Teknisk artikel

Atomar batchudfyldning af PDF-formularer i Delphi med PDFium

PDFium Component til Delphi udfylder en AcroForm atomart med TPdf.SetFormFieldValues: du giver 1 til 256 typede requests adresseret efter side og widget, og komponenten anvender enten hver værdi i én dokumenterstatning eller ingen. Når én request er ugyldig, navngiver TPdfFormValuesWriteReport.FailedRequest den request, der sank batchen

Den garanti betyder mest i de jobs, ingen ser til. En natlig import skubber CRM-rækker ind i en skadesanmeldelsesformular, eller en printservice lægger policidata over en fortrykt skabelon. Kører du de felter én ad gangen, efterlader den første dårlige værdi — en kode, der ikke er en gyldig radioeksport, en streng længere end feltets MaxLen — dig med en PDF, der er halvt gammel data og halvt ny. Intet crashede, så intet alarmerede nogen. Batch-API'en findes, så en fejl er et rent afslag i stedet for et halvskrevet dokument

Hvorfor er felt-for-felt formularudfyldning farlig for dataimport?

Felt-for-felt-udfyldning er farlig, for hver skrivning committer for sig selv, så en fejl midt i løkken efterlader et dokument, der matcher hverken kilderecorden eller den oprindelige skabelon. Det klassiske mønster er en løkke over FormField[Index]-tildelinger, hvor hver går gennem PDFiums native form-fill-miljø én widget ad gangen. Værdi 1 til 17 lander, værdi 18 udløser en fejl, og den kaldende må nu vide præcis, hvilke værdier kom ind for at omgøre dem

De fleste kaldende skriver aldrig den undo-kode, og dem, der gør, tager den normalt subtilt forkert: et fælles felt har allerede skiftet på hver side, det optræder på, og checkbox-tilstande er allerede flippet. Kører gemningen i en finally-blok, ryger den halvt fyldte fil på disken og ser komplet ud. I et overprint-job er det værre end ingen fil, for den printes, og nogen underskriver den

Det andet lydløse problem er fokus. Den klassiske sætter driver PDFiums interaktive editor: den dræber eksisterende fokus, fokuserer target-widgeten, markerer al tekst, erstatter den og dræber fokus igen. Det er fint til et headless-job, men i en viewer afslutter det også enhver redigeringssession, brugeren havde åben i et andet felt. Den typede skrivevej, der kom i PDFium Component 3.231.0, går slet ikke gennem native feltfokus, og 3.233.0 udvidede den fra enkeltfelter til cross-page batches

Hvordan SetFormFieldValues anvender en batch i én erstatning

TPdf.SetFormFieldValues opstiller hver request mod ét indfanget snapshot af dokumentet, skriver alle felt-dictionaries i ét træk og bytter det færdige resultat ind med en enkelt erstatning. Det oprindelige dokument forbliver live helt til sidste trin, så enhver fejl før det trin efterlader det urørt

Den femtrins atomare batch-formularudfyldningssekvens i PDFium Component til Delphi, fra preflight til verificering og swap med alt-eller-intet-udfald
hver request opstilles mod ét snapshot, og det live dokument røres kun ved den sidste verificerede swap

Sekvensen forklarer både garantien og omkostningen:

  1. Preflight-tjek kører først: dokumentet skal være aktivt, request-antallet skal være 1 til 256, ingen page-object- eller edit-journal-transaktion må være åben, intet native felt må holde fokus, og dokumentet skal give fill-forms- eller modify-tilladelse
  2. Hver request valideres for sideinterval og inputbudget, og FailedRequest sporer det indeks, der tjekkes
  3. Komponenten fanger et edit-snapshot, normaliserer AcroForm i den kopi og anvender hver request i orden og skriver /V, /I for choice-valg og /AS for hver widget, der deler feltet
  4. Ændringerne tilføjes snapshotet som én sparsom inkrementel opdatering i betydningen ISO 32000-1 §7.5.6, så urelaterede objekter følges med byte for byte
  5. En separat verifikator-instans loader kandidatbyte først; først efter den åbner rent, afliver komponenten det live dokument og loader det nye og falder tilbage til det oprindelige snapshot, hvis loadet fejler

Det interne erstatningstrin er samme maskineri, som komponenten bruger til sine edit-journal-snapshots, og derfor kan undo- og crash-recovery-journalen og en batchskrivning ikke overlappe: afslut eller annuller journaltransaktionen først. Automatiske livscyklushandlinger undertrykkes, mens dokumentet byttes, så et document-open-script kan ikke omskrive felter eller snappe fokus midt i erstatningen

At bygge en typet request-batch i Delphi

En batch er et dynamisk array af TPdfFormValueRequest-records, som hver holder en 1-baseret Page, et 0-baseret side-lokalt FieldIndex og en typet TPdfFormValue. Værdirecorden bærer en Kind af fvkText, fvkChoice, fvkCheckBox eller fvkRadioButton plus Text-, ChoiceIndices- og Checked-medlemmer brugt efter den kind

Importdata kommer normalt nøglet efter feltnavn, så det første job er at oversætte navne til side- og widget-adresser. FieldIndex tæller widget-annotationer på en side i samme orden som FormFieldCount og FormFieldInfo, så et simpelt scan virker. Hjælperen tager den første widget, den finder for et navn, hvilket betyder noget for fælles felter, som næste afsnit forklarer

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);      // kun første widget: én request per felt
      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;

Bemærk at AddRequest udløser en fejl ved et ukendt navn i stedet for at springe det over. Batchen validerer adresser, ikke din mapping, så et manglende skabelonfelt må fejle her, ellers fejler det aldrig. Med adresserne opløst er det kort at bygge værdierne og committe dem

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';                // eksportværdi for knappen, der skal vælges
    AddRequest(Requests, 'Shipping', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkChoice;
    SetLength(V.ChoiceIndices, 1);
    V.ChoiceIndices[0] := 2;            // 0-baseret indeks i feltets Opt-array
    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 typede kinds tjekkes mod felttypen, før noget skrives. En tekstværdi sigtet mod en checkbox, en radioværdi sigtet mod en almindelig checkbox eller en skrivning til en push-knap afviser alle. Choice-indeks skal være gyldige, unikke og stigende; mere end ét indeks kræver MultiSelect-flaget, og fri tekst i et choice-felt accepteres kun af en redigérbar combo box. En tekstværdi længere end feltets MaxLen afvises i stedet for at blive trunkeret, og read-only-felter afviser kategorisk. For radiogrupper med NoToggleToOff-flaget afvises en tom eksportværdi — betydningen "ryd gruppen" — også

Hvorfor fejler skrivning af et fælles felt to gange hele batchen?

TPdf.SetFormFieldValues afviser to requests, der opløses til samme terminalfelt, for de ville begge skrive en enkelt værdi, og komponenten nægter at lade én request lydløst overskrive en anden. Tjekket kører på felt-ejeren, ikke widgeten, så to forskellige widgets af ét felt på to forskellige sider tæller stadig som en duplikat

Årsagen er feltmodellen i ISO 32000-1 §12.7.3.1. Et terminalfelt ejer værdien; dets widget-annotationer er kun steder, hvor den værdi vises. Et felt ved navn PolicyNumber, der optræder i headeren på hver side, er ét felt med flere widgets, og hver af de widgets viser samme /V. En radiogruppe virker på samme måde: hver knap er en widget, gruppen er feltet. At skrive PolicyNumber gennem side 1-widgeten og igen gennem side 9-widgeten er ét felt skrevet to gange, og enhver regel, der udpeger en vinder, skjuler en bug i din mapping

Så behandler batchen det som en fejl og navngiver den anden request i FailedRequest. I praksis er løsningen upstream: opløs hvert feltnavn til præcis én adresse, som FindFieldAddress gør, og lad komponenten propagere værdien til de andre widgets. Det gør den som del af skrivningen ved at sætte /AS på hver widget i en delt checkbox eller radiogruppe, så appearance-tilstanden stemmer med feltværdien — samme opdeling dækket i artiklen om checkbox-feltværdier versus widget-tilstande

Ét terminalfelt ejer værdien, widgets på to sider viser den, og en duplikatrequest fejler batchen i PDFium Component til Delphi
én request per terminalfelt — komponenten skriver /V én gang og sætter /AS på hver widget, der deler den

At læse fejlrapporten

En fejlet batch returnerer False, udløser ingen fejl og efterlader det oprindelige dokument og hver feltværdi præcis, som de var. TPdfFormValuesWriteReport rapporterer så Succeeded = False, nul for både UpdatedFields og UpdatedWidgets, det 0-baserede indeks for den overtrædende request i FailedRequest og en menneskelæsbar grund i 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;

En FailedRequest på -1 betyder, at afvisningen tilhører transaktionen frem for en request: native fokus var aktivt, dokumentet er signeret, formularen er XFA, eller et tilladelsestjek fejlede. En ikke-negativ værdi peger på én post. Ved et samlet budgetoverskud peger den på den request, der skubbede batchen over grænsen, hvilket ikke nødvendigvis er den request, der er forkert. Hold et parallelt array af kildekolonnenavne ved siden af Requests, så kan du vende indeks 14 direkte til "række 2087, kolonne ShipDate" i importloggen

Ved succes er UpdatedFields lig request-antallet, og UpdatedWidgets tæller hver berørt widget, de delte inkluderet — et billigt sanity-tjek mod skabelonen

Forudsætninger og grænser, du vil støde på

Den mest almindelige afvisning i et viewer-program er native fokus: taster en bruger i et felt, nægter SetFormFieldValues, før den så meget som fanger snapshotet, og den ventende editorbuffer overlever intakt. Komponenten dropper ikke fokus bag din ryg, for det ville enten committe eller miste tekst, brugeren ikke er færdig med. Kald ClearFormFieldFocus bevidst, hvilket afslutter editorsessionen som et klik uden for feltet, og genprøv så

Budgetterne er faste og dokumenterede, så du kan dimensionere imports mod dem:

  • 1 til 256 requests pr. batch; opdel større records i flere batches og acceptér, at atomariteten så gælder pr. batch
  • Højst 262.144 UTF-16-enheder tekst og 4.096 choice-indeks pr. request
  • Højst 1.048.576 UTF-16-enheder tekst og 16.384 choice-indeks på tværs af hele batchen
  • De sædvanlige dokumentbudgetter for snapshot-parseren gælder stadig, inklusiv en kilde på 64 MiB og 65.536 objekter

En vellykket erstatning ugyldiggør også native handles, søgninger og markeringer opnået før kaldet, for dokumentet nedenunder er nyt. Side- og view-aktivering og logiske felt-cursorer beholdes; alt andet, du har cachet, som annotation- eller page-object-handles, skal hentes igen

Hvordan adskiller det sig fra XFDF-import?

TPdf.ImportXFDF og TPdf.SetFormFieldValues putter begge data i felter, men XFDF-import er en tolerant merge, mens batchen er en streng transaktion. XFDF-import matcher værdier til felter efter navn, springer navne over, den ikke kan finde, ignorerer duplikater og returnerer, hvor mange poster den anvendte. Det er præcis rigtigt til at merge en reviewers kommentarer, hvor et delvist resultat stadig er nyttigt

Batchen gør det modsatte på hvert punkt. Den adresserer widgets eksplicit, afviser ukendte eller mistypede targets, behandler duplikater som fejl og har intet delvist udfald. Brug XFDF, når data er en interchange-fil fra et andet værktøj; brug SetFormFieldValues, når din egen kode ejer mappingen, og en halvanvendt record ville være en defekt

Beslutningsmatrix, der sammenligner XFDF-import med den atomare SetFormFieldValues-batch i PDFium Component til Delphi: targeting, ukendte og duplikerede targets, udfald og bedste anvendelse
XFDF merger interchange-filer tolerante, mens batchen nægter ukendte navne, duplikater og delvise udfald

Hvad batchskrivningen ikke dækker

Batchen er en afgrænset AcroForm-profil, og den siger nej i stedet for at gætte uden for den profil. XFA-formularer afvises og behøver deres dedikerede editor. Signerede dokumenter afvises, for at omskrive felt-dictionaries under en eksisterende signatur er et signatur-workflow, ikke en udfyldningsoperation; krypterede snapshots er også uden for profilen. Misdannede eller tvetydige button-eksportarrays afvises, og radioeksportværdier, der matcher mere end én knap, nægtes, medmindre gruppen sætter RadiosInUnison. Transaktionen normaliserer også direkte felt-dictionaries før skrivningen — tilfældet bag feltværdier, der tilsyneladende vender tilbage

Appearances fortjener én sætning af omhu. For ændrede tekst- og choice-widgets dropper batchen det forældede /AP i stedet for at skibe et appearance, der viser den gamle værdi, og den slår NeedAppearances fra, så PDFium ikke genopbygger hver urelateret widget i formularen. PDFium regenererer de manglende appearances, når den renderer. Ryger den udfyldte PDF til en streng printpipeline, der aldrig syntetiserer appearances, så render eller flatten den gennem PDFium først og test den vej med dine egne skabeloner

SetFormFieldValues og de typede værdirecords følger med i PDFium Component til Delphi og Lazarus, hvor productsiden lister den fulde formularudfyldnings- og AcroForm-API ved siden af rendering, redigering og viewer-kontroller