Artykuł techniczny

Renderowanie fontów PDF fontami systemu w Delphi

Gdy PDF nie osadza fontu, komponent HotPDF renderuje ten tekst zainstalowanym fontem Windows wybranym przez HPDFMapBaseFontToSystem: dekoduje nazwę /BaseFont, obcina część stylową, próbuje kilku pisowni, dopóki GDI nie potwierdzi, że rodzina jest zainstalowana, mierzy brakujące szerokości standard 14 na fontach zgodnych metrycznie i konwertuje jednobajtowe kody na Unicode przed rysowaniem. Każdy z tych kroków istnieje, bo naiwna wersja wywalała się na prawdziwych plikach. Renderer stron RenderLoadedPageToBitmap radzi sobie dobrze z osadzonymi programami; to historia fontów, których w pliku w ogóle nie ma

Dlaczego GDI po cichu rysuje złą odmianę pisma dla nieosadzonego fontu?

GDI nigdy nie zgłasza brakującego fontu: podaj CreateFontIndirect nazwę odmiany, której nie zna, a po cichu wybierze zamiennik, często inny serif bez grubości bold. Wczesny renderer przekazywał nazwę PDF niemal dosłownie, więc TimesNewRoman,Bold, TimesNewRomanPS-BoldMT i SegoeUI-Semibold niczego nie łapały i wychodziły w tym, co GDI wybrało. Nazwy potrafią być gorsze. ISO 32000-1 §7.3.5 pozwala nazwie zapisać każdy bajt jako #xx, a producenci CJK rutynowo literują nazwy fontów jako escaped UTF-8 albo bajty starszych stron kodowych; przed v2.766.69 same ucieczki stawały się nazwą odmiany. HPDFMapBaseFontToSystem dekoduje teraz najpierw ucieczki, zwraca poprawną sekwencję bajtów UTF-8 jako swoje znaki i czyta pozostałe wysokie bajty w systemowej stronie kodowej

Obcinanie stylu to miejsce, gdzie heurystyki gryzą. Przecinek zawsze kończy rodzinę (Arial,Bold daje Arial), ale łącznik tylko wtedy, gdy słowo po nim jest stylem: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra albo Condensed. Ta reguła trzyma MS-Mincho w całości, zamieniając Calibri-Light w Calibri. HotPDF próbuje potem pisowni rodziny, a za nimi pisowni rodziny z obciętym sufiksem PSMT, MT albo PS. Od v2.768.18 te pisownie pokrywają każdy wybór spacji w miejscach, gdzie może zacząć się słowo: przed wielką literą po małej (MyriadPro staje się Myriad Pro), przy ostatniej wielkiej ciągu, po którym następuje mała (UIGothic), i po wiodącym MS (MSPGothic), od w pełni rozdzielanej postaci aż po nazwę tak, jak zapisano; ponad cztery takie miejsca ograniczają się do w pełni rozdzielanej i sklejonej nazwy. Spacji nie da się stosować na ślepo, bo Windows część słów trzyma sklejone: SimSun jest zainstalowany dokładnie pod tą pisownią, podczas gdy MicrosoftYaHei, MicrosoftJhengHei i MSPGothic należą do Microsoft YaHei, Microsoft JhengHei i MS PGothic. Przed v2.768.18 mapper stawiał spację przed każdą wewnętrzną wielką literą, więc MicrosoftYaHei było wyszukiwane jako Microsoft Ya Hei i nigdy nie było znajdowane. Kandydat liczy się jako zainstalowany, gdy CreateFontIndirect, a za nim GetTextFace, zwracają żądaną nazwę albo, od v2.768.18, gdy tabela name wybranego fontu wymienia go jako rodzinę, pełną albo typograficzną nazwę rodziny w dowolnym języku; odpowiedź jest cache'owana per nazwa, więc dokumenty z wieloma niezainstalowanymi fontami nie dopytują już Windows o każdą nazwę na każdej stronie

Potok HotPDF renderujący nieosadzone fonty PDF fontami systemowymi w Delphi: HPDFMapBaseFontToSystem dekoduje bajty ucieczkowe #xx w nazwie /BaseFont, obcina sufiksy stylowe Bold, Italic i Light, trzymając MS-Mincho w całości, buduje pisownie kandydackie jak Myriad Pro i Microsoft YaHei i przyjmuje jedną tylko wtedy, gdy GetTextFace albo tabela nazw fontu potwierdzi zainstalowaną nazwę
GDI nigdy nie zgłasza brakującego fontu, po cichu podstawia zamiennik — mapowanie próbuje kandydatów po kolei i ufa tylko nazwie, którą GDI odda albo którą wymienia tabela nazw wybranego fontu

