Technický článek

Hodnota pole PDF se vrací zpět: přímé slovníky

Hodnota pole PDF „návrat zpět" nastane, když je pole uložené jako přímý slovník: PDFium povýší kopii v poli /Annots stránky na nepřímý objekt, zatímco /Fields si drží druhou přímou kopii. PDFium Component for Delphi řeší typované zápisy do polí tím, že před zápisem sjednotí obě reference do jednoho nepřímého vlastníka

Symptom obvykle čte jako strašení. Zápis vrátí True a hned následující čtení podá zpět starou hodnotu. Uložte, znovu načtěte a stará hodnota je pořád tam. Nic nevyhodilo výjimku a PDF vypadá v každém prohlížeči, který máte, naprosto obyčejně

Symptom: úspěšný zápis, který se nezafixuje

Vzorec selhání je úzký, ale velice opakovatelný: ukazuje se jen u formulářů, jejichž slovníky polí jsou zapsané inline (jako přímé objekty) místo číslovaných nepřímých objektů. Většina autorských nástrojů emituje nepřímá pole, takže se problém schovává měsíce a pak se objeví na souborech od jednoho konkrétního generátoru, často ručně vyráběného exporteru, který píše celé /AcroForm inline

Dvě pozorování dohromady ukazují na příčinu: zápis hlásí úspěch a nativní čtení pole přes vrstvu formulářů vrací předchozí hodnotu. Když vidíte obojí, přestaňte podezírat kódování řetězců a podívejte se na vlastnictví objektů

Proč se hodnota pole PDF vrátí zpět po úspěšném zápisu?

Hodnota pole PDF se vrací zpět, protože dva oddělené slovníky si oba nárokují totéž pole a zápis aktualizoval kopii, kterou strom polí nekonultuje. Hodnota pole žije v jeho položce /V a ISO 32000-1 §12.7.3.1 definuje, kde je ta položka autoritativní: ve slovníku pole dosaženém z /AcroForm /Fields, jdoucím dolů přes /Kids k terminálovému poli, s dědičnými položkami rozřešenými z /Parent

Tatáž klauzule dovoluje pohodlí, které způsobuje všechnu potíž. Když má pole přesně jednu anotaci widgetu, slovník pole a slovník anotace widgetu se mohou sloučit do jediného slovníku. V dobře utvořeném souboru je ten sloučený slovník jeden nepřímý objekt odkazovaný ze dvou míst: z /Fields (nebo z /Kids rodiče) a z pole /Annots stránky. Dvě reference, jeden objekt, jedno /V

Dvě kopie jednoho slovníku

ISO 32000-1 §7.3.10 je explicitní v tom, že jen nepřímý objekt má identitu, kterou lze sdílet. Přímý slovník nemá číslo objektu, takže když generátor zapíše sloučený slovník pole/widgetu inline jak v /Fields, tak v /Annots, soubor neobsahuje jeden objekt odkazovaný dvakrát. Obsahuje dva stejně vypadající slovníky, které z náhody mají tytéž klíče a hodnoty

PDFium to toleruje a tím ze splitu dělá trvalý. Když potřebuje handle na anotaci, PDFium může přímý slovník widgetu v poli /Annots stránky povýšit na nepřímý objekt, ale ekvivalentní přímá kopie zůstane tam, kde byla, uvnitř /AcroForm /Fields (nebo uvnitř /Kids rodiče). Od té chvíle dokument drží dva vlastníky pro jedno logické pole. Writer, který edituje /V na anotaci stránky, dostane na úrovni slovníku opravdový úspěch. Strom polí, kterém vrstva formulářů a každý pozdější čtenář věří jako zdroji pravdy, změnu nikdy nevidí

PDFium Component v Delphi: dva přímé slovníky nárokují jedno pole, zápis zasáhne povýšenou anotaci a strom polí pořád čte starou kopii z /Fields
zápis se věrně serializoval do objektu, kterého hierarchie polí nevlastní, takže zastaralá hodnota přežije uložení a znovunačtení

Proto zastaralá hodnota přežije uložení a znovunačtení: nová hodnota se věrně serializovala do objektu, kterého hierarchie polí nevlastní

