Technický článek

Atomické dávkové vyplnění formulářů v Delphi

PDFium Component for Delphi vyplňuje AcroForm atomicky přes TPdf.SetFormFieldValues: předáte 1 až 256 typovaných požadavků adresovaných stránkou a widgetem a komponenta buď aplikuje každou hodnotu v jedné náhradě dokumentu, nebo neaplikuje žádnou. Když je jeden požadavek neplatný, TPdfFormValuesWriteReport.FailedRequest pojmenuje ten, který batch potopil

Ta garance se hodí nejvíc v jobech, které nikdo nehlídá. Noční import tlačí řádky CRM do formuláře claimu nebo tisková služba překrývá data pojistky na předtištěnou šablonu. Pusťte je pole po poli a první špatná hodnota, kód, který není platným radio exportem, nebo řetězec delší než MaxLen pole vám nechá v rukou PDF, které je napůl stará data a napůl nová. Nic nespadlo, takže nikdo nebyl upozorněn. Batch API existuje proto, aby selhání bylo čisté odmítnutí místo napůl napsaného dokumentu

Proč je vyplňování pole za polem nebezpečné pro datový import?

Vyplňování pole za polem je nebezpečné, protože každý zápis commituje sám za sebe, takže chyba v půlce smyčky zanechá dokument, který neodpovídá ani zdrojovému záznamu, ani původní šabloně. Klasický vzorec je smyčka přes přiřazení FormField[Index], každé jdoucí přes nativní form-fill prostředí PDFium po jednom widgetu. Hodnoty 1 až 17 dopadnou, hodnota 18 selže a volající teď musí vědět přesně, které hodnoty se dostaly dál, aby je vzal zpět

Většina volajících tu undo logiku nikdy nenapíše a ti, kteří ji napíšou, ji obvykle pokazí nenápadně: sdílené pole se už změnilo na každé stránce, kde se objevuje, a stavy checkboxů se už překlopily. Pokud ukládání běží v bloku finally, napůl vyplněný soubor jde na disk a vypadá hotový. U overprint jobu je to horší než žádný soubor, protože se vytiskne a někdo ho podepíše

Druhý tichý problém je focus. Klasický setter řídí interaktivní editor PDFium: zabije existující focus, zamerí cílový widget, označí celý text, nahradí ho a zase zabije focus. Pro headless job je to v pořádku, ale v prohlížeči tím taky skončí jakákoli editační session, kterou měl uživatel otevřenou v jiném poli. Typovaná zápisová cesta, která přišla v PDFium Component 3.231.0, nechodí přes nativní focus polí vůbec a 3.233.0 ji rozšířila z jednotlivých polí na batche napříč stránkami

Jak SetFormFieldValues aplikuje batch jednou náhradou

TPdf.SetFormFieldValues nasazuje každý požadavek proti jednomu zachycenému snapshotu dokumentu, zapisuje všechny slovníky polí v jedné passáži a hotový výsledek vymění jedinou náhradou. Původní dokument zůstává živý až do úplně posledního kroku, takže jakékoli selhání před tím krokem ho nechá nedotčený

Pětistupňová atomická dávka vyplnění formuláře v PDFium Component pro Delphi, od preflightu po verify a swap s výsledky všechno-nebo-nic
každý požadavek se nasazuje proti jednomu snapshotu a živého dokumentu se dotkne jen finální ověřená výměna

Ta posloupnost vysvětluje jak garanci, tak cenu:

  1. Nejdřív běží preflight kontroly: dokument musí být aktivní, počet požadavků musí být 1..256, nesmí být otevřená žádná transakce page-objectu ani edit-journalu, žádné nativní pole nesmí držet focus a dokument musí udělovat permission fill-forms nebo modify
  2. Každý požadavek se kontroluje na rozsah stránek a vstupní rozpočet a FailedRequest sleduje právě prověřovaný index
  3. Komponenta zachytí edit snapshot, v té kopii normalizuje AcroForm a aplikuje všechny požadavky v pořadí, zapisuje /V, /I pro volby choice a /AS u každého widgetu, který pole sdílí
  4. Změny se k snapshotu připojí jako jedna sparse inkrementální aktualizace v smyslu ISO 32000-1 §7.5.6, takže nesouvisející objekty se přenesou bajt za bajtem
  5. Samostatná verifier instance nejdřív načte kandidátní bajty; teprve když se otevřou čistě, komponenta odloží živý dokument a načte nový, s návratem k původnímu snapshotu, když se ten load nepovede

Interní krok výměny je tatáž mechanika, kterou komponenta používá pro snapshoty edit-journalu, a proto journal undo a crash recovery s dávkovým zápisem nemůžou překřížit: nejdřív dokončete nebo zrušte transakci journalu. Automatické lifecycle akce jsou při výměně dokumentu potlačené, takže dokument-open skript nemůže v půlce výměny přepsat pole nebo si vzít focus

