Artykuł techniczny

Pola PDF multi-select w obiegach FDF i XFDF (Delphi)

Wartość eksportowa listy złożona z Deep, złamania wiersza i Blue to w pełni legalny PDF, i druga opcja o nazwie Red też. Zaznacz obie, spłaszcz wybór do jednego łańcucha rozdzielanego LF dla pliku wymiany, a druga strona dostanie Deep\nBlue\nRed: dwa wybory albo trzy, i nic w bajtach nie mówi, które. Wartości multi-select wracają z FDF albo XFDF nietknięte tylko wtedy, gdy przez całą drogę pozostają tablicami: od momentu opuszczenia PDF aż do zapisania z powrotem

Dlaczego sklejanie złamaniem wiersza nigdy nie przeniesie wartości multi-select?

Sklejony łańcuch gubi granice między wartościami, a ponieważ wartość może zawierać separator, żaden importer nie potrafi tych granic odtworzyć. ISO 32000-1 §12.7.4.4 pozwala, by wpis /V pola wyboru trzymał pojedynczy łańcuch tekstowy albo tablicę łańcuchów tekstowych; lista z flagą MultiSelect (bit 22 /Ff) przechodzi na formę tablicową, gdy tylko wybrano więcej niż jedną opcję. Ta sama sekcja definiuje /I, tablicę indeksów opcji liczonych od zera w kolejności rosnącej, po której przeglądarka odróżnia dwie opcje dzielące wartość eksportową. Żadna z tych struktur nie przejdzie przez API o typie łańcuchowym. GetFormFieldValue w HotPDF zwraca AnsiString i czyta formę łańcuchową /V, więc każdy eksport zbudowany na nim może zapisać co najwyżej jeden skalar na pole

Strona importu ma lustrzany problem. XFDF zapisuje każdy wybór jako własny element <value>, a importer, który poda je skalarnej metodzie ustawiającej, musi najpierw skleić je w jeden, zwykle znakiem LF. Przy opcji Deep/Blue z góry ten klej jest nieodróżnialny od danych. HotPDF trzyma więc skalary z dala od środka obiegu: ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF i ExportLoadedFormToXFDF czytają tablicę prosto z pola, a pasujące metody importu budują nową tablicę, nigdy nie przechodząc przez sklejony łańcuch

Stara podróż multi-select w HotPDF, w której dwa wybrane opcje listy, jedna z wbudowanym złamaniem wiersza, są spłaszczane przez skalarną ścieżkę GetFormFieldValue do pojedynczego łańcucha Deep, line feed, Blue, line feed, Red, który czytniki downstream mogą sparsować jako dwa wybory albo trzy
Sklejenie wartości multi-select w jeden łańcuch niszczy granice wartości, a wartość eksportowa zawierająca sama w sobie złamanie wiersza czyni spłaszczoną formę wieloznaczną

Co zawierają wyeksportowane pliki FDF i XFDF

Na dysku granice pozostają widoczne. W FDF pole multi-select staje się typowaną tablicą, a każdy element zachowuje pisownię ze źródłowego PDF: łańcuchy szesnastkowe wychodzą jako hex, łańcuchy literalne jako literały z CR i LF escapowanymi jako \r i \n. W pliku pisanym przez ExportLoadedFormToFDF bogaty tekst /RV albo niezerowy lokalny /Ff na polu jedzie w tym samym słowniku pola. W XFDF element korzenia deklaruje xml:space="preserve", czego wymaga ISO 19444-1 i co zamienia każdy biały znak wewnątrz elementu tekstowego w dane. To pułapka dla upiększania wydruku. HotPDF pisze tag otwierający, escapowany tekst i tag zamykający każdego <value> w jednym wierszu, trzyma wcięcia poza elementem i koduje CR, LF i TAB jako odwołania znakowe, żeby parser XML stosujący normalizację końców wiersza nie mógł przepisać pary CR LF na pojedyncze LF

Kształty eksportu, które HotPDF pisze dla listy multi-select od 2.755.0: FDF niesie jedną typowaną tablicę na pole z /V [(Deep line break Blue) (Red)] i szesnastkową wartością regionu, a XFDF niesie jeden element value na wybór pod xml:space preserve, więc biały znak liczy się jako dane
Granice pozostają widoczne na dysku: FDF trzyma każdy wybór jako własny element tablicy, a XFDF zapisuje każdy w osobnym elemencie value, więc żaden importer nie musi zgadywać
<!-- FDF: jedna typowana tablica na pole -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>

