Artykuł techniczny

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

HotPDF przepuszcza wartości list multi-select w obie strony przez FDF i XFDF, trzymając wartość pola jako tablicę od początku do końca. Od wersji 2.755.0 ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF i ExportLoadedFormToXFDF zapisują każdą wybraną opcję jako własny łańcuch FDF albo element <value> w XFDF, a pasujące metody importu sprawdzają każdą wartość względem opcji pola i odbudowują indeksy wyboru /I, zanim zmienią cokolwiek. Nic nie jest po drodze sklejane w jeden łańcuch

Ta naprawiona porażka jest łatwa do odtworzenia. Weź formularz zamówienia z listą multi-select opcji produktowych, daj użytkownikowi wybrać dwie z nich, wyeksportuj dane formularza dla systemu back-office, a potem zaimportuj zedytowany plik z powrotem do PDF. Przed tą zmianą lista wracała pusta albo zła. Powód: jedna z wartości eksportowych zawierała złamanie wiersza, a stara ścieżka spłaszczyła wybory do pojedynczego łańcucha rozdzielanego wierszami. Wyciągnięcie wielu wyborów z takiego łańcucha nigdy nie było niezawodne, a przy wartości eksportowej zawierającej samo w sobie złamanie wiersza to w ogóle nie może działać

Dlaczego sklejanie wartości multi-select złamaniami wiersza psuje podróż w obie strony?

Sklejenie wyborów w jeden łańcuch wyrzuca granice między wartościami, a wartość może zawierać separator, więc żaden importer nie umie łańcucha poprawnie rozpleść. ISO 32000-1 §12.7.4.4 pozwala, by wpis /V pola wyboru był pojedynczym łańcuchem tekstowym albo tablicą łańcuchów tekstowych, a lista z flagą MultiSelect (bit 22 /Ff) używa formy tablicowej, gdy wybrano więcej niż jedną opcję. Ta sama sekcja definiuje /I jako tablicę liczonych od zera indeksów opcji w kolejności rosnącej, której przeglądarki używają, by odróżnić dwie opcje, które akurat dzielą wartość eksportową. W HotPDF skalarne GetFormFieldValue czyta tylko formę łańcuchową, więc przepuszczenie tablicy przez niego degradowało eksport do pustego łańcucha, a stary import XFDF sklejał powtórzone elementy <value> znakiem LF. Wyobraź sobie opcję wyeksportowaną jako Deep, line feed, Blue: po sklejeniu Deep\nBlue\nRed może być dwoma wyborami albo trzema, a plik nie daje sposobu, by wiedzieć, którym. Poprawka polegała na całkowitym odstawieniu skalara w środku podróży w obie strony

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?

HotPDF zapisuje wartość multi-select jako typowaną tablicę w FDF i jako jeden element <value> na wybór w XFDF, więc granice pozostają widoczne na dysku. W FDF każdy element zachowuje pisownię ze źródłowego PDF: łańcuchy szesnastkowe wychodzą jako hex, a łańcuchy literalne są escapowane przez jeden helper zamieniający CR i LF na \r i \n. W XFDF korzeń niesie xml:space="preserve", jak wymaga ISO 19444-1, co znaczy, że biały znak wewnątrz elementu tekstowego liczy się jako dane. HotPDF pisze więc tag otwierający, escapowany tekst i tag zamykający każdego <value> w jednym kawałku, 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ł zmienić oryginalnych bajtów

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>

Dwa przypadki brzegowe eksportu warte poznania, zanim napiszesz kod wołający. Po pierwsze, ExportLoadedFormToFDF buduje kompletne ciało FDF w pamięci, zanim utworzy plik docelowy (naprawione w 2.755.1), więc wartość, której nie da się wyeksportować, jak tablica trzymająca coś innego niż łańcuchy, rzuca bez ucinania istniejącego pliku. Po drugie, pusty wybór na liście, która oferuje też wartość eksportową będącą pustym łańcuchem, jest wieloznaczny w XFDF, bo <value/> może znaleźć „nic nie wybrano” albo „wybrano pustą opcję”. ExportLoadedFormToXFDF rzuca w tym przypadku, zamiast zgadywać, i rzuca, zanim plik docelowy zostanie otwarty. FDF takiej wieloznaczności nie ma, bo /V [] i /V [()] są różne. Oba eksportery FDF pomijają też terminaly samych widgetów bez nazwy /T, na równi z eksporterem XFDF, bo żaden importer nie umiałby dopasować tych wpisów 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 HotPDF waliduje wartość multi-select przy imporcie?

HotPDF przyjmuje importowaną tablicę tylko wtedy, gdy celem jest pole wyboru z ustawioną flagą MultiSelect i każda wartość w tablicy pasuje do wartości eksportowej z tablicy /Opt pola. Każde gniazdo opcji może być użyte raz, więc lista z dwiema opcjami dzielącymi wartość eksportową b przyjmuje [<62> <62>] jako dwa różne wybory i odrzuca trzecie b. Odbudowane /I idzie w kolejności /Opt, a nie w kolejności przychodzących wartości, bo §12.7.4.4 wymaga indeksów rosnących. HotPDF buduje nowe /V i /I jako odseparowane obiekty i przypisuje je dopiero, gdy każda wartość przejdzie walidację, więc odrzucona wartość nigdy nie zostawi połowy tablicy ani nieaktualnych indeksów. Kopia jest zapisywana do pola, które jest importowane, a nie do współdzielonej macierzystej tablicy, szesnastkowe pisownie przychodzące z FDF zostają hexem przez zapis, a pola, których obliczenia zależą od listy, są znacznikowane do przeliczenia. Jeśli potrzebujesz ustawić pojedynczą wartość, ustawianie jednej wartości pola formularza we wczytanym PDF idzie ścieżką skalarną, która z założenia nie obsługuje wielu wyborów

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

