Artykuł techniczny

Renderowanie stron PDF do mapy bitowej w Delphi za pomocą HotPDF

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