Artykuł techniczny

Renderowanie wbudowanych czcionek PDF w Delphi za pomocą HotPDF

Komponent HotPDF renderuje wbudowane czcionki PDF w Delphi bez konieczności instalowania czegokolwiek w systemie: potok renderowania wbudowanych glifów w module HPDFGlyphRender.pas analizuje programy czcionek zapisane w samym pliku PDF — obrysy TrueType glyf z sekcji FontFile2, opisy znaków (charstrings) CFF Type 2 z sekcji FontFile3 oraz strumienie zawartości glifów Type 3 — i odtwarza je jako wypełnione ścieżki wektorowe GDI. Ten artykuł to szczegółowe omówienie wierności czcionek stojące za renderowaniem stron PDF do TBitmap za pomocą HotPDF: tamten wpis opisuje renderer jako całość, ten natomiast wyjaśnia, jak tekst na tych stronach uzyskuje swoje dokładne kształty

Kwadraty (tofu), brakujące znaki lub nieznacznie zniekształcone litery w wyrenderowanym pliku PDF prawie zawsze oznaczają, że renderer zażądał czcionki od systemu operacyjnego, zamiast użyć czcionki wbudowanej w pliku. Zgłoszenie błędu wygląda za każdym razem tak samo: dokument prezentuje się idealnie na komputerze autora, a po otwarciu go przez klienta na czystym serwerze lub zablokowanym pulpicie japońska faktura wyświetla puste kwadraty, albo zastosowana czcionka zastępcza powoduje przesunięcie podziałów linii. Czcionki te nigdy nie były zainstalowane na komputerze docelowym — znajdują się jedynie wewnątrz pliku PDF — i renderer polegający na systemowych zamiennikach ich nie odczyta. Czcionki podzbiorowe (subset) dodatkowo komplikują sytuację: podzbiór może zawierać np. czterdzieści glifów przypisanych do kodów znaków w sposób prywatny dla tego jednego pliku, a mapowania tego nie zna żadna zainstalowana czcionka

Dlaczego dokument PDF renderuje się z kwadratami zamiast tekstu?

Standard ISO 32000-1 §9.9 definiuje trzy nośniki wbudowanego programu czcionki w deskryptorze: FontFile przechowuje oryginalny program Type 1, FontFile2 program TrueType, a FontFile3 czysty program CFF (Type1C lub CIDFontType0C) bądź opakowanie OpenType. Czwarty wariant, czyli czcionka Type 3 z sekcji ISO 32000-1 §9.6.5, nie zawiera żadnych danych binarnych — każdy glif to krótki strumień zawartości PDF uruchamiany na miejscu. Te trzy nośniki różnią się matematycznym opisem obrysów (kwadratowe splajny B-sklejane, sześcienne opisy charstrings lub dowolne operatory stron), stąd poprawny renderer potrzebuje osobnego interpretera dla każdego z nich oraz warstwy kodowania, która przekształca kody znaków na właściwy indeks glifu przed przystąpieniem do rysowania obrysu

Jak HotXLS / HotPDF konwertuje obrysy TrueType glyf na ścieżki GDI?

Klasa THPDFEmbeddedTTF w module HPDFGlyphRender.pas odczytuje tabelę loca, aby zlokalizować każdy rekord glifu, przechodzi punkt po punkcie kontury glyf i generuje ścieżkę GDI. Dwie konwencje TrueType wymagają wyraźnej obsługi. Po pierwsze, następujące po sobie punkty leżące poza krzywą (off-curve) implikują obecność punktu na krzywej (on-curve) w ich środku, a kontur zawierający wyłącznie punkty poza krzywą rozpoczyna się w połowie drogi między jego ostatnim a pierwszym punktem — pominięcie którejś z tych reguł powoduje, że zaokrąglone glify stają się kanciaste lub zniekształcone. Po drugie, krzywe TrueType to kwadratowe krzywe Béziera, podczas gdy delphiowska funkcja GDI PolyBezierTo przyjmuje krzywe sześcienne, więc każdy segment kwadratowy musi zostać dokładnie podniesiony o stopień (degree-elevated), zamiast być upraszczany do odcinków prostych

// Dokładne podniesienie stopnia: krzywa kwadratowa (P0, Q, P2) -> sześcienna (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0),  C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// następnie PolyBezierTo z punktami C1, C2, P2 — geometrycznie identyczna krzywa

