HotPDF v2.743.0 spłaszcza adnotacje PDF, które nie mają strumienia wyglądu /AP, zamiast po cichu je pomijać. FlattenLoadedAnnotations kieruje teraz widget bez wyglądu przez EnsureLoadedFieldAppearanceStream i buduje Form XObject dla znaczników bez wyglądu na podstawie właściwości samej adnotacji, dzięki czemu wartości wpisane do formularza /NeedAppearances trafiają do treści strony zamiast znikać podczas spłaszczania. Awaria, która wymusiła tę zmianę, wygląda jak brak działania. Klient przysyła wypełniony formularz wniosku wydrukowany do PDF-a z przeglądarki. Ładujesz go w HotPDF, wywołujesz FlattenLoadedAnnotations, otrzymujesz 0, zapisujesz i dostarczasz dokument z pustymi polami tam, gdzie wnioskodawca wpisał nazwisko i kwotę. Nic nie zostało zgłoszone ani zapisane w logu. Wartości przez cały czas znajdowały się w pliku, w wpisie /V każdego pola, a przebieg flatten przeszedł obok nich, ponieważ żaden z tych widgetów nie miał strumienia wyglądu do zapisania
Dlaczego spłaszczanie formularza wydrukowanego z przeglądarki gubi wpisane wartości?
Bo formularz /NeedAppearances przechowuje wartość, ale nie przechowuje obrazu tej wartości. ISO 32000-1 12.7.2 pozwala formularzowi interaktywnemu ustawić /NeedAppearances true w słowniku AcroForm, co mówi viewerowi, aby przy otwieraniu zbudował wizualną powierzchnię każdego pola z /V, /DA i /Q. Producenci tanio generujący formularze — ścieżki drukowania przeglądarek, wypełniacze serwerowe i niektóre frontendy skanerów — korzystają z tej możliwości i nie zapisują w ogóle /AP. Spłaszczanie, zgodnie z algorytmem wyglądu z ISO 32000-1 12.5.5, jest zadaniem transkrypcji: pobierz normalny strumień wyglądu adnotacji, odwzoruj jego /BBox na /Rect, wywołaj go z treści strony przez operator Do, a następnie usuń adnotację. Bez strumienia źródłowego nie ma czego transkrybować. Pierwotna implementacja HotPDF z v2.386.0 traktowała taki przypadek jako "skip", co w izolacji można obronić, ale w całości jest katastrofalne: dokumenty, które najczęściej potrzebują spłaszczania, najrzadziej zawierają wyglądy. Ta sama luka pochłaniała znaczniki — a Highlight z narzędzia recenzji, Square z redakcji i podpis Ink — gdy producent liczył na to, że narysuje je viewer
Gdzie HotPDF włącza syntezę do FlattenLoadedAnnotations
Punkt zaczepienia jest celowo późny: po nieudanym wyszukiwaniu wyglądu, a nie przed nim. FlattenLoadedAnnotations nadal najpierw pyta GetLoadedAnnotationAppearanceStream o normalny wygląd, a adnotacja, która już go ma, jest zapisywana dokładnie tak jak w v2.386.0. Tylko wynik nil, dla adnotacji z nieosobliwym /Rect i bez flagi hidden, wchodzi na ścieżkę syntezy. Ta kolejność ma znaczenie: autor dokumentu, który zadał sobie trud zapisania /AP, otrzymuje z powrotem własne bajty, a nie rekonstrukcję HotPDF
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
if (NStrm= nil) and (RR> RL) and (RT> RB) and ((FlagsValue and 2)= 0) then
begin
if Subtype= 'Widget' then
begin
FieldIdx:= GetLoadedFormFieldIndexForAnnotation(Indices[PgI], AnI, WidgetIdx);
if FieldIdx>= 0 then
EnsureLoadedFieldAppearanceStream(FieldIdx);
// zapytaj ponownie: generator dołączył do widgetu /AP /N
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
end
else
NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;
Od tego miejsca dwie rodziny adnotacji rozdzielają się. Widget jest rozwiązywany z powrotem do pola właściciela przez GetLoadedFormFieldIndexForAnnotation i przekazywany do EnsureLoadedFieldAppearanceStream, generatora wyglądu pola obecnego w tej bibliotece PDF dla Delphi od v2.328.0. Ponowne użycie go zamiast pisania drugiego renderera pól jest całym sensem rozwiązania — obejmuje już fonty Type0, zawijanie wierszy, quadding, stany checkboxów i radio /AS oraz obrót /MK, czyli tę samą maszynerię, która stoi za dodawaniem pól AcroForm do już załadowanego PDF-a. Wszystko inne trafia do syntezatora znaczników. Dla wywołującego nic się nie zmienia: to samo jednoliniowe wywołanie flatten zwraca teraz niezerową liczbę dla dokumentów, które wcześniej zwracały zero
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: widgety i znaczniki bez AP są syntetyzowane, a potem zapisywane
Flattened:= Doc.FlattenLoadedAnnotations; // wszystkie strony, wszystkie podtypy
// Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
if Flattened= 0 then
raise Exception.Create('nothing was flattened');
Doc.SaveLoadedDocument('flattened.pdf');
finally
Doc.Free;
end;
Dlaczego QuadPoints i InkList trafiają w złe miejsce?
Bo te współrzędne są w przestrzeni użytkownika strony, podczas gdy syntetyzowany strumień wyglądu rysuje we własnej przestrzeni /BBox, a oba początki nie są tym samym punktem. Tabela 176 ISO 32000-1 definiuje /QuadPoints dla adnotacji znaczników tekstowych w domyślnej przestrzeni użytkownika, a tabela 174 robi to samo dla końców /L adnotacji linii; /InkList podąża za tą samą konwencją. HotPDF nadaje syntetyzowanemu formularzowi /BBox o wartości [0 0 W H], którego początek leży w lewym dolnym rogu /Rect. Każdy punkt pobrany z /QuadPoints, /L albo /InkList musi więc zostać przesunięty o zanegowany lewy dolny róg /Rect, zanim zostanie zapisany do strumienia treści. Pomyłka sprawi, że zaznaczenie na linii 700 punktów wyżej na stronie zostanie narysowane 700 punktów nad własnym polem, co w praktyce oznacza brak rysunku. Poprawka to jedno odejmowanie na współrzędną i dobrze składa się z cm emitowanym później przez bake — ta macierz mapuje /BBox z powrotem na /Rect, więc oba kroki znoszą się do poprawnej geometrii absolutnej
// Końce /L są w przestrzeni użytkownika strony (ISO 32000-1 tabela 174); początek
// BBox leży w lewym dolnym rogu /Rect, więc przesuń o -(RL, RB)
X1:= ArrNum(LA, 0, 0)- RL;
Y1:= ArrNum(LA, 1, 0)- RB;
X2:= ArrNum(LA, 2, 0)- RL;
Y2:= ArrNum(LA, 3, 0)- RB;
StrokeOp:= ColorOp(DArr('C'), true);
if StrokeOp= '' then
StrokeOp:= '0 G';
Result:= _FloatToStrR(BW)+ ' w '#10+ StrokeOp+ #10+
_FloatToStrR(X1)+ ' '+ _FloatToStrR(Y1)+ ' m '+
_FloatToStrR(X2)+ ' '+ _FloatToStrR(Y2)+ ' l S'#10;
Co faktycznie rysuje syntetyzowany wygląd znacznika
Syntezator znaczników odczytuje wyłącznie słownik adnotacji, dzięki czemu wynik jest przewidywalny i uczciwie pokazuje, czego nie da się wiedzieć. FreeText i Stamp rysują /Contents z użyciem fontu i koloru odczytanych z /DA, wyrównanych przez /Q, z marginesem 2 pt. Square rysuje re, a Circle obrys złożony z czterech łuków Béziera, obrysowany w /C i wypełniony przez /IC, jeśli jest obecne, szerokością z /BS /W. Line i Ink obrysowują swoje wierzchołki. Highlight wypełnia każdy quad, a Underline, StrikeOut i Squiggly rysują linię na dole quada, w jego środku albo jako zygzak o jednym punkcie. /CA poniżej 1 staje się ExtGState z wpisem ca, referencjonowanym jako /GSA gs na początku strumienia
Kodowanie tekstu jest wybierane z wpisu /DR /Font AcroForm o nazwie wskazanej przez /DA. Jeśli /Subtype tego fontu to Type0, HotPDF zapisuje łańcuch jako szesnastkowy literał UTF-16BE ze znacznikiem kolejności bajtów FEFF; w przeciwnym razie zapisuje escapowany literał, w którym nawiasy i odwrotne ukośniki są escapowane, a bajty powyżej 126 zapisywane oktalnie. Operator Tf z /DA jest emitowany przed BT, co jest legalne, bo stan tekstu trwa poza granicą obiektu tekstowego, i pozwala uniknąć rozbierania łańcucha /DA. Warto jasno nazwać dwa ograniczenia. Szerokość linii używana do zawijania i quadding jest szacowana heurystyką pół em / pełne em zamiast prawdziwymi metrykami fontu, więc wyrównanie w foncie proporcjonalnym jest bliskie, ale nie dokładne. Podtyp, dla którego nie da się niczego syntetyzować — Popup, Link albo Stamp, którego jedyną treścią jest nazwa ikony — daje nil i pozostaje nietknięty, dokładnie jak wcześniej
Tymczasowa zamiana /Annots, która karze za pomocne porządki
FlattenOneWidget, ścieżka pojedynczego widgetu używana przez FlattenLoadedFormFields, jest pułapką aliasowania, którą musi respektować każda zmiana we wspólnej pętli flatten. Tymczasowo zastępuje wartość /Annots strony tablicą z jednym elementem, aby ogólny przebieg flatten działał na jednym widgecie, a następnie przywraca pierwotny wskaźnik PHPDFDictionaryItem w bloku finally. Przywrócenie zapisuje do slotu słownika przechwyconego przed wywołaniem
DictItem:= PHPDFDictionaryItem(PageObj.Items.Items[AnnotsIndex]);
Item:= DictItem^.Value;
TemporaryAnnots:= THPDFArrayObject.Create(nil);
TemporaryAnnots.AddObject(Target);
DictItem^.Value:= TemporaryAnnots;
try
Result:= FlattenLoadedAnnotations(IntToStr(PageIndex+ 1), 'Widget')= 1;
finally
DictItem^.Value:= Item; // wiszący, jeśli wewnętrzna pętla zwolniła ten element
TemporaryAnnots.Free;
end;
Dodaj rozsądnie wyglądające porządki do wspólnej pętli wewnętrznej — DeleteValue('Annots') po opróżnieniu tablicy, aby zachowana strona nie miała kosmetycznej pustej tablicy — a to wywołanie zwolni właśnie element słownika wskazywany przez DictItem. finally zapisze wtedy przez wiszący wskaźnik, a proces zakończy się komunikatem "Invalid pointer operation". Dwa istniejące testy wykryły to natychmiast, co jest jedynym powodem, dla którego trafiło do przypisu, a nie do zgłoszenia wsparcia. Zasada jest szersza: przed dodaniem porządków do wspólnej pętli sprawdź w wywołujących kontrakty aliasowania albo zamiany. Pozostawiona pusta tablica /Annots jest kosmetyczną skazą i nie warto za nią płacić utratą gwarancji czasu życia wskaźnika
Co pozostaje niespłaszczone i ile kosztuje flatten
Ukryte adnotacje są wykluczone celowo. Adnotacja, której liczba całkowita /F ma ustawiony bit na pozycji 2, jest ukryta zgodnie z ISO 32000-1 12.5.3, a gdy nie ma również /AP, pojawia się realna pokusa, aby zsyntetyzować wygląd i zapisać go jak resztę. Byłby to błąd o konsekwencjach bezpieczeństwa: zapisanie niewidocznej notatki w treści strony uczyniłoby ją widoczną dla każdego, kto otworzy plik. HotPDF pozostawia takie adnotacje dokładnie tam, gdzie były, i nie zlicza ich w wartości zwrotnej. Równie jasno trzeba mówić użytkownikom o cenie adnotacji, które zostaną zapisane. Spłaszczanie jest nieodwracalne — adnotacja jest usuwana z tablicy /Annots strony, a jej wygląd staje się teraz treścią strony, więc nie ma już edycji wartości pola, wątku komentarzy, przełączania stanu /AS ani sposobu odzyskania danych strukturalnych poza oryginalnym plikiem. Spłaszczaj kopię, zachowaj oryginał i sięgaj po niego dopiero wtedy, gdy dokument przestaje być formularzem, a staje się rekordem. Jeśli problem dotyczy XFA, a nie braku wyglądu, zacznij od osobnej ścieżki spłaszczania XFA do AcroForm w HotPDF, a jeśli dopiero budujesz formularz, notatki o podłączaniu akcji pól AcroForm i walidacji opisują stronę zapisu
Jedna uwaga dotycząca weryfikacji oszczędzi ci popołudnia. ExtractLoadedPageGlyphs nie schodzi do Form XObject, a zapisany wygląd znajduje się właśnie wewnątrz niego — strumień treści strony zawiera tylko sekwencję q ... cm /FlatAn<n> Do Q. Ekstrakcja glifów ze spłaszczonej strony nie zwróci więc niczego i jest to poprawne zachowanie, a nie utrata zapisu. Weryfikuj albo na poziomie bajtów, sprawdzając nazwę zasobu /FlatAn, wywołanie Do i /Subtype /Form, albo przez pipeline renderowania, który rozwija XObjecty
Spłaszczanie adnotacji wygląda jak trzy linie transkrypcji dopóty, dopóki nie spotkasz dokumentów, które ludzie naprawdę generują. Jeśli pracujesz z wypełnionymi formularzami, znacznikami recenzji albo wynikiem archiwalnym w Delphi lub C++Builder, warto przeczytać, jak komponent PDF HotPDF dla Delphi obsługuje załadowane dokumenty po stronie AcroForms i adnotacji, zanim zbudujesz własny generator wyglądu na jego bazie