HotPDF renderuje załadowaną stronę PDF do obiektu Delphi TBitmap za pomocą jednego wywołania: RenderLoadedPageToBitmap(PageIndex, DPI). Funkcja ta interpretuje strumień zawartości strony i zwraca należącą do wywołującego 24-bitową mapę bitową RGB w wybranej rozdzielczości, co jest dokładnie tym, czego potrzebuje pasek miniatur, podgląd wydruku lub potok eksportu PDF do obrazu. Ten artykuł omawia to API, a następnie część, która odróżnia użyteczny renderer od zabawki: rysowanie tekstu bezpośrednio z wbudowanych programów fontów zamiast z podobnych czcionek systemowych
Dlaczego renderowanie strony PDF jest trudniejsze niż rysowanie obrazu?
Strona PDF nie jest obrazem. Jest to program: strumień operatorów, które budują ścieżki, wybierają fonty, ustawiają kolory i umieszczają glify, wykonywany w odniesieniu do modelu graficznego zdefiniowanego w ISO 32000-1 §8. Nic w pliku nie określa, jak wygląda dany piksel. Aby wyprodukować mapę bitową, musisz uruchomić ten program — utrzymywać bieżącą macierz przekształceń (CTM), stos stanu grafiki dla q/Q, ścieżkę odcinania (clipping path), przestrzenie kolorów wypełnienia i obrysu — oraz zrasteryzować wynik. Właśnie dlatego „zwykłe pokazanie strony 3 jako obrazu” wymaga interpretera strumienia zawartości, a nie zwykłej konwersji formatu pliku
Renderer HotPDF, wprowadzony w wersji 2.253.0, składa się z sześciu rozdzielonych jednostek odzwierciedlających ten model: rdzenia macierzy afinicznych dla algebry przekształceń PDF [a b c d e f], stosu stanu grafiki, modułu rozwiązywania przestrzeni kolorów (DeviceRGB, DeviceGray, DeviceCMYK, Indexed), modułu budowania ścieżek łączącego operatory ścieżek PDF z GDI, warstwy metryk fontów odczytującej tablice /Widths w celu uzyskania prawidłowych szerokości znaków oraz interpretera, który rozsyła operatory i steruje pozostałą piątką. Obiekt graficzny Image XObject przechodzą przez ten sam stos dekodowania, którego biblioteka używa do ekstrakcji, więc każdy filtr obrazu, który HotPDF potrafi zdekodować na potrzeby ekstrakcji — w tym obrazy JPEG 2000 skompresowane filtrem JPXDecode — pojawia się również w wyrenderowanym wyniku
Renderowanie załadowanej strony do obiektu TBitmap
Metoda RenderLoadedPageToBitmap przyjmuje liczony od zera indeks strony oraz wartość DPI, gdzie 72 DPI odwzorowuje jedną jednostkę przestrzeni użytkownika PDF na jeden piksel. W przypadku niepowodzenia (indeks poza zakresem, brak zasobów) zwraca wartość nil zamiast zgłaszać wyjątek, dzięki czemu przeglądarka może pominąć uszkodzoną stronę i kontynuować pracę. Wywołujący staje się właścicielem zwróconej mapy bitowej i musi ją zwolnić
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('report.pdf') > 0 then
begin
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144); // strona 1 przy 144 DPI
if Bmp <> nil then
try
Image1.Picture.Assign(Bmp);
finally
Bmp.Free; // bitmapa należy do wywołującego
end;
end;
finally
Pdf.Free;
end;
end;
Argument DPI wykonuje pracę skalowania dla każdego typowego scenariusza. Pasek miniatur renderuje się przy 36 lub 48 DPI, uzyskując małe, szybko generowane mapy bitowe; podgląd na ekranie przy 96 lub 144 DPI odpowiada typowej gęstości wyświetlacza; ścieżka eksportu przy 300 DPI tworzy obrazy o jakości druku. Obrót strony określony we wpisie /Rotate oraz odwrócenie punktu początkowego /MediaBox (PDF umieszcza go w lewym dolnym rogu, GDI w lewym górnym) są obsługiwane wewnątrz macierzy strona-urządzenie, więc strona formatu US Letter przy 72 DPI jest zwracana dokładnie jako obraz o rozmiarach 612×792 pikseli we właściwej orientacji
Dlaczego wyrenderowane miniatury PDF pokazują błędne glify?
Błędne lub przybliżone glify w wyrenderowanym pliku PDF prawie zawsze oznaczają, że renderer zastępuje fonty systemowe zamiast używać fontu osadzonego w pliku. Pierwszy renderer HotPDF robił dokładnie to: usuwał prefiks podzbioru z /BaseFont (zamieniając ABCDEF+Arial na Arial), prosił GDI o czcionkę systemową o tej nazwie i rysował nią tekst. W przypadku dokumentu, który używa fontów Arial lub Times New Roman ze standardowym kodowaniem, wynik wygląda blisko oryginału. Jest to jednak tylko przybliżenie i zawodzi w określonych przypadkach
Najgorszym przypadkiem są fonty osadzone jako podzbiory (subset). Czcionka podzbioru może zawierać tylko czterdzieści glifów faktycznie używanych w dokumencie, a kody znaków są przypisane w kolejności specyficznej dla tego pliku — kod 1 może oznaczać „T”, kod 2 „h” i tak dalej. Font systemowy nie wie nic o tym prywatnym przypisaniu, więc tekst albo znika, albo wyświetla się jako całkowicie błędne znaki. Kodowania niestandardowe, fonty symboliczne, fonty z kodami kreskowymi oraz kroje niezainstalowane na maszynie renderującej kończą się w ten sam sposób. Renderer, który poprzestaje na substytucji czcionek systemowych, produkuje miniatury, na których można rozpoznać stronę — dopóki nie używa ona fontów, które w pierwszej kolejności sprawiły, że ich osadzenie było konieczne
Renderowanie wbudowanych glifów: rysowanie bezpośrednio z programu fontu
HotPDF zlikwidował tę lukę w ciągu pięciu wydań (od wersji 2.268.0 do 2.272.0) poprzez analizowanie wbudowanych programów fontów i odtwarzanie obrysów ich glifów jako wypełnionych ścieżek wektorowych GDI. Tekst na wyrenderowanej stronie pochodzi teraz z tych samych danych obrysu, których używa zgodna z przepisami przeglądarka, co oznacza, że fonty z wydzielonym podzbiorem, niestandardowe kodowania i niezainstalowane kroje renderują się w swoich dokładnych kształtach. Zakres obsługi był rozwijany według typów fontów:
W przypadku fontów Type0/CIDFontType2 z osadzonym programem TrueType (FontFile2) renderer bezpośrednio analizuje tabele glyf i loca: kontury kwadratowe są konwertowane na sześcienne krzywe Béziera zrozumiałe dla GDI, rekonstruowane są domniemane punkty na krzywej między kolejnymi punktami poza krzywą, a glify złożone są odtwarzane rekurencyjnie. Obsługiwane są zarówno układy Identity, jak i jawne strumienie CIDToGIDMap, a szerokości CID uwzględniają wpisy szerokości /W oraz /DW, dzięki czemu dwubajtowy tekst Identity-H jest rozmieszczany prawidłowo
Programy CFF (FontFile3, bez względu na to, czy są to CIDFontType0C, Type1C, czy wrapper OpenType) otrzymują pełny interpreter instrukcji Type 2 charstring: linie, krzywe, rodzinę flex, maski podpowiedzi (hint masks) oraz lokalne i globalne wywołania podprogramów z prawidłowym przesunięciem podprogramu. Programy CFF indeksowane według CID mapują kody znaków przez tablicę znakową fontu (charset), co ma znaczenie dla fontów podzbiorów, których kolejność glifów różni się od kolejności CID; uwzględniany jest również wybór słownika fontu dla poszczególnych glifów przez FDArray/FDSelect. Proste fonty TrueType (niebędące CID) dopasowują jednobajtowe kody przez własną tabelę cmap wbudowanego fontu z solidnym łańcuchem podtabel — najpierw formaty Unicode 4 i 12, następnie podtabele symboli z lustrem obszaru prywatnego użytku F000, na końcu starsze formaty Macintosh — podczas gdy proste fonty Type1 są dopasowywane poprzez wbudowane kodowanie programu CFF
Dwa ulepszenia dopełniają obrazu. Po pierwsze, słowniki /Encoding dla prostych fontów są rozwiązywane zgodnie z priorytetem określonym w ISO 32000-1 §9.6.6: tablice /Differences nadpisują kodowanie bazowe, które z kolei nadpisuje własną mapę programu fontu — czyli ścieżkę, od której zależą przepływy pracy oparte na TeX i PostScript, z nazwami glifów rozwiązywanymi za pomocą Adobe Glyph List, zestawu znaków CFF lub tabeli TrueType cmap. Po drugie, fonty Type3, których glify same są miniaturowymi strumieniami zawartości, są odtwarzane w rendererze z połączeniem macierzy fontu, jego rozmiaru i macierzy tekstowej; szerokości /Widths w przestrzeni glifu są interpretowane przez /FontMatrix zgodnie z wymogami ISO 32000-1 §9.6.5, a procedury glifów deklarujące ramkę otaczającą d1 są do niej przycinane, dzięki czemu zniekształcony glif kodu kreskowego nie może wyjść poza swoją komórkę. Gdy kodu nie można zmapować — z powodu uszkodzonego programu czy braku mapowania znaku — renderer powraca do rysowania tego glifu czcionką systemową, zamiast pomijać cały ciąg tekstowy
Jak przyspieszyć wielokrotne renderowanie?
Rozwiązaniem dostarczanym przez HotPDF jest bufor stron oparty na algorytmie MRU (ostatnio używane): metoda RenderLoadedPageToBitmapCached przechodzi do RenderCacheCapacity wyrenderowanych stron (domyślnie 8) indeksowanych według numeru strony i DPI, a trafienie w pamięci podręcznej zwraca nową, należącą do wywołującego kopię bez dotykania strumienia zawartości — zazwyczaj tysiące razy szybciej niż ponowna interpretacja strony. Ten wzorzec idealnie pasuje do przeglądarek: użytkownik przełączający się między dwiema stronami lub zdarzenie zmiany rozmiaru okna żądające tej samej strony w tym samym DPI za każdym doomed trafia w pamięć podręczną
// Pasek miniatur: pierwszy przebieg renderuje, przewinięcie wstecz trafia w pamięć podręczną
for I := 0 to ThumbCount - 1 do
begin
Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
if Bmp <> nil then
try
ThumbList.AddThumbnail(I, Bmp);
finally
Bmp.Free;
end;
end;
// Po edycji wczytanej strony na miejscu:
Pdf.InvalidateRenderedPageCache; // następne renderowanie odzwierciedla zmianę
Należy pamiętać o zużyciu pamięci przed zwiększeniem pojemności bufora. Strona formatu US Letter przy 300 DPI to 2550×3300 pikseli, co daje około 25 MB jako 24-bitowa mapa bitowa, więc osiem stron w pamięci podręcznej w rozdzielczości eksportowej zajmuje około 200 MB. Przy DPI miniatur te same osiem wpisów kosztuje znacznie mniej niż megabajt. Dostosuj RenderCacheCapacity do DPI, w którym faktycznie buforujesz strony, i wywołaj InvalidateRenderedPageCache po każdej edycji w miejscu — pamięć podręczna jest indeksowana tylko według strony i DPI, więc nie widzi, że zmieniła się zawartość pod spodem. Załadowanie nowego dokumentu czyści ją automatycznie
Pod pamięcią podręczną stron działa drugi bufor: zdekodowane obiekty graficzne image XObject są przechowywane w buforze ograniczonym bajtowo przez ImageCacheMaxBytes (domyślnie 32 MB) z usuwaniem elementów metodą LRU. Logo lub nagłówek powtarzający się na każdej stronie jest dekodowany tylko raz przy załadowaniu dokumentu, zamiast przy każdym wywołaniu operatora Do, co skraca czas renderowania stron ze wspólnym obrazem o około połowę i w tym samym stopniu przyspiesza eksport do wielostronicowych plików TIFF. Wywołanie InvalidateRenderedPageCache czyści również tę pamięć podręczną
Co nadal renderuje się w sposób przybliżony
Renderer jest przeznaczony dla powszechnie używanego podzbioru dokumentów PDF i warto wiedzieć, gdzie leżą jego ograniczenia. Przestrzenie kolorów CalRGB, Lab oraz oparte na ICC są przybliżane, a nie zarządzane profilami kolorów — obsługiwane są podstawowe przestrzenie kolorów (Device), palety indeksowane (Indexed) oraz próbkowane tabele kolorów funkcji Type 0, ale plik produkcyjny opierający się na intencjach renderowania ICC nie będzie dokładny kolorymetrycznie. Wzory cieniowania (sh) oraz tryby mieszania wykraczające poza prosty kanał alfa również pozostają poza zakresem, a rekurencja obiektów Form XObject jest ograniczona głębokością jako zabezpieczenie przed pętlą. W przypadku faktur, raportów, umów i formularzy — stron składających się z tekstu, ścieżek i obrazów — wynik jest wierny; w przypadku próbnych wydruków projektowych pełnych gradientów i grup przezroczystości mapę bitową należy traktować jako podgląd, a nie wydruk próbny (proof)
Wnioski praktyczne: jeśli Twój potok generuje dokumenty za pomocą HotPDF lub przetwarza typowe biznesowe pliki PDF, metoda RenderLoadedPageToBitmap odtwarza je z dokładnymi kształtami osadzonych glifów, poprawnymi szerokościami CID i prawidłową geometrią strony. Przybliżenia występują w rzadkich obszarach modelu graficznego, których dokumenty biznesowe prawie nigdy nie odwiedzają
Metoda RenderLoadedPageToBitmap, jej wersja z buforowaniem oraz opisany tutaj potok renderowania osadzonych glifów są dostarczane jako część komponentu HotPDF Component dla Delphi i C++Builder — natywnej biblioteki VCL bez zewnętrznych zależności DLL, obsługującej tworzenie, edycję, wyodrębnianie tekstu i renderowanie stron PDF w jednym pakiecie