Artykuł techniczny

Ustawianie wartości pól formularza w PDF z HotPDF i Delphi

Na liście pól prawie każdego czytnika PDF pole ulicy na niemieckojęzycznym formularzu zgłoszeniowym czyta Straße. Bajty za jego wpisem /T opowiadają inną historię: <FEFF005300740072006100DF0065>, szesnastkowy napis UTF-16BE ze znacznikiem kolejności bajtów z przodu. Kod, który dopasowuje nazwy pól do tych surowych bajtów, zgłasza pole jako brakujące, choć użytkownik może w nie kliknąć bezpośrednio. Ten sam formularz może trzymać stan włączenia checkboxa jako 2 zamiast Yes, a jego listę krajów jako pary [export display], co daje trzy osobne sposoby na to, żeby wartość, którą zapiszesz, i wartość, którą maluje czytnik, się rozjechały

Wszystkie trzy wariacje pochodzą z tego samego miejsca: formularz zbudowany lata temu w czym tam narzędziu autorskim miało klient, takim, który twoja aplikacja Delphi musi teraz wypełnić z bazy danych i oddać jako plik renderujący się tak samo w każdym czytniku. Nie możesz przepisać formularza od nowa, więc kod wypełniający musi iść za regułami, pod którymi formularz został napisany. ISO 32000-1 ma regułę dla każdego z trzech przypadków: §7.9.2.2 dla napisów tekstowych, §12.7.4.2.3 dla stanów wyglądu przycisków, tabela 231 w §12.7.4.4 dla wyborów w polach choice. Tworzenie pól, które jeszcze nie istnieją, to inna robota — opisuje ją artykuł dodawanie pól AcroForm do wczytanego PDF w Delphi

Znajdowanie pola: HotPDF dekoduje /T, zanim dopasuje nazwy

THotPDF.SetFormFieldValue ma dwa przeciążenia — jedno bierze liczony od zera indeks pola, drugie pełną nazwę kwalifikowaną — i oba rozwiązują się względem tabeli nazw, którą komponent buduje z drzewa AcroForm przy pierwszym użyciu. Gdy tabela powstaje, każda nazwa częściowa przechodzi przez dekoder. Szesnastkowy napis /T jest zamieniany z powrotem na bajty; jeśli te bajty zaczynają się od FE FF i mają parzystą długość, reszta jest czytana jako UTF-16BE i rekodowana na UTF-8. Każda zdekodowana nazwa częściowa jest potem sklejana z rodzicem kropką, dokładnie jak opisuje §12.7.3.1, więc pole dziecka o nazwie City pod rodzicem o nazwie Address rejestruje się jako Address.City. Dekodowany jest tylko klucz wyszukiwania. Obiekt /T w pliku zachowuje swój zapis heksowy, a zapis dokumentu nie dotyka tożsamości pola, które jedynie wypełniłeś

Klucze są zapisywane małymi literami, zanim trafią do tabeli, więc 'address.city' znajduje Address.City. To wykracza poza standard, który traktuje nazwy pól jako rozróżniające wielkość liter, i znaczy, że dwa pola różniące się tylko wielkością liter zderzyłyby się w wyszukiwaniu. Dla kodu czytającego nazwy pól z pliku konfiguracyjnego, który ktoś wpisywał ręcznie, opuszczona wielka litera to dużo częstsza porażka niż dwa pola w jednym formularzu różniące się wyłącznie wielkością liter

Jak HotPDF rozwiązuje nazwy AcroForm spoza ASCII: szesnastkowy napis /T jest zamieniany z powrotem na bajty, znacznik kolejności bajtów FE FF jest dekodowany jako UTF-16BE i rekodowany na UTF-8, a nazwa kwalifikowana skleja się z rodzicem, więc Applicant.FullName i pole o nazwie Straße lądują oba w tabeli wyszukiwania
Zmienia się tylko klucz w cache: słownik pola zachowuje swoje szesnastkowe kodowanie, wyszukiwania normalizują do małych liter jako wygoda ponad standard, a zapis dokumentu nigdy nie przepisuje tożsamości pola, które tylko wypełniłeś

Zdekodowane nazwy to bajty UTF-8 niesione w AnsiString i właśnie tam konwersje napisów samego Delphi stają na drodze. Literal taki jak 'Applicant.Straße' przekazany do parametru AnsiString jest konwertowany przez systemową stronę kodową ANSI, więc ß przychodzi jako pojedynczy bajt DF zamiast pary UTF-8 C3 9F i wyszukiwanie mija. Nazwy ASCII nie cierpią na tym. Dla czegokolwiek innego wylicz pola raz, zdekoduj każdą nazwę przez UTF8ToString i zapisuj przez przeciążenie indeksowe:

