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í
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é
/NMo 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ě
/Parenta 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é
/Parentna anotaci stránky nemůže zakrýt konfliktní na původní přímé kopii - Chybějící
/Parentse 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
/AcroFormzů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
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
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 FalseplusErrorMessageberte 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
Trimani dalšístringRTL helpery nad surovými PDF bajty drženými vAnsiString
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