Ponieważ funkcja mapująca jest publiczna w jednostce HPDFRenderFontMetrics, raport wstępny może pokazać, z którą zainstalowaną rodziną wyrenderuje się każdy nieosadzony font, korzystając z wyliczania fontów, które THotPDF już wystawia dla wczytanych dokumentów:

uses
  HPDFDoc, HPDFRenderFontMetrics;

procedure ListSystemFontMappings(Pdf: THotPDF; Log: TStrings);
var
  Page, I: Integer;
  Info: THPDFLoadedFontInfo;
begin
  for Page := 0 to Pdf.LoadedPageCount - 1 do
    for I := 0 to Pdf.GetLoadedFontCount(Page) - 1 do
      if Pdf.GetLoadedFontInfo(Page, I, Info) and not Info.IsEmbedded then
        Log.Add(Format('page %d  /%s  %s -> %s',
          [Page + 1, string(Info.ResourceName), string(Info.FontName),
           HPDFMapBaseFontToSystem(Info.FontName)]));
end;

Jak HotPDF mierzy standardowe 14 fontów bez /Widths?

HotPDF mierzy brakujące posuwy na zainstalowanym fontie o tych samych metrykach, bo ISO 32000-1 §9.6.2.2 pozwala standardowym 14 fontom pominąć /Widths, a biblioteka nie wozi ze sobą tabel AFM. Arial niesie metryki Helvetica, Times New Roman niesie Times, a Courier New niesie Courier, więc HPDFMeasureBaseFontWidths tworzy pasującą odmianę przy lfHeight = -1000 i woła GetCharWidth32W; przy tej wysokości wynik jest już w jednostkach 1/1000 em, którymi posługują się szerokości PDF. Renderer najpierw zamienia każdy kod na Unicode przez /Encoding, /BaseEncoding i /Differences, z domyślnym StandardEncoding. Eksport SVG i ekstrakcja tekstu wpadły w jeszcze jedną pułapkę: standardowy font Type 1 bez jakiegokolwiek /Encoding produkował dekoder bez informacji o kodowaniu, eksport SVG nigdy go nie rejestrował i żadna zmierzona szerokość nie była używana. Podanie implikowanego StandardEncoding naprawiło sprawę, pod warunkiem oznaczenia go jako kodowanie predefiniowane; skierowanie go ścieżką nazwy CMap dekoduje każdy kod jako 0 i każda szerokość za tym idzie

Bold, italic i przesunięcie o jeden w flagach deskryptora fontu

Wpis /Flags deskryptora fontu numeruje swoje bity od 1, nie od 0, więc ForceBold to bit 19 ($40000), a Italic to bit 7 ($40), zgodnie z tabelą 123 ISO 32000-1. Stary kod testował $20000, czyli bit 18, SmallCap. Pomyłka przeżyła od v2.345.0 do v2.766.53, bo /FontDescriptor jest niemal zawsze referencją pośrednią, a budowniczy fontów czytał tylko obiekty bezpośrednie, więc cała gałąź flag nigdy się nie uruchamiała, i ta sama ślepota ignorowała /Widths 12 0 R, składając tekst z zastępczym posuwem 500 jednostek. Gdy v2.766.53 zaczęło rozwiązywać referencje pośrednie przez renderer, bit trzeba było poprawić w tej samej zmianie, inaczej każda odmiana kapitalików nagle renderowałaby się jako bold:

const
  // Tabela 123 ISO 32000-1 liczy pozycje bitów od 1
  FD_ITALIC     = $00040;  // bit 7
  FD_SMALLCAP   = $20000;  // bit 18, nie grubość
  FD_FORCEBOLD  = $40000;  // bit 19

procedure ApplyDescriptorFlags(Flags: Integer; var LF: TLogFont);
begin
  if (Flags and FD_FORCEBOLD) <> 0 then
    LF.lfWeight := FW_BOLD;
  if (Flags and FD_ITALIC) <> 0 then
    LF.lfItalic := 1;
end;
Numeracja bitów wpisu /Flags deskryptora fontu PDF: tabela 123 ISO 32000-1 liczy od bitu 1, więc Italic to $40 przy bicie 7, SmallCap $20000 przy bicie 18, a ForceBold $40000 przy bicie 19, więc test deskryptora w HotPDF sprawdzający $20000 celował w SmallCap i pozostawał nieszkodliwy tylko dopóki pośrednie referencje /FontDescriptor nie były rozwiązywane
Gałąź flag była martwym kodem przez czterdzieści wersji, bo deskryptor był pośredni — gdy referencje zaczęły się rozwiązywać, bit przekłamany o jeden stał się widocznym tekstem kapitalikami

Dlaczego fonty CJK z CMap UCS2 dostają złe szerokości?