Podniesienie stopnia jest bezstratne: krzywa sześcienna opisuje identyczny łuk, więc wyrenderowany obrys odpowiada temu, co wyświetla zgodna ze specyfikacją przeglądarka z tej samej tabeli, przy dowolnym powiększeniu. Pozostała część zadania to pozycjonowanie. Każdy glif jest opisany w jednostkach czcionki (zazwyczaj siatka 1000 lub 2048 jednostek na em), a renderer łączy macierz skali, macierz tekstu oraz bieżącą macierz transformacji w jedną transformację glifu na urządzenie przed wypełnieniem ścieżki. Kolejność ma tu większe znaczenie, niż mogłoby się wydawać: błędne złożenie tych trzech macierzy w odwrotnej kolejności sprawi, że każdy glif zapadnie się w kierunku początku układu współrzędnych — co da zniekształconą stronę, za którą stoi błąd w jednej linijce algebry macierzy

Jak interpreter opisów znaków (charstring) typu Type 2 obsługuje czcionki CFF

Klasa THPDFEmbeddedCFF zapewnia programom z sekcji FontFile3 pełny interpreter opisów znaków (charstrings) typu Type 2: analizuje struktury CFF INDEX, słowniki Top DICT oraz Private DICT, a następnie wykonuje każdy opis i przesyła segmenty ścieżek bezpośrednio do GDI. Opakowanie OpenType (kontener OTTO) jest najpierw usuwane, aby dotrzeć do czystej tabeli CFF; surowe strumienie CIDFontType0C i Type1C są pobierane bezpośrednio. Opisy charstrings to kompaktowy język stosowy, a o poprawności jego interpretacji decydują trzy zasady. Opcjonalny prefiks szerokości oznacza, że pierwszy operator czyszczący stos może nieść dodatkowy, początkowy argument. Operator hintmask implikuje wywołanie vstemhm, gdy argumenty są jeszcze na stosie, a liczba bajtów maski do pominięcia zależy od zgromadzonej liczby trzonów (stems) — jeden błąd w zliczeniu powoduje błędną interpretację kolejnych kodów operacji. Z kolei wywołania podprogramów dodają przesunięcie (bias) do swojego indeksu (107, 1131 lub 32768, w zależności od liczby podprogramów) przed wyszukiwaniem, więc wywołanie bez tego przesunięcia wskaże zupełnie inny podprogram

Czcionki CFF kluczowane za pomocą CID wprowadzają jeden stopień pośredniości, na którym zawodzą uproszczone implementacje: kod znaku wybiera identyfikator CID, lecz indeksem opisu znaku jest GID, a zestaw znaków czcionki (charset) mapuje GID na CID — stąd renderer musi zbudować odwrotne mapowanie CID-na-GID przed rysowaniem i wybierać słownik Private DICT dla każdego glifu za pomocą FDSelect w przypadku czcionek zawierających kilka takich słowników. Z kolei programy Type1C kluczowane nazwami (zwykłe źródło dla czcionek Type 1) rozwiązują jednobajtowe kody za pomocą wbudowanego kodowania programu CFF lub poprzez opisany dalej mechanizm kodowania na poziomie pliku PDF. Jedno szczere zastrzeżenie: interpreter odczytuje operatory hint w celu zachowania synchronizacji strumienia, lecz nie wykonuje samego hinting (wygładzania w siatce pikseli), co omówiono na końcu

Czym jest czcionka Type 3 i jak jest rysowana?

Glif typu Type 3 w ogóle nie jest obrysem — sekcja ISO 32000-1 §9.6.5 definiuje go jako strumień zawartości, stąd HotPDF renderuje go poprzez zapisanie stanu grafiki, złożenie macierzy czcionki, rozmiaru czcionki i macierzy tekstu w macierz CTM, a następnie wykonanie procedury glifu przez ten sam interpreter operatorów, który rysuje strony, z uwzględnieniem zasobów /Resources czcionki. Dwa szczegóły specyfikacji decydują o poprawności. Szerokości /Widths czcionek Type 3 są wyrażane w przestrzeni glifu, a nie w przestrzeni tekstu 1/1000 używanej przez wszystkie inne czcionki, więc przesunięcia (advances) muszą przechodzić przez /FontMatrix — w przeciwnym razie czcionki kodów kreskowych z macierzą 0.01 będą miały zaburzone odstępy o rząd wielkości. Po drugie, procedura glifu rozpoczynająca się operatorem d1 nakłada dwa warunki, które renderer musi wymusić: rysowanie jest ograniczane do zadeklarowanego kontenera (bounding box), a zgodnie z ISO 32000-1 §9.6.5.2 glif ignoruje własne operatory kolorów i rysuje się bieżącym kolorem wypełnienia wywołującego, stąd operatory rg, g, k oraz ich odpowiedniki dla obrysów wewnątrz procedury są ignorowane na czas rysowania glifu. Pominięcie reguły koloru sprawi, że czcionka kodu kreskowego d1 naniesiona na niebiesko przez stronę wyrenderuje się na czarno; pominięcie ograniczenia sprawi, że niepoprawnie zbudowany glif wyjdzie poza swoją komórkę

Jak kody znaków stają się identyfikatorami glifów

