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
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
<!-- 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
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
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