// FindFieldIndex to pomocnik, który dodajesz sam: porównuje
// zdekodowane nazwy UTF-8 z napisem Unicode, więc żadna strona
// kodowa ANSI nie siedzi między twoim literałem a wyszukiwaniem
function FindFieldIndex(Pdf: THotPDF; const Name: string): Integer;
var
  I: Integer;
begin
  for I := 0 to Pdf.GetFormFieldCount - 1 do
    if SameText(UTF8ToString(Pdf.GetFormFieldName(I)), Name) then
      Exit(I);
  Result := -1;
end;

var
  Pdf: THotPDF;
  Street: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Nazwy ASCII mogą iść wprost do przeciążenia po nazwie
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');

    Street := FindFieldIndex(Pdf, 'Applicant.Straße');
    // Wartość też jest AnsiString, więc literal 'ß' zależałby od
    // systemowej strony kodowej; szesnastkowy FEFF (Hauptstraße 12) omija to
    if Street >= 0 then
      Pdf.SetFormFieldValue(Street,
        'FEFF00480061007500700074007300740072006100DF0065002000310032');

    // Ten sam szesnastkowy UTF-16BE z przedrostkiem FEFF działa z
    // przeciążeniem po nazwie (to jest München); zapisuje się jako napis heksowy
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

Co SetFormFieldValue zapisuje, krok po kroku

Oba przeciążenia po znalezieniu pola biegną po tej samej sekwencji: zapisać /V, przebudować indeks wyboru choice, oznaczyć pole jako zmienione, przełączyć stany wyglądu przycisków, przegenerować wygląd tekstu albo wyboru i wpisać pole do zbioru brudnych. Ten ostatni krok zasila bezparametrowe RecalculateLoadedFormFieldsIncremental, które ponownie uruchamia wyłącznie skrypty obliczeń czytające zmienione pole — bezpośrednio albo przez łańcuch taki jak pozycja faktury, suma częściowa, suma całkowita

Typ obiektu nowego /V zależy od pola. Dla checkboxa albo przycisku radio SetFormFieldValue zawsze zapisuje obiekt nazwy, bo §12.7.4.2.3 przechowuje wybrany stan jako nazwę, a nazwy PDF to gołe bajty bez kodowania napisów. Każde inne pole dostaje obiekt napisu, a przekazana wartość jest najpierw oglądana. Napis zaczynający się od FEFF, o parzystej długości i złożony wyłącznie z cyfr szesnastkowych jest brany za postać UTF-16BE z §7.9.2.2 i serializowany jako <FEFF...>. Na tym opiera się linia City wyżej. Cokolwiek innego jest zapisywane jako napis literalny z dokładnie takimi bajtami, jakie podałeś, więc zwykły tekst łaciński wchodzi jako zwykły tekst

Dlaczego checkbox zachowuje stary znacznik po zmianie wartości?

Dla pola przycisku wartość nie rozstrzyga, co jest rysowane. Widget checkboxa niesie wpis /AS, który nazywa, który strumień w /AP /N jest akurat pokazywany, a czytniki malują z /AS, nie z /V. Zmień /V na Yes, podczas gdy /AS nadal mówi Off, i plik zaprzecza sam sobie; spłaszczacz wpiecze niezaznaczony kwadracik w stronę, podczas gdy dane formularza twierdzą, że jest zaznaczony. SetFormFieldValue odwiedza więc pole przycisku i każdy wpis w jego tablicy /Kids, czyta nazwę stanu włączenia z /AP /N każdego widgetu i ustawia /AS na tę nazwę, gdy pasuje do wartości pola, albo na Off, gdy nie pasuje

Dlaczego checkbox HotPDF zachowuje stary znacznik, gdy zmienia się tylko /V: czytniki malują ze stanu wyglądu /AS do /AP /N, więc SetFormFieldValue odwiedza pole i każde dziecko, czyta nazwę stanu włączenia jako pierwszy klucz inny niż Off i przepisuje /AS przy dopasowaniu albo na Off
Grupy radio porównują każde dziecko z wartością odziedziczoną z pola rodzica przez łańcuch /Parent, więc ustawienie grupy na jedną wartość eksportową włącza dokładnie ten widget i wyłącza każdego sąsiada

