Wartość pola formularza PDF „wraca", gdy pole jest zapisane jako słownik direct: PDFium awansuje kopię z tablicy /Annots strony do obiektu indirect, podczas gdy /Fields trzyma drugą bezpośrednią kopię. PDFium Component dla Delphi obsługuje typowane zapisy pól przez zunifikowanie obu referencji w jednego właściciela indirect przed zapisem
Objaw zwykle brzmi jak nawiedzenie. Zapis zwraca True, a natychmiast po nim odczyt podaje starą wartość. Zapisz, otwórz ponownie — stara wartość nadal tam siedzi. Nic nie rzuciło i PDF wygląda całkowicie zwyczajnie w każdej przeglądarce, jaką posiadasz
Objaw: udany zapis, który się nie klei
Wzorzec porażki jest wąski, ale bardzo powtarzalny: pokazuje się wyłącznie na formularzach, których słowniki pól są zapisane inline (jako obiekty direct), a nie jako numerowane obiekty indirect. Większość narzędzi autorskich emituje pola indirect, więc problem chowa się miesiącami i potem wychodzi na plikach od jednego konkretnego generatora, często ręcznie skleconego eksportera piszącego całe /AcroForm inline
Dwie obserwacje razem wskazują przyczynę: zapis raportuje sukces, a natywny odczyt pola przez warstwę formularzy zwraca poprzednią wartość. Gdy widzisz oba, przestań podejrzewać kodowanie stringów i zacznij patrzeć na własność obiektów
Dlaczego wartość pola formularza PDF wraca po udanym zapisie?
Wartość pola formularza PDF wraca, bo dwa osobne słowniki oba podają się za to samo pole, a zapis zaktualizował kopię, której drzewo pól nie konsultuje. Wartość pola mieszka we wpisie /V, a ISO 32000-1 §12.7.3.1 definiuje, gdzie ten wpis jest wiążący: na słowniku pola osiąganym z /AcroForm /Fields, schodzącym przez /Kids do pola terminalnego, z wpisy dziedzicznymi rozwiązanymi z /Parent
Ta sama klauzula pozwala na wygodę, która rosi wszystkie kłopoty. Gdy pole ma dokładnie jedną adnotację widgeta, słownik pola i słownik adnotacji widgeta mogą być scalone w jeden słownik. W dobrze uformowanym pliku ten scalony słownik to jeden obiekt indirect, referowany z dwóch miejsc: z /Fields (albo /Kids rodzica) i z tablicy /Annots strony. Dwie referencje, jeden obiekt, jedno /V
Dwie kopie jednego słownika
ISO 32000-1 §7.3.10 mówi wprost, że tylko obiekt indirect ma tożsamość, którą można współdzielić. Słownik direct nie ma numeru obiektu, więc gdy generator zapisuje scalony słownik pola/widgeta inline w obu miejscach — w /Fields i w /Annots — plik nie zawiera jednego obiektu referowanego dwa razy. Zawiera dwa wyglądające identycznie słowniki, które przypadkiem mają te same klucze i wartości
PDFium to toleruje i tym samym utrwala rozdwojenie. Gdy potrzebuje uchwytu do adnotacji, PDFium potrafi awansować bezpośredni słownik widgeta w tablicy /Annots strony do obiektu indirect, ale równoważna bezpośrednia kopia zostaje tam, gdzie była, wewnątrz /AcroForm /Fields (albo wewnątrz /Kids rodzica). Od tej chwili dokument trzyma dwóch właścicieli dla jednego logicznego pola. Writer edytujący /V na adnotacji strony dostaje prawdziwy sukces na poziomie słownika. Drzewo pól — to, co warstwa formularzy i każdy późniejszy czytnik traktują jako źródło prawdy — zmiany nigdy nie widzi
Dlatego nieaktualna wartość przeżywa zapis i ponowne otwarcie: nowa wartość została wiernie zserializowana do obiektu, którego hierarchia pól nie posiada
Jak PDFium Component przywraca jednego właściciela pola
PDFium Component normalizuje bezpośrednią własność pól przed każdym typowanym zapisem: awansuje bezpośrednie słowniki /AcroForm i pól do obiektów indirect, wiąże każdy bezpośredni widget z jego pasującą adnotacją strony i przepisuje /Fields, /Kids, /Parent i /Annots tak, żeby każda referencja wskazywała ten sam obiekt indirect. Dopiero potem zmienia /V, więc wartość pola, wybrany widget i zapisany dokument dzielą jednego właściciela
Trudną częścią jest tożsamość. Dwa słowniki direct o identycznej treści nie dowodzą, że to ten sam widget; formularz może legalnie zawierać dwa podobne widgety. Normalizator odmawia więc zgadywania i wymaga jawnej, unikalnej nazwy adnotacji /NM (ISO 32000-1 §12.5.2) na każdym bezpośrednim widgetcie. Reguły stosowane przez ścieżkę zapisu są surowe:
- Bezpośredni widget potrzebuje niepustego
/NMo długości najwyżej 1024 jednostek UTF-16, i dokładnie jedna adnotacja strony może nosić tę samą nazwę - Bezpośrednia kopia i adnotacja strony muszą serializować się identycznie poza
/Parenti regenerowanym przez PDFium/AP; każda inna różnica odrzuca zapis - Oba łącza rodzica są sprawdzane osobno, więc poprawne
/Parentna adnotacji strony nie może zamaskować konfliktowego na oryginalnej kopii direct - Brakujące
/Parentjest dokładane tylko wtedy, gdy unikalna własność w drzewie pól czyni odpowiedź jednoznaczną, a równoważne bezpośrednie kopie rodzica są przepinane do awansowanego właściciela - Zduplikowane w pełni kwalifikowane nazwy pól, cykliczne albo wielokrotnie posiadane drzewa pól i pola podpisów odrzucają, zanim cokolwiek zostanie opublikowane
- Niepowiązane adnotacje, jak linki na tej samej stronie, i niepowiązane wpisy
/AcroFormzostają nietknięte
Normalizator działa też pod twardymi budżetami: 4096 odwiedzin drzewa pól, stron albo członów tablic, głębokość zagnieżdżenia 64, 256 nowych obiektów indirect, 1 MiB na awansowany słownik, 4 MiB awansowanych bajtów łącznie, 128 MiB inspekcjonowanych bajtów słowników oraz 4096 jednostek UTF-16 na pełną nazwę pola i 262144 po całym formularzu. Wrogi albo zepsuty plik trafia w budżet i pada z komunikatem błędu, zamiast zamieniać wypełnianie formularza w denial of service
Cała operacja jest transakcyjna. TPdf.SetFormFieldValues uchwytuje snapshot, buduje aktualizację inkrementalną zawierającą znormalizowane obiekty i nowe wartości, ładuje kandydata i podmienia żywy dokument dopiero, gdy kandydat się zweryfikuje. Jeśli normalizacja odrzuci plik albo późniejsze żądanie we wsadzie padnie, oryginalny dokument, jego wartości i logiczny kursor pól zostają dokładnie takie, jakie były
Zapisywanie i weryfikowanie wartości pola direct w Delphi
Bezpieczny wzorzec to zapis, potem odczyt zwrotny przez natywną warstwę formularzy i traktowanie rozbieżności jako porażki, nawet gdy zapis zwrócił True. TPdf.SetFormFieldValue adresuje pole indeksem na bieżącej stronie, bierze typowane TPdfFormValue i wypełnia TPdfFormValueWriteReport; TPdf.FormField[Index] czyta wartość zwrotnie przez warstwę formularzy PDFium, która rozwiązuje drzewo pól, zamiast ufać widgetowi
uses
System.SysUtils, PDFium;
procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
const NewText: WString);
var
Value: TPdfFormValue;
Report: TPdfFormValueWriteReport;
begin
Pdf.PageNumber := PageNo; // SetFormFieldValue celuje w bieżącą stronę
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]);
// Weryfikuj drzewem pól, nie własną zmienną
if Pdf.FormField[FieldIndex] <> NewText then
raise Exception.CreateFmt('Field "%s" did not keep the new value',
[Pdf.FormFieldInfo[FieldIndex].Name]);
end;
Na pliku z nierozwiązywalną własnością, na przykład bezpośrednim widgetcie bez /NM, SetFormFieldValue zwraca False, Report.UpdatedWidgets jest zerowe, a Report.ErrorMessage nazywa problem, jak "Direct widgets require an explicit unique NM identity". To zachowanie, którego chcesz: jasne odrzucenie zamiast cichego pół zapisu. Jeszcze jeden warunek wstępny wart znajomości. Typowany zapis dotyczy wyłącznie adnotacji i nie zdobywa natywnego fokusu, więc gdy natywny edytor pola nadal trzyma fokus, cała transakcja jest odrzucana przed uchwyceniem snapshotu; wołaj Pdf.ClearFormFieldFocus świadomie na początku, zamiast ponawiać w pętli
Dla kilku pól, zwłaszcza między stronami, użyj formy wsadowej. TPdf.SetFormFieldValues przyjmuje od 1 do 256 żądań, aplikuje wszystkie na jednym kandydacie i publikuje wszystko albo nic. TPdfFormValuesWriteReport.FailedRequest mówi ci, które żądanie połamało wsad
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 liczy każdy zsynchronizowany widget
end;
Dwa żądania adresujące to samo pole terminalne są odrzucane jako duplikat właściciela, zamiast pozwolić ostatniemu zapisowi wygrać, a dokument źródłowy jest zachowany. Wartości checkbox i radio podlegają tym samym regułom; podział /V kontra /AS dla przycisków opisuje wartość pola checkbox kontra stan widgeta w PDFium
Dowodzenie, że wartość przeżywa zapis i ponowne otwarcie
Odczyt zwrotny w tej samej sesji dowodzi, że żywy dokument jest spójny. Zapis z ponownym otwarciem dowodzi, że plik jest, a kontrola jest na tyle tania, że warto trzymać ją we własnych testach integracyjnych
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;
Jeszcze dwie pułapki bajtowe: Trim i trailer /ID
Normalizowanie bezpośrednich pól znaczy ponowne czytanie surowych bajtów słowników, co przypomina o dwóch mniejszych pułapkach istotnych dla każdego kodu Delphi dotykającego składni PDF
Pierwsza to SysUtils.Trim na surowych bajtach PDF trzymanych w AnsiString. Literal string jak opcja choice może legalnie zawierać tekst UTF-16BE ze znacznikiem kolejności bajtów FE FF i osadzonymi bajtami NUL. W Unicode Delphi SysUtils.Trim bierze string, więc wywołanie po cichu konwertuje AnsiString na UTF-16 aktywną stroną kodową ANSI i konwertuje wynik z powrotem. Zależnie od tej strony kodowej runda potrafi przepisać wysokie bajty, uszkodzić BOM i nawet ruszyć delimiter idący za nim. Bezpieczne podejście trzyma lexer na bajtach: pomijaj białe znaki PDF pozycyjnie (ISO 32000-1 §7.2.2 wylicza NUL, tab, LF, FF, CR i spację) i tnij przez Copy, dekodując dopiero, gdy wiadomo, że wartość to PDF text string
// Ryzykowne na surowych bajtach PDF: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);
// Bezpieczne dla bajtów: pomijaj białe znaki PDF pozycyjnie, potem tnij
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
Inc(P);
Token := Copy(Token, P, MaxInt);
Druga pułapka siedzi w samej aktualizacji inkrementalnej. ISO 32000-1 §7.5.5 (Table 15) i §14.4 wymagają, by trailer /ID był tablicą dwóch stringów bajtowych. Wewnętrzny helper składający identyfikator rzadkiej aktualizacji zwraca tylko zawartości dwóch stringów, bez klucza /ID i nawiasów tablicy, a trailer musi owinąć to jawnie jako /ID [ ... ]. Pobłażliwy loader PDFium i późniejszy pełny zapis ponownie oba chowają brakujące opakowanie, więc niezawodną kontrolą jest sparsowanie kandydata i zapisanych plików niezależnym surowym parserem i szukanie dwóch stringów po 16 bajtów. Jeśli sam piszesz aktualizacje inkrementalne, ta sama pułapka obowiązuje; artykuł o aktualizacjach inkrementalnych zaszyfrowanych PDF-ów schodzi głębiej w łańcuch trailera
Co zrobić we własnym kodzie formularzy Delphi?
- Zawsze czytaj wartość zwrotnie przez
FormField[Index]po zapisie, a w testach dodatkowo zapisuj, otwieraj ponownie i czytaj jeszcze raz - Traktuj
FalseplusErrorMessagejako informację: odrzucenie nazywająceNM, konflikt rodzica albo duplikat nazwy znaczy, że własność pliku jest faktycznie dwuznaczna - Jeśli generujesz PDF-y, emituj pola i widgety jako obiekty indirect i dawaj każdemu widgetowi unikalne
/NM; to usuwa dwuznaczność u źródła - Używaj
SetFormFieldValuesdo wypełnień wielopolowych i międzystronicowych, żeby późna porażka nie mogła zostawić połowy formularza zaktualizowanej - Nigdy nie puszczaj
Trimani innych helperów RTL dlastringpo surowych bajtach PDF trzymanych wAnsiString
Miej też jasność co do granic. Dokumenty podpisane, formularze XFA, zaszyfrowane snapshoty i pliki, których własność widgetów nie da się rozwiązać jednoznacznie, zostają poza zadeklarowanym profilem i są odrzucane zamiast być przepychane na siłę. Jeśli twój workflow przenosi pola między dokumentami, pytania o własność robią się jeszcze trudniejsze; przeszczepianie pól AcroForm między dokumentami PDF zajmuje się tym przypadkiem
PDFium Component wnosi to typowane, transakcyjne pisanie formularzy do aplikacji Delphi, C++Builder i Lazarus obok renderowania, ekstrakcji tekstu i edycji dokumentów. Historia wersji, licencjonowanie i pobrania są na stronie produktu PDFium Component dla Delphi