Interpretery obrysów to tylko połowa sukcesu, ponieważ bajty w ciągu tekstowym PDF to kody znaków, a nie indeksy glifów, i standard ISO 32000-1 poświęca temu mapowaniu dwa podrozdziały. Dla czcionek prostych sekcja §9.6.6 definiuje ścisły priorytet: tablica /Differences nadpisuje kodowanie bazowe (WinAnsiEncoding, MacRomanEncoding lub StandardEncoding), które z kolei nadpisuje własną mapę programu czcionki. HotPDF przekształca ten łańcuch w 256-pozycyjną tabelę kodów do GID, tłumacząc nazwy glifów na indeksy glifów na trzy sposoby: poprzez dokładne dopasowanie zestawu znaków w programie CFF, numeryczne nazwy typu gNN/glyphNN traktowane jako dosłowne indeksy oraz konwersję nazw na Unicode według listy Adobe Glyph List, po której następuje wyszukiwanie w tabeli cmap dla programów TrueType. Dla czcionek złożonych sekcja §9.7 powierza to mapowanie kluczowi CIDToGIDMap: najczęstszym przypadkiem jest /Identity, lecz wpis ten może być strumieniem par typu big-endian indeksowanych przez CID — a wyjściowy kod Unicode generowany przez HotPDF używa dokładnie tej formy strumienia dla kompaktowych podzbiorów, stąd ścieżka ta nie jest rzadkim przypadkiem

// /CIDToGIDMap jako strumień: pary słów (Word) typu big-endian indeksowane przez CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // wartości poza zakresem są mapowane na .notdef

Gdy wymagane jest wyszukiwanie w tabeli cmap TrueType, HotPDF przechodzi po łańcuchu wariantów awaryjnych (fallback) zamiast polegać na jednej podtabeli: pierwszeństwo mają podtabele Windows Unicode (format 4, następnie format 12 dla płaszczyzn uzupełniających), po nich występuje podtabela symboli (3,0) z konwencją obszaru prywatnego (private-use-area) F000 zrzutowaną na młodszy bajt — to powód, dla którego czcionka symboliczna typu Wingdings odpowiada na zwykłe kody ASCII — a na koniec tradycyjne formaty 6 oraz 0. Podtabele formatu 2 celowo nie są interpretowane: mapują one tradycyjne wielobajtowe strony kodowe, takie jak Shift-JIS i Big5, a nie Unicode, a współczesne czcionki CJK i tak niezmiennie zawierają podtabelę formatu 4 lub formatu 12. Kod, który nie pasuje do żadnej z tych ścieżek, wycofuje się do rysowania systemowego GDI dla tego pojedynczego glifu, więc jeden niemapowalny znak zniekształca tylko ten glif, a nie cały blok tekstu

Czego nie robi potok wbudowany

Wygładzanie (hinting) nie jest wykonywane — obrysy są wypełniane w oryginalnym kształcie, co przy rozdzielczości 150 DPI i wyższej jest nie do odróżnienia od wygładzonego obrazu, lecz przy bardzo małych rozmiarach może różnić się o pojedyncze piksele od wygładzonego rastra. Oryginalne programy Type 1 w sekcji FontFile (opisy charstrings szyfrowane eexec) nie są interpretowane, a osie czcionek zmiennych (variable fonts) OpenType nie są stosowane. W obu tych sytuacjach — jak również przy uszkodzonym programie czcionki czy tabeli glyf bez poprawnego mapowania cmap — następuje wycofanie do rysowania systemowego, zamiast przerywania renderowania strony. To samo podejście stawiające na dokładność obowiązuje w innych miejscach renderera — np. wzorce cieniowania osiowego i radialnego są traktowane tak dokładnie, jak na to zasługują gradienty — a strona generowania czcionek ma własny rozdział opisany w artykule o porządkowaniu podzbiorów czcionek przez EndDoc

Korzystanie z potoku nie wymaga żadnego kodu specyficznego dla czcionek — każdy z powyższych mechanizmów uruchamia się automatycznie wewnątrz wywołania renderowania strony

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Wbudowane czcionki TrueType, CFF oraz Type 3 renderują się bezpośrednio
      // z pliku — nie ma potrzeby instalowania czegokolwiek w systemie
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Praktyczny skutek to ten, na którym najbardziej zależy działowi wsparcia: plik PDF niosący ze sobą czcionki renderuje się z ich użyciem na serwerze budowania, w kontenerze Windows czy na pulpicie klienta, który nigdy nie posiadał danej czcionki. Potok renderowania wbudowanych glifów wchodzi w skład HotPDF Component dla Delphi i C++Builder — natywnej biblioteki VCL obsługującej tworzenie, edycję, ekstrakcję tekstu oraz renderowanie stron PDF bez żadnych zewnętrznych bibliotek DLL