Formularz PDF z wartościami pól, ale bez strumieni wyglądu, może wyjść z zadania renderowania na Linuksie jako puste pola, zła czcionka albo wyrównanie, lub z EHPDFHeadlessRenderError o treści "Widget field requires a name field type". Renderer headless HotPDF rysuje tymczasowe wyglądy dla widgetów tekstowych, przyciskowych i wyboru, dziedzicząc /FT, /V, /DA i /Q przez drzewo pól
Gdzie widget trzyma ustawienia swojego pola
Większość kłopotów zaczyna się od rozdziału między polem a jego widgetami. Słownik pola trzyma logiczną część kontrolki formularza: jej typ, wartość, flagi i domyślny wygląd. Adnotacja widget trzyma to, czego potrzebuje strona: /Rect, obramowanie, wskaźnik na swoją stronę. Pole z jednym widgetem bywa zwykle scalone w jeden słownik. Gdy ta sama wartość ma pokazać się na kilku stronach, widgety stają się dziećmi pola i często niosą same geometrię i odwołanie /Parent. ISO 32000-1 §12.7.3.1 czyni /FT, /Ff, /V i /DV dziedzicznymi wzdłuż tego łańcucha (tabela 220), a §12.7.3.3 dorzuca do zbioru dziedzicznych z tabeli 222 tylko dwa atrybuty tekstu zmiennego: /DA, który jest wymagany, ale może przyjść od przodka, i /Q. Słownik formularza interaktywnego (tabela 218) dostarcza domyślnych wartości /DA i /Q dla całego dokumentu, plus /DR — domyślny słownik zasobów, w którym szuka się nazw czcionek z /DA. /DR nie ma w tabeli 222; należy do formularza jako całości. Renderer czytający tylko słownik widgetu nie znajdzie w pliku poniżej ani typu, ani wartości, ani /DA i spadnie na własną domyślną czcionkę
1 0 obj % katalog
<< /Type /Catalog /Pages 2 0 R
/AcroForm << /Fields [10 0 R] /DA (/Helv 0 Tf 0 g) /Q 0
/DR << /Font << /Helv 20 0 R /Cour 21 0 R >> >> >> >>
endobj
10 0 obj % pole końcowe: typ, wartość i domyślne wyglądy siedzą tutaj
<< /FT /Tx /T (invoice_no) /V (INV-2026-0417)
/DA (/Cour 10 Tf 0 0 0.6 rg) /Q 2
/Kids [11 0 R 12 0 R] >>
endobj
11 0 obj % widget na stronie 1: tylko geometria, rogi podane od górnego prawego rogu
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj % widget na stronie 2: tylko geometria
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj
HotPDF szuka każdego wpisu pola na widgetcie, a gdy go tam nie ma, wspina się po /Parent po jednym słowniku; bliższa wartość wygrywa, a tylko /DA i /Q spadają na słownik AcroForm, gdy łańcuch się kończy. Oczekiwane dla pliku powyżej: oba widgety pokazują INV-2026-0417 wyrównane do prawej, Courierem 10-punktowym, ciemnoniebieskim, a ogólnodokumentowe /Helv 0 Tf nigdy nie wchodzi do gry, bo obiekt 10 ma własne /DA. Gdy pole wychodzi zamiast tego jako czarna Helvetica 12-punktowa, oczywistym podejrzanym jest czcionka Courier nieobecna na linuksowej maszynie. Rozmiar zdradza sprawę. To rysuje HotPDF, gdy żadne /DA nie wyskoczy ani na widgetcie, ani na żadnym przodku, ani w słowniku AcroForm. Zrzuć więc widget, przejdź ręcznie po jego /Parent i znajdź, gdzie producent położył /DA — na przykład na słowniku pola, do którego widget nigdy nie linkuje z powrotem
Nazwy czcionek z /DA rozwiązują się względem /DR na widgetcie albo polu nadrzędnym, /DR AcroForm i zasobów czcionek strony — w tej kolejności. /DR na poziomie pola jest poza tabelą 222, ale gdy jest, nie zostawia wątpliwości, więc HotPDF go przyjmuje. Wspinaczka ma granice: pętla w drzewie zatrzymuje się z "Widget field inheritance contains a cycle", /Parent niebędący słownikiem z "Widget Parent requires a dictionary", a łańcuch głębszy niż MaxDepth w opcjach renderowania (domyślnie 32) z "Widget field inheritance depth budget exceeded". Komponent VCL stosuje to samo dziedziczenie, gdy czyta dziedziczone wartości AcroForm i resetuje pola
Jak renderer headless rozstrzyga, co rysować?
HPDFRenderHeadlessPagePNG i jego wariant plikowy uruchamiają fallback tylko dla widocznego widgetu, którego /AP brakuje albo jest null; ISO 32000-1 §7.3.7 mówi, że wartość null w słowniku to samo co wpis nieobecny, i renderer traktuje /AP null właśnie tak. Zwykły wygląd, jeśli jest, zawsze wygrywa — nieważne, co mówi /NeedAppearances — więc nieaktualny wygląd z wcześniejszego wypełnienia dalej się pokazuje, dopóki coś go nie wygeneruje na nowo. Wczytany dokument nie jest nigdy modyfikowany. Co zostanie narysowane, zależy od rozwiązanych typu pola i flag:
| Pole | Syntezowany wygląd |
|---|---|
Tekst jednoliniowy, /FT /Tx | Ciąg /V, ułożony z rozwiązanymi /DA i /Q wewnątrz /Rect |
Checkbox, /FT /Btn | Prostokątna ramka z promieniami narożników z /Border, wypełnieniem z /MK /BG i obrysowanym wektorowym ptaszkiem, gdy włączony |
| Przycisk radio, Btn z bitem 16 | Okrągła ramka z wypełnioną kropką, gdy włączony |
| Pushbutton, Btn z bitem 17 | Prostokątna ramka z napisem /MK /CA wyśrodkowanym |
Pole combo, /FT /Ch z bitem 18 | Tekst wyświetlany wybranego wpisu /Opt jako wyśrodkowany napis |
| Lista, Ch z wyzerowanym bitem 18 | Jeden wiersz na opcję, począwszy od /TI, z pasem podświetlenia za każdym zaznaczonym wierszem |
Ptaszek wyboru i kropka radia to ścieżki wektorowe, a nie glify ZapfDingbats, jak rysują je liczne generatory, bo chudy kontener Linuksa może nie mieć zainstalowanego ZapfDingbats. Wyglądy pushbuttonów i pól combo zgadzają się z tym, co generator wyglądów VCL HotPDF rysuje dla tego samego pola, więc formularz wygląda podobnie, niezależnie od strony, która wyprodukowała obraz. Napis bierze czcionkę, rozmiar i kolor z /DA; skoro układ napisu nie dopasowuje się sam, rozmiar /DA równy 0 staje się 70 procentami wysokości pola, z limitem 12 punktów. Stan checkboxów i radio pochodzi z własnego /AS widgetu, z fallbackiem na dziedziczone /V, i każdy stan inny niż Off rysuje znacznik. Daj każdemu dziecku radio własne /AS. /V rodzica nazywa zaznaczony stan dla całej grupy i nie odróżni jednego dziecka od drugiego
uses
SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;
function RenderFormPage(const FileName, PngName: string; PageIndex: Integer;
out Diagnostic: string): Boolean;
var
Doc: THPDFHeadlessDocument;
Options: THPDFHeadlessRenderOptions;
Info: THPDFHeadlessRenderResult;
begin
Result := False;
Diagnostic := '';
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile(FileName);
Options := THPDFHeadlessRenderOptions.Default; // RenderAnnotations = True, MaxDepth = 32
Options.Usage := hruPrint; // jak wydruk z przeglądarki: widgety bez flagi Print są pomijane
try
// PageIndex liczy od zera
Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
PngName, Options);
Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
Result := True;
except
on E: EHPDFHeadlessRenderError do
// np. 'Widget field requires a name field type'; przy wypełnionym formularzu nie
// ponawiaj z RenderAnnotations := False, co wyrzuca każdy widget i jego wartość
Diagnostic := E.Message;
end;
finally
Doc.Free;
end;
end;
Fallback pola tekstowego rysuje pola jednoliniowe trzymające wartość ciągu. Dla bogatszego układu tekstu generuj wygląd w czasie wypełniania po stronie Windows, gdzie EnsureLoadedFieldAppearanceStream buduje brakujący strumień /AP, zanim plik zostanie spłaszczony albo wysłany; zadanie na Linuksie dostaje wtedy prawdziwe strumienie wyglądu, a renderer headless wymienia z nazwy każde pole, jakie spotka, a które nadal jakiegoś potrzebuje. Wynik z Linuksa osądzaj jednak rendererem Linuksa. RenderLoadedPageToBitmap rysuje przez backend GDI Windows, który ten fallback nigdy nie uruchamia, więc strona wyglądająca dobrze tam nie mówi ci nic o tym, co urządzenie Cairo wyprodukuje z tego samego pliku
Bity flag pól są numerowane od jedynki
ISO 32000-1 §12.7.3.1 stwierdza, że pozycje bitów w słowie /Ff biegną od 1 na końcu najmłodszym do 32, i tabele 226, 228 oraz 230 używają tej numeracji. W Delphi bit N to zatem 1 shl (N - 1). Przesuń o sam numer bitu, a każdy test przenosi się o pozycję w górę: 1 shl 16 zapisane dla "radio, bit 16" faktycznie testuje flagę pushbuttona, a 1 shl 18 zapisane dla "combo, bit 18" testuje Edit. Obie złe maski to prawdziwe flagi. Kod się kompiluje, działa i sortuje pola do złego gatunku
| Flaga | Tabela | Bit | Maska Delphi |
|---|---|---|---|
| Multiline | 228 (tekst) | 13 | 1 shl 12 = 4096 |
| Password | 228 (tekst) | 14 | 1 shl 13 = 8192 |
| Comb | 228 (tekst) | 25 | 1 shl 24 = 16777216 |
| RichText | 228 (tekst) | 26 | 1 shl 25 = 33554432 |
| Radio | 226 (przycisk) | 16 | 1 shl 15 = 32768 |
| Pushbutton | 226 (przycisk) | 17 | 1 shl 16 = 65536 |
| Combo | 230 (wybór) | 18 | 1 shl 17 = 131072 |
uses
Classes, SysUtils, HPDFHeadlessDocument;
const
// ISO 32000-1 numeruje bity Ff od 1 (najmłodszy), więc bit N to 1 shl (N - 1)
TxMultiline = 1 shl 12; // bit 13
TxPassword = 1 shl 13; // bit 14
TxFileSelect = 1 shl 20; // bit 21
TxDoNotScroll = 1 shl 23; // bit 24
TxComb = 1 shl 24; // bit 25
TxRichText = 1 shl 25; // bit 26
TxProducerLayout = TxMultiline or TxPassword or TxFileSelect or
TxDoNotScroll or TxComb or TxRichText;
procedure ListTextFieldsForProducerAppearance(const FileName: string;
Report: TStrings);
var
Doc: THPDFHeadlessDocument;
Field: THPDFHeadlessFormField;
I: Integer;
begin
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile(FileName);
for I := 0 to Doc.FormFieldCount - 1 do
begin
// FieldType, Flags i MaxLength przychodzą już odziedziczone od przodków
Field := Doc.GetFormField(I);
if Field.FieldType <> 'Tx' then
Continue;
if (Field.Flags and TxProducerLayout) <> 0 then
Report.Add(Format('%s: flags $%.8x, needs a producer /AP',
[Field.Name, Field.Flags]))
else if Field.MaxLength > 0 then
Report.Add(Format('%s: /MaxLen %d, needs a producer /AP',
[Field.Name, Field.MaxLength]))
else if Field.Value = '' then
Report.Add(Format('%s: blank, fill it or give it an /AP', [Field.Name]));
end;
finally
Doc.Free;
end;
end;
Skan czyta flagi na poziomie pola — tam, gdzie kładą je producenci; renderer honoruje też /Ff na osobnym widgetcie, więc traktuj listę jako pomoc planistyczną, a komunikat renderowania jako słowo ostateczne. Kontrola pustych ma znaczenie, bo pole tekstowe bez /V i bez /AP nie ma czego syntezować: render zatrzymuje się z "A text-field widget without a value has no appearance to synthesize", a lekarstwem jest wartość albo wygląd, zanim plik dotrze do renderera
Co znaczy "Degenerate appearance geometry"?
Znaczy to, że prostokąt albo pudło wyglądu nie ma powierzchni, a odwrócony /Rect nie jest przyczyną. ISO 32000-1 §7.9.5 pozwala prostokątowi wymieniać dowolne dwa przeciwległe narożniki po przekątnej, a jego nota każe oprogramowaniu przetwarzającemu być gotowym na ich normalizację. Naiwny renderer liczący szerokość jako x2 - x1 dostaje z [300 720 160 700] liczbę ujemną i albo odrzuca adnotację, albo lustrowo odbija jej wygląd. HotPDF zamienia współrzędne na porządek lewy-dolny i prawy-górny przed jakąkolwiek kontrolą rozmiaru — dla syntezowanych wyglądów i istniejących strumieni /AP równo — więc widget 11 powyżej renderuje się, jakby jego rogi były konwencjonalne. Błąd jest wyrzucany tylko dla adnotacji mającej strumień wyglądu, gdy po normalizacji /Rect ma zerową szerokość albo wysokość, albo gdy /BBox strumienia zapada się pod jego /Matrix do linii albo punktu. Gdy się pojawi, zrzuć /Rect widgetu razem z /BBox i /Matrix strumienia: jedno z tych trzech ma zerowy wymiar
Widget bez /AP i z /Rect o zerowej powierzchni idzie inną drogą: fallback go pomija i jedzie dalej ze stroną. ISO 32000-2 §12.5.2 zwalnia z wymogu wyglądu tylko adnotacje Popup, Projection i Link oraz prostokąt zapadający się do pojedynczego punktu. Prostokąt zaledwie liniowy nadal wymaga wyglądu według PDF 2.0; renderer pomija jednak i ten — z prostego powodu: nie ma powierzchni do zamalowania
Gdy typu pola nie da się znaleźć
"Widget field requires a name field type" znaczy, że renderer wspiął się od widgetu tak daleko, jak sięgnęły odwołania /Parent, i nie znalazł żadnego /FT. Renderer startuje od tablicy /Annots strony, więc odwołanie w górę /Parent to jego jedyna droga do pola. Wczytywanie idzie drugą stroną, w dół od tablicy /Fields AcroForm przez /Kids, dlatego LoadFromFile może się udać na pliku, którego strona później pada przy renderowaniu. Typowa przyczyna to widget wymieniony w /Kids pola, który nie wskazuje z powrotem. ISO 32000-1, tabela 188, daje na to pewne przykrycie: wymaga /Parent na widgetcie tylko, gdy widget jest jednym z kilku dzieci, i mówi "absent otherwise", więc writer czytający ją dosłownie wyrzuca odwołanie dla pola z jednym osobnym widgetem. ISO 32000-2 rozluźniło ten sam wpis do "optional otherwise". My i tak pisalibyśmy /Parent na każdym widgetcie niescalonym ze swoim polem. ISO 32000-2 na to pozwala, i to jest jedyna droga, jaką czytnik prowadzony od strony — nasz czy cudzy — może dosięgnąć pola. Jeśli nie możesz naprawić producenta, scalenie samotnego widgetu ze słownikiem jego pola daje ten sam rezultat
Widget, który linkuje do rodzica i znajdzie /DA gdziekolwiek po drodze w górę, renderuje się poprawnie bez żadnego /AP w rendererze headless komponentu HotPDF dla Delphi, co czyni drzewo pól — bardziej niż strumienie wyglądu — pierwszą rzeczą do ogarnięcia w linuksowym potoku formularzy