Jak PDFium Component obnovuje jediného vlastníka pole

PDFium Component normalizuje přímé vlastnictví polí před každým typovaným zápisem: povýší přímé /AcroForm a slovníky polí na nepřímé objekty, sváže každý přímý widget s jeho odpovídající anotací na stránce a přepíše /Fields, /Kids, /Parent i /Annots tak, aby každá reference mířila na tentýž nepřímý objekt. Teprve potom změní /V, takže hodnota pole, vybraný widget i uložený dokument sdílejí jednoho vlastníka

Těžká část je identita. Dva přímé slovníky se shodným obsahem nedokazují, že jsou totýž widget; formulář může legitimně obsahovat dva widgety, které vypadají stejně. Normalizer se proto odmítá hádat a vyžaduje explicitní, unikátní anotační název /NM (ISO 32000-1 §12.5.2) na každém přímém widgetu. Pravidla, která aplikační cesta vynucuje, jsou přísná:

  • Přímý widget potřebuje neprázdné /NM o nejvýš 1024 UTF-16 jednotkách a přesně jedna anotace na stránce smí nést tentýž název
  • Přímá kopie i anotace na stránce se musí serializovat identicky, kromě /Parent a PDFiem regenerovaného /AP; jakýkoli jiný rozdíl zápis odmítne
  • Oba rodičovské odkazy se kontrolují odděleně, takže správné /Parent na anotaci stránky nemůže zakrýt konfliktní na původní přímé kopii
  • Chybějící /Parent se dodá jen tehdy, když unikátní vlastnictví ve stromu polí dělá odpověď jednoznačnou, a ekvivalentní přímé rodičovské kopie se znovu svážou se svým povýšeným vlastníkem
  • Duplicitní plně kvalifikované názvy polí, cyklické nebo vícekrát vlastněné stromy polí a podpisová pole se odmítají, ještě než se cokoliv publikuje
  • Nesouvisející anotace, jako odkazy na téže stránce, a nesouvisející položky /AcroForm zůstávají nedotčené

Normalizer běží taky pod tvrdými rozpočty: 4096 návštěv ve stromu polí, stránkách nebo členech pole, hloubka zanoření 64, 256 nových nepřímých objektů, 1 MiB na povýšený slovník, 4 MiB povýšených bajtů celkem, 128 MiB prohlížených slovníkových bajtů a 4096 UTF-16 jednotek na celý název pole s 262144 napříč formulářem. Nezpůsobilý nebo poškozený soubor narazí na rozpočet a selže s chybovou zprávou místo toho, aby z vyplnění formuláře udělal denial of service

Celá operace je transakční. TPdf.SetFormFieldValues zachytí snapshot, postaví inkrementální aktualizaci obsahující normalizované objekty a nové hodnoty, načte tenhle kandidát a živý dokument vymění jen tehdy, když kandidát projde ověřením. Pokud normalizace soubor odmítne nebo pozdější požadavek v batchi selže, původní dokument, jeho hodnoty i logický kurzor polí zůstanou přesně tak, jak byly

Jak PDFium Component pro Delphi sjednocuje přímé vlastnictví AcroForm polí: snapshot, kontrola identity NM, sjednocení referencí, zápis hodnoty a ověřené publikování s explicitními cestami odmítnutí
přímé slovníky polí se povýší a znovu sváží k jednomu nepřímému vlastníkovi před změnami /V a nejednoznačné vlastnictví se odmítá čistě

Zápis a ověření hodnoty přímého pole v Delphi

Bezpečný vzorec je zapsat a pak číst zpět přes nativní vrstvu formulářů a nesouhlas brát jako selhání, i když zápis vrátil True. TPdf.SetFormFieldValue adresuje pole indexem na aktuální stránce, bere typovaný TPdfFormValue a plní TPdfFormValueWriteReport; TPdf.FormField[Index] čte hodnotu zpět přes vrstvu formulářů PDFium, která rozřeší strom polí, místo aby věřila widgetu