<!-- XFDF: jeden <value> na wybór -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
  <fields>
    <field name="options">
      <value>Deep&#xA;Blue</value>
      <value>Red</value>
    </field>
  </fields>
</xfdf>

Przy pisaniu kodu wołającego ważne są dwa przypadki brzegowe eksportu. ExportLoadedFormToFDF składa kompletne ciało FDF w pamięci i dopiero wtedy tworzy plik docelowy, więc wartość, której nie da się wyeksportować (na przykład tablica trzymająca coś innego niż łańcuchy), podnosi wyjątek, podczas gdy poprzedni plik .fdf na dysku jest jeszcze nietknięty. Drugi przypadek to prawdziwa luka samego XFDF: pusty wybór na liście, która oferuje też wartość eksportową będącą pustym łańcuchem. <value/> może znaczyć „nic nie wybrano” albo „wybrano pustą opcję”, a format nie ma trzeciej pisowni. ExportLoadedFormToXFDF podnosi w tej sytuacji wyjątek, zanim plik docelowy zostanie otwarty. FDF takiego problemu nie zna, bo /V [] i /V [()] to różne obiekty. Wszystkie trzy eksportery pomijają terminaly zawierające wyłącznie widget bez nazwy /T, bo żaden importer nie dopasowałby takiego wpisu z powrotem do pola

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    begin
      // Listy multi-select są zapisywane jako /V [(...) (...)]
      Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
      try
        Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
      except
        on E: Exception do
          // Pusty wybór plus pusta opcja eksportowa: XFDF nie odróżni
          // ich, a istniejący plik .xfdf zostaje nietknięty
          ShowMessage('XFDF export refused: ' + E.Message);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Jak importowany wybór jest sprawdzany względem /Opt?

HotPDF przyjmuje przychodzącą tablicę tylko wtedy, gdy celem jest pole wyboru z ustawioną flagą MultiSelect i każda wartość pasuje do wartości eksportowej z tablicy /Opt pola (dla dwuelementowej pary opcji: pierwszy element). Każde gniazdo opcji może być użyte raz. Lista z dwiema opcjami eksportującymi obie b przyjmuje więc [<62> <62>] jako dwa różne wybory i odrzuca trzecie b, bo nie zostało dla niego wolne gniazdo. Odbudowane /I idzie w kolejności /Opt, niezależnie od kolejności, w jakiej przyszły wartości, co utrzymuje indeksy rosnące, jak wymaga §12.7.4.4. Nowe /V i /I powstają jako odseparowane obiekty i są przypisywane dopiero, gdy każda wartość przejdzie, więc odrzucona wartość nie zostawi na polu połowy tablicy ani nieaktualnych indeksów

Po przypisaniu pole dostaje świeży strumień wyglądu. Ten krok łatwo pominąć we własnoręcznie napisanym importerze, a objaw myli: przeglądarki rysują zapisane /AP, a nie /V, więc stary wybór zostaje na ekranie i w druku, mimo że dane się zmieniły. Lista wchodzi też do zbioru zmienionych pól, czyli wywołanie RecalculateLoadedFormFieldsIncremental bez parametrów przelicza dokładnie te obliczenia, które ją czytają. Pisownie hex przychodzące z FDF zostają hexem aż po zapis. Jeśli potrzebujesz ustawić tylko jedną wartość skalarną, ustawianie jednej wartości pola formularza we wczytanym PDF opisuje tamtą ścieżkę, która z założenia nie zajmuje się wielokrotnymi wyborami

Walidacja importu wartości multi-select w HotPDF: cel musi być polem wyboru z MultiSelect ustawionym w /Ff, każda przychodząca wartość musi pasować do eksportowej wartości /Opt przy każdym gnieździe użytym raz, /I jest odbudowywane rosnąco w kolejności /Opt, a odseparowane /V i /I są przypisywane dopiero, gdy wszystkie wartości przejdą
Każda przychodząca wartość jest sprawdzana względem opcji pola, zanim cokolwiek zostanie zapisane, więc odrzucona wartość nigdy nie zostawia połowy tablicy ani nieaktualnych indeksów wyboru