Tekst CJK z predefiniowanym CMap UCS2 rysuje właściwe glify, ale złe odstępy, gdy renderer traktuje kod jak CID, bo /W jest indeksowana CID-ami, a nie kodami znaków. Przy STSong-Light i UniGB-UCS2-H kod akurat równa się wartości Unicode, więc GDI rysuje właściwe znaki, a błąd kryje się w posuwach: małe litery przychodzą jako kody 97 i wyżej, wypadają poza wpis /W takim jak [1 95 500] i wszystkie dostają domyślną szerokość /DW równą 1000. Od v2.766.56 renderer HotPDF czyta kody przez zakresy codespace CMap (ISO 32000-1 §9.7.6.2) i mapuje je na CID-y, zanim zacznie szukać szerokości. Używane są tylko wbudowane tabele UCS2 i UTF16 oraz osadzone strumienie CMap; przybliżenie tożsamościowe dla czegoś takiego jak GBK-EUC-H jedynie udawałoby wsparcie, produkując zły wynik, więc renderer nie udaje

Dlaczego tekst CJK z CMap UCS2 rysuje właściwe glify przy złych posuwach: /W jest indeksowana CID-ami, podczas gdy kody to wartości Unicode, więc przy STSong-Light i UniGB-UCS2-H kody małych liter od 97 wzwyż miną wpis /W [1 95 500] i wezmą domyślne /DW, naprawione w HotPDF przez mapowanie kodów na CID-y zakresami codespace CMap
Błąd się kryje, bo tutaj kod równa się Unicode — glify wyglądają dobrze, podczas gdy każdy posuw po cichu spada na domyślny, więc oceniaj renderowanie CJK po odstępach, nie po kształtach

Dlaczego znaki z akcentami zmieniają się w znaki zapytania na chińskim Windows?

Jednobajtowe kody nie mogą nigdy dotrzeć do funkcji GDI ANSI („A"), bo GetGlyphOutlineA i GetGlyphIndicesA interpretują bajty w systemowej stronie kodowej, podczas gdy TextOutA używa zestawu znaków wybranego fontu. Na chińskim systemie bajt $A9 Ariala (znak copyright w Windows-1252) stawał się wiodącym bajtem GBK i renderował się jako „?", w co prosto wdepnęła ścieżka konturów bez hintingu dodana w v2.766.83. v2.767.3 dopytuje zrealizowany font o jego zestaw znaków przez GetTextCharset, przelicza to na stronę kodową przez TranslateCharsetInfo, przepuszcza bajt przez MultiByteToWideChar i woła funkcje W; fonty symboli używają zamiast tego U+F000 plus kod. Kodowania niezgodne z Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — są mapowane na Unicode, zanim zobaczy je jakikolwiek font systemowy

Jakie są granice rysowania fontami systemowymi?

Renderowanie fontami systemowymi to przybliżenie, i komponent HotPDF mówi szczerze, gdzie się kończy. Przed v2.768.18 sprawdzenie instalacji porównywało tylko nazwę, którą zwraca GetTextFace, a na zlokalizowanym Windows ta funkcja raportuje nazwę rodziny w języku systemu, więc Microsoft YaHei na chińskim Windows albo Yu Mincho na japońskim był uznawany za brakujący i rysowany zamiennikiem GDI; od v2.768.18 odmiana wracająca pod inną nazwą jest też wyszukiwana w tabeli name fontu i takie fonty są znajdowane. Zgodność metryk jest gwarantowana tylko dla rodzin Helvetica, Times i Courier; Symbol mapuje się na Symbol, a ZapfDingbats na Wingdings, co jest protezą, nie dopasowaniem. Preflight z góry widzi też tylko fonty w słowniku /Resources każdej strony, nie te wskazywane z wnętrza form XObjects. Gdy kodu nadal nie da się narysować, raportuje to śledzenie nierozwiązanych glifów w czasie rysowania, co jest lepszym sygnałem niż oglądanie miniatur

Trwała poprawka siedzi po stronie autorskiej. HotPDF sam pisze z FontEmbedding ustawionym domyślnie na True, podstawiając osadzony Arial nawet, gdy kod woła SetFont z Helvetica, a tekst osadzony przechodzi przez renderer glifów fontów osadzonych zamiast przez jakiekolwiek zgadywanie z góry. Tanią strażą dla przychodzących plików jest ostrzeżenie przed renderowaniem, gdy zmapowanej rodziny nie ma na liście fontów ekranu:

// VCL: Screen.Fonts wymienia zainstalowane nazwy rodzin (jednostka Forms)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

Pełny komponent, łącznie z renderowaniem stron, ekstrakcją tekstu i subsetowaniem fontów po stronie zapisu, opisuje strona produktu HotPDF Delphi PDF component