Stavba typovaného batche požadavků v Delphi

Batch je dynamické pole záznamů TPdfFormValueRequest, každý drží jednozákladové Page, nulozákladové FieldIndex lokální v rámci stránky a typovaný TPdfFormValue. Záznam hodnoty nese Kind typu fvkText, fvkChoice, fvkCheckBox nebo fvkRadioButton plus členy Text, ChoiceIndices a Checked používané podle toho druhu

Importní data obvykle přichází klíčovaná názvem pole, takže první práce je přeložit názvy na adresy stránky a widgetu. FieldIndex počítá anotace widgetů na stránce ve stejném pořadí jako FormFieldCount a FormFieldInfo, takže prostý sken stačí. Helper vezme první widget, který pro název najde, což má význam u sdílených polí, jak vysvětluje další sekce

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);      // jen první widget: jeden požadavek na pole
      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;

Všimněte si, že AddRequest u neznámého názvu selže, místo aby ho přeskočil. Batch kontroluje adresy, ne vaše mapování, takže chybějící pole šablony musí selhat tady, nebo nikdy. Když jsou adresy rozřešené, stavba hodnot a commit je krátký

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';                // export value vybíraného tlačítka
    AddRequest(Requests, 'Shipping', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkChoice;
    SetLength(V.ChoiceIndices, 1);
    V.ChoiceIndices[0] := 2;            // index do pole Opt od nuly
    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;

Typované druhy se kontrolují proti typu pole, ještě než se cokoliv zapíše. Textová hodnota mířící na checkbox, radio hodnota mířící na obyčejný checkbox nebo zápis do push button se všechny odmítnou. Choice indexy musí být platné, unikátní a rostoucí; víc než jeden index vyžaduje příznak MultiSelect a volný text v choice poli přijímá jen editovatelný combo box. Textová hodnota delší než MaxLen pole se odmítne místo zkrácení a read-only pole se odmítnou rovnou. U radio skupin s příznakem NoToggleToOff se odmítá i prázdná export value, to je „vyčistit skupinu"

Proč dvojnásobný zápis sdíleného pole selže celý batch?

TPdf.SetFormFieldValues odmítne dva požadavky, které se rozřeší na totéž terminálové pole, protože oba by zapsaly jednu hodnotu, a komponenta odmítá dovolit, aby jeden požadavek tiše přepsal druhý. Kontrola běží na vlastníkovi pole, ne na widgetu, takže dva různé widgety jednoho pole na dvou různých stránkách se počítají jako duplicita pořád

Důvodem je model polí v ISO 32000-1 §12.7.3.1. Terminálové pole vlastní hodnotu; jeho anotace widgetů jsou jen místa, kde se hodnota zobrazuje. Pole pojmenované PolicyNumber, které stojí v hlavičce každé stránky, je jedno pole s několika widgety a každý z těch widgetů ukazuje totéž /V. Radio skupina funguje stejně: každé tlačítko je widget a skupina je pole. Zapsat PolicyNumber přes widget stránky 1 a znovu přes widget stránky 9 je jedno pole zapsané dvakrát a jakékoli pravidlo, které vybere vítěze, schová bug ve vašem mapování

Batch to proto bere jako chybu a v FailedRequest pojmenuje druhý požadavek. V praxi je oprava po proudu: rozřešte každý název pole na přesně jednu adresu, jako to dělá FindFieldAddress, a nechte komponentu propagovat hodnotu na ostatní widgety. Dělá to jako součást zápisu, nastavuje /AS na každém widgetu sdíleného checkboxu nebo radio skupiny, aby stav podoby souhlasil s hodnotou pole, totéž rozdělení, které popisuje článek o hodnotách checkbox polí verzus stavech widgetů

Jedno terminálové pole vlastní hodnotu, widgety na dvou stránkách ji zobrazují a duplicitní požadavek selže v PDFium Component pro Delphi
jeden požadavek na terminálové pole — komponenta zapíše /V jednou a nastaví /AS na každém widgetu, který ho sdílí

Čtení hlášení o selhání

Selhávající batch vrací False, nevyhazuje výjimku a nechává původní dokument i každou hodnotu polí přesně tak, jak byly. TPdfFormValuesWriteReport pak hlásí Succeeded = False, nulu pro UpdatedFields i UpdatedWidgets, nulozákladový index problematického požadavku v FailedRequest a lidsky čitelný důvod v 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;

FailedRequest rovný -1 znamená, že odmítnutí patří transakci, ne požadavku: nativní focus byl aktivní, dokument je podepsaný, formulář je XFA nebo neprošla kontrola permission. Nezáporná hodnota ukazuje na jednu položku. U agregovaného přetečení rozpočtu ukazuje na požadavek, který přetlačil batch přes limit, což nemusí být ten, který je špatný. Držte si vedle Requests paralelní pole názvů zdrojových sloupců a index 14 se vám promění rovnou v „řádek 2087, sloupec ShipDate" v importním logu

Při úspěchu se UpdatedFields rovná počtu požadavků a UpdatedWidgets počítá každý dotčený widget, sdílené nevyjímaje, což je levná kontrola rozumu proti šabloně

Podmínky a limity, do kterých narazíte

Nejčastějším odmítnutím v aplikaci prohlížeče je nativní focus: když uživatel píše do pole, odmítne SetFormFieldValues, ještě než vůbec zachytí snapshot, a buffer čekajícího editoru přežije vcelku. Komponenta focus nezahodí za vašimi zády, protože by tím buď zapsala, nebo ztratila text, který uživatel nedopsal. Zavolejte záměrně ClearFormFieldFocus, které ukončí editační session stejně jako kliknutí vedle pole, a pak to zkuste znovu

Rozpočty jsou fixní a zdokumentované, takže se podle nich dají dimenzovat importy:

  • 1 až 256 požadavků na batch; větší záznamy rozdělte do několika batchí a počítejte s tím, že atomičnost pak platí po batchi
  • Nejvýš 262 144 UTF-16 jednotek textu a 4 096 choice indexů na požadavek
  • Nejvýš 1 048 576 UTF-16 jednotek textu a 16 384 choice indexů napříč celým batchem
  • Obvyklé dokumentové rozpočty pro parser snapshotů platí dál, včetně zdroje 64 MiB a 65 536 objektů

Úspěšná výměna taky zneplatní nativní handle, vyhledávání a výběry získané před voláním, protože dokument pod nimi je nový. Aktivace stránek a pohledů i logické kurzory polí se drží; cokoliv jiného, co jste si cacheovali, jako handle anotací nebo page-objectů, si musíte znovu vyžádat

V čem se to liší od importu XFDF?

TPdf.ImportXFDF a TPdf.SetFormFieldValues dávají do polí oba data, ale XFDF import je shovívavé slučování, zatímco batch je přísná transakce. XFDF import páruje hodnoty s poli podle názvu, přeskočí názvy, které nenajde, ignoruje duplicity a vrátí, kolik položek aplikoval. To je přesně ono pro slučování komentářů recenzenta, kde je částečný výsledek pořád užitečný

Batch dělá na každém bodě opak. Adresuje widgety explicitně, odmítá neznámé nebo špatně typované cíle, bere duplicity jako chyby a nemá žádný částečný výsledek. XFDF použijte, když jsou data výměnným souborem od jiného nástroje; SetFormFieldValues použijte, když mapování vlastní váš kód a napůl aplikovaný záznam by byl defekt

Rozhodovací matice srovnávající XFDF import s atomickým batchem SetFormFieldValues v PDFium Component pro Delphi: cílení, neznámé a duplicitní cíle, výsledek a ideální použití
XFDF slučuje výměnné soubory shovívavě, zatímco batch odmítá neznámé názvy, duplicity a částečné výsledky

Co dávkový zápis nepokrývá

Batch je ohraničený AcroForm profil a mimo ten profil říká ne, místo hádání. XFA formuláře se odmítají a potřebují svůj dedikovaný editor. Podepsané dokumenty se odmítají, protože přepisovat slovníky polí pod existujícím podpisem je workflow podpisů, ne operace vyplnění; šifrované snapshoty jsou mimo profil taky. Deformované nebo nejednoznačné pole exportů tlačítek se odmítají a radio export values odpovídající víc než jednomu tlačítku se odmítají, pokud skupina nenastaví RadiosInUnison. Transakce taky normalizuje přímé slovníky polí před zápisem, ten případ za hodnotami polí, které zřejmě návrat zpět

Podoby si zaslouží jednu větu péče. U změněných textových a choice widgetů batch zahodí zastaralé /AP místo odeslání podoby ukazující starou hodnotu a vypne NeedAppearances, aby PDFium nepřestavoval každý nesouvisející widget formuláře. PDFium chybějící podoby regeneruje při renderování. Když vyplněné PDF jde do přísné tiskové pipeline, která podoby nikdy neskládá, renderujte ho nebo ho flattenete nejdřív přes PDFium a tu cestu otestujte na vlastních šablonách

SetFormFieldValues a typované hodnotové záznamy dodává PDFium Component for Delphi and Lazarus, kde produktová stránka uvádí kompletní API vyplňování formulářů a AcroForm spolu s renderováním, editací a viewer kontrolami