Jeden wzorzec wejścia zasługuje na specjalną obsługę. PDF może trzymać zwykłą ASCII-ową wartość eksportową jako łańcuch szesnastkowy bez znacznika kolejności bajtów, <416272> dla Abr, a eksporter kopiujący ciało łańcucha PDF dosłownie wyprodukuje XFDF, którego tekst elementu to surowe cyfry 416272, a nie zdekodowany tekst. XFDF niosący szesnastkowo zakodowaną wartość eksportową jako surowe cyfry nie dopasuje /Opt po pisowni: ścisłe porównanie nie znajduje opcji o nazwie 416272 i import pada. Gdy importer XFDF w HotPDF dostaje taki plik, ponawia niedopasowaną wartość raz, dekodując ją jako ładunek szesnastkowy. Ponowienie jest celowo wąskie. Działa tylko dla wartości bez żadnego dokładnego dopasowania; każde dokładne dopasowanie jest rezerwowane najpierw, więc ocalona pisownia nigdy nie zajmie gniazda opcji, którego potrzebuje późniejsza wartość zapisana dokładnie; a zapisany element /V to własny łańcuch PDF opcji, więc /V i /I opisują ten sam wybór. To właściwa dawka pobłażliwości: odpala się tylko tam, gdzie import inaczej by padł, a wartość już dopasowana nigdy nie jest reinterpretowana. Samo dopasowanie przechodzi przez jeden dekoder Unicode rozumiejący PDFDocEncoding, UTF-16 z dowolnym znacznikiem kolejności bajtów i UTF-8, więc ten sam logiczny tekst dopasuje tę samą opcję nawet w dokumentach mieszających kodowania

Dwie pułapki parsowania FDF: terminatory hex i obce numery obiektów

Pierwsza pułapka jest leksykalna. W << /T (region) /V <416273>>> pierwszy > zamyka łańcuch szesnastkowy, a dwa kolejne zamykają słownik. Skaner, który szuka wyłącznie par >>, kończy słownik o znak za wcześnie i gubi pole. Importer FDF na poziomie pliku w HotPDF śledzi, czy jest wewnątrz łańcucha szesnastkowego, czy literału, podczas gdy kroi tablicę /Fields, a strumieniowy czytnik za ImportLoadedInterchangeFromFDF tokenizuje cały plik jako składnię PDF, więc oba widzą trzy nawiasy zamykające tam, gdzie naiwny skan widzi dwa

Druga pułapka dotyczy referencji. Plik FDF to mały dokument w składni PDF z własną numeracją obiektów (ISO 32000-1 §12.7.7), więc /V [11 0 R] oznacza obiekt 11 pliku FDF, nigdy obiekt 11 wypełnianego PDF. Rozwiązanie go względem dokumentu docelowego wciągnęłoby po cichu, czymkolwiek akurat jest tam obiekt o tym numerze. W buildach Delphi 2009 i nowszych czytnik wymiany dekoduje FDF jako kompletny graf obiektów, podąża za referencją wewnątrz tego grafu, waliduje wynikowe łańcuchy jak każdą inline'ową tablicę i podnosi wyjątek, gdy FDF nie zawiera obiektu 11. ImportLoadedFormFromFDF na poziomie pliku czyta każdy słownik pola samodzielnie, bez tabeli obiektów FDF; tablica referencji nie przechodzi jego kontroli tylko-łańcuchy i to pole jest pomijane

Wybór trasy importu: pomiń, podnieś wyjątek albo wycofaj

Wszystkie trzy trasy importu walidują wartości multi-select tak samo. Różnią się tym, co dzieje się, gdy wartość pada. ImportLoadedFormFromFDF pomija każde pole, które nie przechodzi, i zwraca liczbę pól, które faktycznie zastosował, więc licznik niższy od liczby wysłanych wpisów to jedyny znak kłopotu. ImportLoadedFormFromXFDF i ImportLoadedInterchangeFromFDF podnoszą wyjątek przy pierwszym odrzuconym polu, a każde wywołanie biegnie jako jedna transakcja na wczytanym dokumencie: gdy wyjątek opuszcza metodę, każde pole (a w trasie wymiany każda adnotacja), które wywołanie zdążyło zmienić, jest wycofywane. Import wymiany zwracający False też jest wycofywany, z wyczyszczonymi Status i rekordem informacyjnym

Dla danych, które ktoś przejrzał i odesłał, użyj jednej z tras podnoszących wyjątek. Formularz zamówienia zastosowany w połowie jest gorszy niż odrzucony, bo nic w zapisanym PDF nie pokazuje, które listy przyszły z przejrzanego pliku, a które zachowały stare wartości. Zachowanie pomijaj-i-licz w ImportLoadedFormFromFDF pasuje do innego zawodu: przybliżone wypełnienie wstępne z FDF, które mogło powstać dla lekko innej rewizji formularza