Część innych narzędzi zapisuje zwykłe ASCII-owe wartości eksportowe jako łańcuchy szesnastkowe bez znacznika kolejności bajtów, na przykład <416272>, a potem eksportuje XFDF, wypisując te cyfry szesnastkowe jako tekst. Rygorystyczne porównanie literalne w drodze powrotnej pada, a import przerywa. Wersja 2.755.1 dokłada jedno ponowienie: gdy wartość nie pasuje do żadnej opcji, HPDFHexSpellingText dekoduje tekst jako ładunek szesnastkowy i porównuje wynik jeszcze raz. Ponowienie dotyczy tylko wejścia, które inaczej by rzuciło, więc nigdy nie zmienia wartości, która już pasowała. To samo wydanie sprawiło też, że ścieżki skalarne i tablicowe używają tego samego dekodera Unicode, który rozumie PDFDocEncoding, UTF-16 z dowolnym znacznikiem kolejności bajtów i UTF-8. Wcześniej jedna logiczna wartość mogła pasować na jednej ścieżce i padać na drugiej w dokumentach mieszających kodowania

Dlaczego poprawny plik FDF wciąż może tracić pola przy parsowaniu?

Skaner FDF, który nie śledzi łańcuchów szesnastkowych, może przeciąć słownik pola na pół, gdy wartość hex kończy się tuż przy terminatorze słownika. W << /T (region) /V <416273>>> pierwszy > zamyka łańcuch szesnastkowy, ale naiwny skaner czyta go razem z następnym > jako koniec słownika i po cichu gubi pole. Importer FDF na poziomie pliku już prowadził ewidencję, czy jest wewnątrz łańcucha szesnastkowego, a w 2.755.1 robią to samo skanery tablic i słowników za ImportLoadedInterchangeFromFDF. Druga sprawa dotyczy referencji pośrednich. Plik FDF to mały dokument w składni PDF z własną numeracją obiektów (ISO 32000-1 §12.7.7), więc wartość taka jak /V [11 0 R] wskazuje na obiekt 11 pliku FDF, a nie obiekt 11 PDF, który wypełniasz. Uproszczony parser FDF w HotPDF nie rozwiązuje referencji wewnątrz pliku, więc taką tablicę odrzuca, zamiast czytać, czym akurat jest obiekt 11 w dokumencie docelowym

Importy z pliku, strumienia i XFDF raportują błędy inaczej

Trzy ścieżki importu walidują tak samo, ale raportują porażki inaczej i warto wybrać jedną z premedytacją. ImportLoadedFormFromFDF pomija każde pole, które nie przechodzi walidacji, i zwraca liczbę pól, które faktycznie zastosował, więc liczba niższa od oczekiwanej to jedyny znak problemu. ImportLoadedInterchangeFromFDF i ImportLoadedFormFromXFDF rzucają przy pierwszym odrzuconym polu. Każde pole jest komitowane na własną rękę, więc pola przetworzone przed wyjątkiem zachowują swoje nowe wartości. Nie traktuj żadnej z tych ścieżek jako transakcji na całym pliku wymiany: jeśli potrzebujesz zachowania wszystko-albo-nic, porzuć wczytany dokument, gdy wystąpi wyjątek, zamiast go zapisywać

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 cel nie-multi-select rzuca
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
    except
      on E: Exception do
        ShowMessage('Import rejected, nothing saved: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

Rozszerzanie callbacków XFDF bez łamania istniejących wołających

Wsparcie tablic w jednostce XFDF niższego poziomu mieszka w osobnym rekordzie THPDFXFDFArrayAccess i w nowych przeciążeniach HPDFXFDFExportFields oraz HPDFXFDFImportFields, a nie w dodatkowych polach doklejonych na końcu istniejącego rekordu THPDFXFDFAccess. Powód to kompatybilność binarna. Kod wypełniający THPDFXFDFAccess jako zmienną lokalną często ustawia tylko gniazda, które zna, i nigdy nie czyści reszty, więc nowy wskaźnik funkcji doklejony do tego rekordu zawierałby śmieci ze stosu, a biblioteka wzięłaby go za prawdziwy callback. Przy osobnym rekordzie starzy wołający zachowują stary układ i stare przeciążenia, a te przeciążenia przekazują wewnętrznie rekord tablicowy samych nili. Oryginalne skalarne przeciążenie importu nadal skleja powtórzone wartości znakiem LF dla zgodności, a tylko przeciążenie świadome tablic trzyma je osobno. Gdy podpinasz własny magazyn danych, zacznij od Default(THPDFXFDFArrayAccess). Zwracaj True z GetFormFieldValueArray dla każdego pola o wartości listowej, łącznie z takim, w którym nic nie wybrano, i False, by cofnąć się do callbacka skalarnego

uses HPDFXFDF;

// Zwykły wskaźnik funkcji, nie „of object”: Context niesie twój własny magazyn
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;

Wymiana multi-select działa na listach, które już istnieją i mają bit MultiSelect ustawiony w /Ff. Za tym, jak pola wyboru i ich bity flag powstają w ogóle, zajrzyj do artykułu o dodawaniu ListBox i innych pól AcroForm do wczytanego PDF. O znacznikach komentarzy przechodzących przez drzewo <annots> XFDF pisze artykuł o imporcie i eksporcie adnotacji XFDF w HotPDF. Pełna referencja API i pobranie wersji próbnej są na stronie komponentu PDF HotPDF dla Delphi