Prawdziwe formularze wyginają tę regułę na dwa sposoby. Wygląd wyłączenia zwie się w standardzie Off, ale narzędzia autorskie często zostawiają jego strumień w ogóle poza słownikiem i pozwalają czytnikowi nie rysować niczego, więc słownik wyglądu normalnego z pojedynczym wpisem jest całkowicie legalny. Nazwa stanu włączenia to cokolwiek wybrał autor: Yes, On, 2, słowo w języku formularza. Stan włączenia jest więc brany jako pierwszy klucz w /AP /N, który nie jest Off, a porównanie z twoją wartością ignoruje wielkość liter. Przyciski radio dokładają układ z §12.7.4.2.4: wybrana wartość mieszka w /V na polu rodzica, podczas gdy każdy widget dziecka zwykle nie ma własnego /V. Każde dziecko jest porównywane z wartością odziedziczoną przez jego łańcuch /Parent

// Checkbox: wartość eksportowa musi pasować do klucza stanu włączenia w /AP /N
// (często 'Yes', ale prawdziwe formularze używają '2', 'On' albo czegokolwiek innego)
Pdf.SetFormFieldValue('Consent', 'Yes');

// Grupa radio: /V jest zapisywane na rodzicu; każdy widget dziecka dostaje
// /AS ustawione na swoją nazwę eksportową albo na Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Wyczyszczenie checkboxa: każda wartość, która nie pasuje do żadnego stanu włączenia, daje /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Drugą stroną medalu jest to, że literówka nie produkuje wyjątku. 'yes ' ze spacją na końcu nie pasuje do niczego, więc kwadracik kończy jako Off, a plik jest nadal idealnie poprawny. Jeśli twoje wartości pochodzą z kolumny bazy danych, przytnij je przed wywołaniem

Pola wyboru: trzymanie /I w kroku z /V

Pole kombi albo lista zapisuje swój wybór w dwóch miejscach. Obok /V tabela 231 definiuje /I jako tablicę liczonych od zera indeksów w /Opt, a czytnik, który znajdzie /I wskazujące opcję 0, podczas gdy /V wymienia opcję 3, może podświetlić niewłaściwy wiersz. SetFormFieldValue przebudowuje /I przy każdym wywołaniu na polu choice. Zanim cokolwiek zrobi, usuwa własny wpis /I pola i nigdy nie edytuje starej tablicy w miejscu: tablica pośrednia może być współdzielona z innym polem, a jej mutacja zmieniłaby też wybór tamtego pola. Potem rozwiązuje /Opt przez łańcuch /Parent, bo opcje mogą być dziedziczone, i obchodzi wpisy. Goła opcja napisowa jest porównywana wprost, para [export display] po swoim elemencie eksportowym, a para z mniej niż dwoma elementami jest pomijana

Jak HotPDF trzyma pole wyboru w spójności: SetFormFieldValue usuwa lokalną tablicę /I, zanim ją dotknie, rozwiązuje /Opt przez łańcuch /Parent, porównuje eksportową połowę każdej opcji jako zdekodowany tekst, zapisuje jednoelementowe /I przy pierwszym dopasowaniu i nie zapisuje niczego, gdy wartość edytowalnego kombi nie ma indeksu
Goła opcja napisowa jest porównywana wprost, a para export display po swoim elemencie eksportowym, natomiast wartość spoza /Opt słusznie nie zostawia żadnego indeksu — nieaktualne /I wskazujące niewłaściwy wiersz byłoby gorsze niż żadne

Porównanie biegnie po zdekodowanym tekście, więc opcja przechowywana w PDFDocEncoding pasuje do wartości zapisanej jako szesnastkowy UTF-16BE i odwrotnie. Wartość skalarna produkuje co najwyżej jeden indeks; jakikolwiek wielokrotny wybór pole trzymało przedtem, zostaje zastąpiony. Gdy nic nie pasuje, żadne /I nie jest zapisywane. Dla edytowalnego pola kombi to poprawny wynik, bo §12.7.4.4 pozwala użytkownikowi wpisać wartość spoza listy, a taka wartość nie ma indeksu, na który można wskazać

Ta sama reguła stoi za najczęstszą skargą na pola kombi: wywołanie się udaje, /V trzyma nowy tekst, a czytnik nie pokazuje nic wybranego. Zwykle wołający podał etykietę wyświetlania (Canada) do listy zbudowanej z par [CA Canada]. Komponent nie może traktować tego jako błędu, nie łamiąc edytowalnych kombi, więc kontrola należy do twojego kodu. GetFormFieldOptionValue zwraca element eksportowy każdej opcji, co robi strażnika krótkim:

// SetChoiceByExport to twój własny strażnik, nie część komponentu:
// odmawia wartości, które nie pasują do żadnego elementu eksportowego w /Opt
procedure SetChoiceByExport(Pdf: THotPDF; const FieldName,
  ExportValue: AnsiString);