var
  Pdf: THotPDF;
  Source: TMemoryStream;
  Status: AnsiString;
  Info: THPDFFDFInterchangeInfo;
begin
  Pdf := THotPDF.Create(nil);
  Source := TMemoryStream.Create;
  try
    Source.LoadFromFile('order-form-reviewed.fdf');
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    try
      // Tylko pola. Wartość poza /Opt albo kilka wartości wymierzonych w
      // listę single-select podnosi wyjątek i wycofuje cały import
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
      begin
        // Przelicz ponownie tylko te obliczenia, które czytają importowane pola
        Pdf.RecalculateLoadedFormFieldsIncremental;
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
      end;
    except
      on E: Exception do
        ShowMessage('Import rejected, document unchanged: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

Podpinanie własnego magazynu danych do tablicowych callbacków XFDF

Jednostka HPDFXFDF, która konwertuje między XFDF a dowolnym magazynem pól opisywalnym callbackami, niesie obsługę tablic w osobnym rekordzie THPDFXFDFArrayAccess i w dodatkowych przeciążeniach HPDFXFDFExportFields oraz HPDFXFDFImportFields. Doklejenie nowych gniazd do istniejącego rekordu THPDFXFDFAccess byłoby krótsze i błędne. Wołający często deklarują ten rekord jako zmienną lokalną i wypełniają tylko gniazda, które znają, więc nowy wskaźnik funkcji na końcu trzymałby śmieci ze stosu, które biblioteka wzięłaby za callback. Przy osobnym rekordzie istniejący kod zachowuje układ i przeciążenia, a te przeciążenia przekazują wewnętrznie rekord tablicowy samych nili

Dwa przeciążenia importu zachowują się inaczej przy powtórzonych elementach <value>. Pierwotne skalarne przeciążenie wciąż skleja je znakiem LF i woła SetFormFieldValue, bo istniejący wołający polegają na tej konwencji. Przeciążenie świadome tablic nigdy nie skleja: podaje listę do SetFormFieldValueArray, cofa się do metody skalarnej tylko dla pojedynczej wartości, której twój callback odmówi, i podnosi wyjątek, gdy callback odmówi listy dwóch lub więcej. Przy eksporcie zacznij od Default(THPDFXFDFArrayAccess), żeby gniazda, których nie przypiszesz (włącznie z opcjonalnym SetFormFieldRV), zostały nilami. Zwracaj True z GetFormFieldValueArray dla każdego pola o wartości listowej, łącznie z takim, w którym nic nie wybrano, a False, by pole obsłużył callback skalarny. Pusta tablica wychodzi jako pojedynczy pusty <value/>, więc magazyn oferujący opcję pustego łańcucha powinien odrzucić ten przypadek wewnątrz callbacka, tak samo jak robi to ExportLoadedFormToXFDF

uses HPDFXFDF;

// Zwykły wskaźnik funkcji, nie „of object”: Context niesie twój własny magazyn.
// TFormStore i jego metody to twój kod, nie część HotPDF
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
  out Values: THPDFXFDFValueArray): Boolean;
begin
  Result := TFormStore(Context).IsListField(FieldIndex);
  if Result then
    Values := TFormStore(Context).Selections(FieldIndex);
end;

procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
  Access: THPDFXFDFAccess;
  ArrayAccess: THPDFXFDFArrayAccess;
begin
  Access := MakeStoreAccess(Store);             // twoje istniejące powiązania skalarne
  ArrayAccess := Default(THPDFXFDFArrayAccess); // każde nieużywane gniazdo to nil
  ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
  HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;

Całe powyższe zakłada listy, które już istnieją i mają już ustawiony bit MultiSelect w /Ff. Tworzeniem pól wyboru i ustawianiem ich bitów flag zajmuje się artykuł o dodawaniu ListBox i innych pól AcroForm do wczytanego PDF, a znacznikami komentarzy podróżującymi przez część <annots> XFDF zajmuje się artykuł o imporcie i eksporcie adnotacji XFDF w HotPDF. Referencja metod dla każdego tu pokazanego wywołania, plus build próbny do przetestowania na własnych formularzach, są na stronie komponentu HotPDF dla Delphi