Vzorec zapiš a ověř pro pole formulářů PDF v Delphi s PDFium Component: typované SetFormFieldValue, čtení zpět přes FormField, pak kontroly uložení a znovunačtení
samotné True dokazuje jen to, že se změnil slovník — čtěte zpět přes strom polí a otestujte uložení a znovunačtení
uses
  System.SysUtils, PDFium;

procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
  const NewText: WString);
var
  Value: TPdfFormValue;
  Report: TPdfFormValueWriteReport;
begin
  Pdf.PageNumber := PageNo;          // SetFormFieldValue míří na aktuální stránku
  Value := Default(TPdfFormValue);
  Value.Kind := fvkText;
  Value.Text := NewText;

  if not Pdf.SetFormFieldValue(FieldIndex, Value, Report) then
    raise Exception.CreateFmt('Field %d on page %d rejected: %s',
      [FieldIndex, PageNo, Report.ErrorMessage]);

  // Ověřujte proti stromu polí, ne proti vlastní proměnné
  if Pdf.FormField[FieldIndex] <> NewText then
    raise Exception.CreateFmt('Field "%s" did not keep the new value',
      [Pdf.FormFieldInfo[FieldIndex].Name]);
end;

Na souboru s nezřešitelným vlastnictvím, třeba přímém widgetu bez /NM, vrátí SetFormFieldValue False, Report.UpdatedWidgets je nula a Report.ErrorMessage pojmenuje problém, třeba „Direct widgets require an explicit unique NM identity". To je chování, které chcete: jasné odmítnutí místo tichého napůl zápisu. Jedna podmínka navíc stojí za znalost. Typovaný zápis je jen o anotacích a nebere nativní focus, takže pokud je nativní editor pole stále zamerený, celá transakce se odmítne, ještě než se pořídí snapshot; zavolejte nejdřív záměrně Pdf.ClearFormFieldFocus, místo opakování ve smyčce

Pro několik polí, zejména napříč stránkami, použijte batch formu. TPdf.SetFormFieldValues přijímá 1 až 256 požadavků, aplikuje je všechny na jednoho kandidáta a publikuje všechno nebo nic. TPdfFormValuesWriteReport.FailedRequest řekne, který požadavek rozbil batch

procedure FillInvoiceHeader(Pdf: TPdf);
var
  Requests: TPdfFormValueRequests;
  Batch: TPdfFormValuesWriteReport;
begin
  SetLength(Requests, 2);
  Requests[0] := Default(TPdfFormValueRequest);
  Requests[0].Page := 1;
  Requests[0].FieldIndex := 0;
  Requests[0].Value.Kind := fvkText;
  Requests[0].Value.Text := 'ACME Tooling Inc.';

  Requests[1] := Default(TPdfFormValueRequest);
  Requests[1].Page := 2;
  Requests[1].FieldIndex := 0;
  Requests[1].Value.Kind := fvkText;
  Requests[1].Value.Text := '2026-10-27';

  if not Pdf.SetFormFieldValues(Requests, Batch) then
    raise Exception.CreateFmt('Request %d failed: %s',
      [Batch.FailedRequest, Batch.ErrorMessage]);
  // Batch.UpdatedFields = 2; Batch.UpdatedWidgets počítá každý synchronizovaný widget
end;

Dva požadavky mířící na totéž terminálové pole se odmítnou jako duplicitní vlastník místo toho, aby vyhrál poslední zápis, a zdrojový dokument se zachová. Hodnoty checkboxů a radií následují ta samá pravidla; rozdělení /V verzus /AS pro tlačítka popisuje hodnota checkbox pole verzus stav widgetu v PDFium

Dokázat, že hodnota přežije uložení a znovunačtení

Čtení zpět v téže session dokazuje, že živý dokument je koherentní. Uložení a znovunačtení dokazuje, že soubor taky je, a ta kontrola je dost levná na to, abyste si ji drželi ve vlastních integračních testech

procedure SaveAndReopenCheck(Pdf: TPdf; const Target: string;
  PageNo, FieldIndex: Integer; const Expected: WString);
var
  Reopened: TPdf;