var
  I: Integer;
begin
  for I := 0 to Pdf.GetFormFieldOptionCount(FieldName) - 1 do
    if Pdf.GetFormFieldOptionValue(FieldName, I) = ExportValue then
    begin
      // /Opt to [[US United States] [CA Canada] [MX Mexico]]:
      // 'CA' pasuje do elementu eksportowego, a /I staje się [1]
      Pdf.SetFormFieldValue(FieldName, ExportValue);
      Exit;
    end;
  raise Exception.CreateFmt('%s is not an export value of %s',
    [string(ExportValue), string(FieldName)]);
end;

// Edytowalne kombi z wolnym tekstem: zapisz go wprost. /V jest ustawiane,
// /I jest usuwane, a żaden indeks nie jest wymyślany
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Czy nowa wartość pokaże się bez /NeedAppearances?

Tak, dla pól tekstowych i wyboru. Czytnik maluje przechowywany strumień /AP i nie patrzy na /V, więc nowa wartość ze starym wyglądem zostawia stary tekst na ekranie i w druku, chyba że słownik AcroForm niesie /NeedAppearances true (§12.7.3.3) i czytnik zdecyduje się to uszanować. Spłaszczacze i generatory miniatur często tę flagę ignorują. SetFormFieldValue przegenerowuje strumień wyglądu dla pól tekstowych i wyboru w ramach wywołania, używając tego samego buildera co publiczne EnsureLoadedFieldAppearanceStream. Strumień jest budowany z odziedziczonego napisu /DA, wyrównania /Q, /MaxLen dla pól comb i samej wartości. Font nazwany w /DA jest rozwiązywany przez zasoby /DR AcroForm, więc font Type0 zachowuje własne glify zamiast spadać do Helvetica. Przyciski trzymają stany wyglądu namalowane przez swojego projektanta; dla nich rusza się tylko /AS

SetFormFieldValue jest procedurą i nie mówi ci, czy cokolwiek zostało namalowane. Widget bez użytecznego /Rect nie dostaje strumienia, a połamane formularze z osieroconymi albo bezprostokątnymi widgetami istnieją naprawdę. Gdy wynik renderowania ma znaczenie, zawołaj po ustawieniu wartości EnsureLoadedFieldAppearanceStream sam. Zwraca True, gdy co najmniej jeden widget dostał strumień. GetFormField podaje ci THPDFLoadedFormField, którego właściwość Index jest tym, czego wywołanie oczekuje; ten obiekt jest twój i musisz go zwolnić

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // False znaczy, że żaden widget tego pola nie miał prostokąta,
    // do którego można malować; wartość jest zapisana, ale nic jej nie pokaże
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

Sprawdzenie, że /V się zmieniło, dowodzi bardzo małego o tym, co zobaczy użytkownik. Test, który coś znaczy, renderuje stronę przed i po oraz potwierdza, że piksele wewnątrz prostokąta widgetu się zmieniły, a piksele poza nim nie. Trzymamy tę kontrolę, bo same bajty wyglądu już nas kiedyś zmyliły: strumień trzymał każdy właściwy bajt, podczas gdy czytnik uszanował /NeedAppearances true, przebudował wyświetlanie z /V i nie pokazał niczego z tego

Czytanie wartości z powrotem i co zwykle przychodzi potem

GetFormFieldValue zwraca odziedziczone /V pola. Wartość zapisana jako szesnastkowy UTF-16BE z przedrostkiem FEFF wraca zdekodowana do UTF-8, więc pole City wyżej czyta się z powrotem jako bajty UTF-8 München, a UTF8ToString zamienia je w napis Delphi. Napis literalny wraca z bajtami, z którymi był zapisany. Przy kontrolach round trip porównuj zdekodowany tekst po obu stronach, a nie pisownię, którą przekazałeś

Przenoszenie danych pól hurtowo między formularzem a systemem zewnętrznym, zamiast jednego wywołania na pole, to temat artykułu import i eksport XFDF w Delphi. Zablokowanie gotowego formularza, żeby nikt go nie edytował, to spłaszczanie pól AcroForm i XFA w Delphi, które zamienia dokładnie te stany /AS i strumienie wyglądu opisane tutaj w statyczną treść strony. Cokolwiek jest w tym momencie niespójne, pozostaje niespójne na zawsze, więc wypełnij, sprawdź, potem spłaszcz

SetFormFieldValue, EnsureLoadedFieldAppearanceStream i API przeliczeń przyrostowych są częścią HotPDF Delphi Component, obok wywołań, które przemianowują, usuwają i spłaszczają pola w dokumencie wczytanym z Delphi albo C++Buildera