begin
  if not Pdf.SaveAs(Target, saNoIncremental, pvUnknown) then
    raise Exception.Create('Save failed');

  Reopened := TPdf.Create(nil);
  try
    Reopened.FileName := Target;
    Reopened.Active := True;
    Reopened.PageNumber := PageNo;
    if Reopened.FormField[FieldIndex] <> Expected then
      raise Exception.Create('Saved file lost the field value');
  finally
    Reopened.Free;
  end;
end;

Další dvě bajtové pasti: Trim a trailer /ID

Normalizace přímých polí znamená znovu číst surové slovníkové bajty, což vynáší na povrch dvě menší pasti relevantní pro jakýkoli kód v Delphi, který sahá po syntaxi PDF

První je SysUtils.Trim nad surovými PDF bajty drženými v AnsiString. Literální řetězec, třeba volba v choice, může legitimně obsahovat text UTF-16BE s byte order mark FE FF a vloženými NUL bajty. V Unicode Delphi bere SysUtils.Trim string, takže volání tiše konvertuje AnsiString do UTF-16 přes aktivní ANSI kódovou stránku a výsledek konvertuje zpět. Podle té kódové stránky může tenhle round-trip přepsat vysoké bajty, poškodit BOM a dokonce narušit oddělovač, který následuje. Bezpečný přístup drží lexer na bajtech: přeskočte PDF whitespace podle pozice (ISO 32000-1 §7.2.2 vyjmenovává NUL, tab, LF, FF, CR a mezeru) a krájejte přes Copy a dekódujte jen tehdy, když je jisté, že hodnota je PDF textový řetězec

// Rizikové nad surovými PDF bajty: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);

// Bezpečné po bajtech: přeskočte PDF whitespace podle pozice, pak krájejte
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
  Inc(P);
Token := Copy(Token, P, MaxInt);

Druhá past sedí v samotné inkrementální aktualizaci. ISO 32000-1 §7.5.5 (Table 15) a §14.4 vyžadují, aby trailer /ID bylo pole dvou bajtových řetězců. Interní helper skládající identifikátor sparse aktualizace vrací jen obsah těch dvou řetězců, bez klíče /ID a bez hranatých závorek pole, a trailer to musí explicitně obalit jako /ID [ ... ]. Shovívavý loader PDFium i pozdější plný re-save chybějící obálku schovaly, takže spolehlivá kontrola je parsovat kandidátní i uložené soubory nezávislým přísným parserem a hledat dva 16bajtové řetězce. Pokud inkrementální aktualizace píšete sami, stejná past platí; článek o inkrementálních aktualizacích pro šifrované PDF pokrývá řetěz trailerů do hloubky

Co dělat ve vlastním formulářovém kódu v Delphi?

  • Po zápisu si vždy čtěte hodnotu zpět přes FormField[Index] a v testech taky uložte, znovunačtěte a přečtěte znovu
  • False plus ErrorMessage berte jako informaci: odmítnutí pojmenovávající NM, konflikt rodiče nebo duplicitní název znamená, že vlastnictví souboru je doopravdy nejednoznačné
  • Pokud PDF generujete, emitujte pole a widgety jako nepřímé objekty a dejte každému widgetu unikátní /NM; tim zmizí nejednoznačnost u zdroje
  • Pro víc polí nebo zápis napříč stránkami použijte SetFormFieldValues, aby pozdní selhání nemohlo nechat aktualizovanou polovinu formuláře
  • Nikdy nespouštějte Trim ani další string RTL helpery nad surovými PDF bajty drženými v AnsiString

Držte si jasno i v hranicích. Podepsané dokumenty, XFA formuláře, šifrované snapshoty a soubory, jejichž vlastnictví widgetů se nedá rozřešit jednoznačně, zůstávají mimo deklarovaný profil a odmítají se místo protahování násilím. Pokud váš workflow přesouvá pole mezi dokumenty, otázky vlastnictví jsou ještě tvrdší; štěpování AcroForm polí napříč PDF dokumenty se tím případem zabývá

PDFium Component přináší tohle typované, transakční psaní do formulářů aplikacím Delphi, C++Builder a Lazarus spolu s renderováním, extrakcí textu a editací dokumentů. Historie verzí, licencování a stahování jsou na produktové stránce PDFium Component for Delphi