Technischer Artikel

PDF-Fonts ohne Einbettung in Delphi mit System-Fonts rendern

Lässt eine PDF einen Font nicht einbetten, rendert die HotPDF-Komponente diesen Text mit einem installierten Windows-Font, den HPDFMapBaseFontToSystem auswählt: Sie dekodiert den /BaseFont-Namen, schneidet den Stil-Anteil ab, probiert mehrere Schreibweisen durch, bis GDI bestätigt, dass die Familie installiert ist, misst fehlende Standard-14-Breiten an metrikkompatiblen Fonts und wandelt Einbyte-Codes vor dem Zeichnen in Unicode um. Jeder dieser Schritte existiert, weil die naive Version an echten Dateien scheiterte. Der RenderLoadedPageToBitmap-Seiten-Renderer kommt mit eingebetteten Programmen gut klar; dies hier ist die Geschichte der Fonts, die gar nicht in der Datei sind

Warum zeichnet GDI lautlos die falsche Schrift für einen nicht eingebetteten Font?

GDI meldet nie einen fehlenden Font: Gibt man CreateFontIndirect einen ihm unbekannten Namen, wählt es lautlos einen Ersatz, oft eine andere Serifenschrift ohne Bold-Schnitt. Der frühe Renderer reichte den PDF-Namen fast unverändert durch, TimesNewRoman,Bold, TimesNewRomanPS-BoldMT und SegoeUI-Semibold trafen also nichts und kamen in dem heraus, was GDI zufällig wählte. Namen können noch übler sein. ISO 32000-1 §7.3.5 erlaubt, ein beliebiges Byte als #xx zu schreiben, und CJK-Producer buchstabieren Fontnamen routinemäßig als escapte UTF-8- oder Legacy-Codepage-Bytes; vor v2.766.69 wurden die Escapes selbst zum Namen. HPDFMapBaseFontToSystem dekodiert die Escapes jetzt zuerst, gibt eine gültige UTF-8-Bytefolge als ihre Zeichen zurück und liest andere hohe Bytes in der System-Codepage

Wer den Stil abschneidet, stößt auf Heuristik-Gelände. Ein Komma beendet immer die Familie (Arial,Bold ergibt Arial), aber ein Bindestrich nur dann, wenn das Wort dahinter ein Stil ist: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra oder Condensed. Diese Regel lässt MS-Mincho ganz und macht aus Calibri-Light ein Calibri. HotPDF probiert dann Schreibweisen der Familie, gefolgt von Schreibweisen der Familie ohne PSMT-, MT- oder PS-Suffix. Seit v2.768.18 decken diese Schreibweisen jede Wahl von Leerzeichen an den Stellen ab, an denen ein Wort beginnen kann: vor einem Großbuchstaben, dem ein Kleinbuchstabe vorangeht (MyriadPro wird zu Myriad Pro), am letzten Großbuchstaben einer Folge, der ein Kleinbuchstabe folgt (UIGothic), und nach einem führenden MS (MSPGothic) — von der vollständig getrennten Form bis hinunter zum Namen, wie er dasteht; über vier solcher Stellen hinaus werden nur die getrennte und die zusammengeschriebene Form probiert. Leerzeichen darf man nicht blind setzen, denn Windows hält manche Wörter zusammen: SimSun ist exakt unter dieser Schreibweise installiert, während MicrosoftYaHei, MicrosoftJhengHei und MSPGothic zu Microsoft YaHei, Microsoft JhengHei und MS PGothic gehören. Vor v2.768.18 setzte der Mapper vor jeden inneren Großbuchstaben ein Leerzeichen, MicrosoftYaHei wurde also als Microsoft Ya Hei gesucht und nie gefunden. Ein Kandidat gilt als installiert, wenn CreateFontIndirect gefolgt von GetTextFace den angeforderten Namen zurückliefert oder — seit v2.768.18 — wenn die name-Tabelle des gewählten Fonts ihn als Familie führt, als voller oder typografischer Familienname in jeder Sprache; die Antwort wird pro Name gecacht, Dokumente mit vielen nicht installierten Fonts befragen Windows also nicht mehr bei jedem Namen auf jeder Seite

Die HotPDF-Pipeline, die nicht eingebettete PDF-Fonts in Delphi mit System-Fonts rendert: HPDFMapBaseFontToSystem dekodiert #xx-escapte Bytes im /BaseFont-Namen, schneidet Bold-, Italic- und Light-Stil-Suffixe ab, während MS-Mincho ganz bleibt, baut Kandidaten-Schreibweisen wie Myriad Pro und Microsoft YaHei auf und akzeptiert eine erst, wenn GetTextFace oder die Namen-Tabelle des Fonts den installierten Namen bestätigt
GDI meldet nie einen fehlenden Font, es substituiert lautlos — das Mapping probiert seine Kandidaten der Reihe nach und vertraut nur einem Namen, den GDI zurückgibt oder die Namen-Tabelle des gewählten Fonts führt

Weil die Mapping-Funktion in der Unit HPDFRenderFontMetrics öffentlich ist, kann ein Preflight-Report zeigen, mit welcher installierten Familie jeder nicht eingebettete Font gerendert wird — mit der Font-Enumeration, die THotPDF für geladene Dokumente ohnehin anbietet:

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;

Wie misst HotPDF Standard-14-Fonts ohne /Widths?

HotPDF misst die fehlenden Advances am installierten Font mit denselben Metriken, denn ISO 32000-1 §9.6.2.2 erlaubt den Standard-14-Fonts, /Widths wegzulassen, und die Bibliothek schifft keine AFM-Tabellen mit. Arial trägt Helvetica-Metriken, Times New Roman trägt Times und Courier New trägt Courier, HPDFMeasureBaseFontWidths erzeugt also das passende Face bei lfHeight = -1000 und ruft GetCharWidth32W; bei dieser Höhe ist das Ergebnis bereits in den 1/1000-em-Einheiten, die PDF-Breiten benutzen. Der Renderer wandelt jeden Code zuerst über /Encoding, /BaseEncoding und /Differences in Unicode um, mit StandardEncoding als Default. SVG-Export und Textextraktion stolpern über eine weitere Falle: Ein Standard-Type-1-Font ganz ohne /Encoding erzeugte einen Decoder ohne Encoding-Information, der SVG-Export registrierte ihn nie, und jede gemessene Breite blieb ungenutzt. Die stillschweigend angenommene StandardEncoding zu liefern, fixte es — vorausgesetzt, sie ist als vordefiniertes Encoding markiert; leitet man sie stattdessen in den CMap-Namen-Pfad, dekodiert jeder Code als 0 und jede Breite folgt diesem Wert

Bold, Italic und ein Off-by-one in den Font-Descriptor-Flags

Der /Flags-Eintrag eines Font-Descriptors nummeriert seine Bits ab 1, nicht ab 0, ForceBold ist also Bit 19 ($40000) und Italic ist Bit 7 ($40), gemäß ISO 32000-1, Tabelle 123. Der alte Code testete $20000 — Bit 18, SmallCap. Der Fehler überlebte von v2.345.0 bis v2.766.53, weil /FontDescriptor fast immer eine indirekte Referenz ist und der Font-Builder nur direkte Objekte las, der ganze Flags-Zweig lief also nie, und dieselbe Blindheit ignorierte /Widths 12 0 R und setzte Text mit einem 500-Einheiten-Fallback-Advance. Als v2.766.53 begann, indirekte Referenzen durch den Renderer aufzulösen, musste das Bit im selben Zug korrigiert werden, sonst hätte jede Small-Caps-Schrift plötzlich fett gerendert:

const
  // ISO 32000-1, Tabelle 123, zählt Bitpositionen ab 1
  FD_ITALIC     = $00040;  // Bit 7
  FD_SMALLCAP   = $20000;  // Bit 18, keine Gewichtung
  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;
Bit-Nummerierung des /Flags-Eintrags eines PDF-Font-Descriptors: ISO 32000-1, Tabelle 123, zählt ab Bit 1 — Italic ist $40 an Bit 7, SmallCap $20000 an Bit 18 und ForceBold $40000 an Bit 19; HotPDFs Descriptor-Test von $20000 zielte also auf SmallCap und blieb nur solange harmlos, wie indirekte /FontDescriptor-Referenzen nie aufgelöst wurden
Der Flag-Zweig war vierzig Versionen lang toter Code, weil der Descriptor indirekt war — kaum lösten sich die Referenzen auf, wurde das Off-by-one-Bit zu sichtbarem Small-Caps-Text

Warum bekommen CJK-Fonts mit UCS2-CMap die falschen Breiten?

CJK-Text mit einer vordefinierten UCS2-CMap zeichnet die richtigen Glyphen, aber die falschen Abstände, wenn ein Renderer den Code als CID behandelt — denn /W ist per CID indiziert, nicht per Zeichencode. Mit STSong-Light und UniGB-UCS2-H fällt der Code zufällig mit dem Unicode-Wert zusammen, GDI zeichnet also die richtigen Zeichen, und der Bug versteckt sich in den Advances: Kleinbuchstaben kommen als Codes ab 97 an, liegen außerhalb eines /W-Eintrags wie [1 95 500] und bekommen alle die Default-Breite /DW von 1000. Seit v2.766.56 liest der HotPDF-Renderer Codes durch die CMap-Codespace-Bereiche (ISO 32000-1 §9.7.6.2) und bildet sie auf CIDs ab, bevor er Breiten nachschlägt. Nur die eingebauten UCS2- und UTF16-Tabellen und eingebettete CMap-Streams werden benutzt; eine Identity-Näherung für etwas wie GBK-EUC-H würde nur so tun, als sei sie unterstützt, während sie falsche Ausgabe produziert — der Renderer gibt also nichts vor

Warum CJK-Text mit UCS2-CMap die richtigen Glyphen an den falschen Advances zeichnet: /W ist per CID indiziert, während die Codes Unicode-Werte sind — mit STSong-Light und UniGB-UCS2-H verfehlen Kleinbuchstaben-Codes ab 97 den /W-Eintrag [1 95 500] und nehmen den /DW-Default, gefixt in HotPDF durch das Abbilden der Codes auf CIDs über CMap-Codespace-Bereiche
Der Bug versteckt sich, weil der Code hier dem Unicode entspricht — die Glyphen sehen richtig aus, während jeder Advance lautlos auf den Default fällt, beurteilen Sie CJK-Rendering also am Abstand, nicht an den Formen

Warum werden akzentuierte Zeichen auf chinesischem Windows zu Fragezeichen?

Einbyte-Codes dürfen nie an die ANSI-(„A“)-GDI-Funktionen durchgereicht werden, denn GetGlyphOutlineA und GetGlyphIndicesA interpretieren Bytes in der System-Codepage, während TextOutA den Zeichensatz des gewählten Fonts benutzt. Auf einem chinesischen System wurde das Arial-Byte $A9 (das Copyright-Zeichen in Windows-1252) zu einem GBK-Führende-Byte und als „?“ gerendert — eine Falle, in die der in v2.766.83 hinzugekommene ungehintete Outline-Pfad direkt hineinstolperte. v2.767.3 fragt den realisierten Font mit GetTextCharset nach seinem Zeichensatz, wandelt ihn über TranslateCharsetInfo in eine Codepage um, schickt das Byte durch MultiByteToWideChar und ruft die W-Funktionen; Symbol-Fonts benutzen stattdessen U+F000 plus den Code. Encodings, die mit Windows-1252 nicht einverstanden sind — /Differences, StandardEncoding, MacRomanEncoding — werden in Unicode umgesetzt, bevor irgendein System-Font sie sieht

Wo liegen die Grenzen des Zeichnens mit System-Fonts?

System-Font-Rendering ist eine Näherung, und die HotPDF-Komponente sagt ehrlich, wo sie aufhört. Vor v2.768.18 verglich der Installations-Check nur den Namen, den GetTextFace zurückgibt, und auf lokalisiertem Windows meldet diese Funktion den Familiennamen in der Systemsprache — Microsoft YaHei auf chinesischem Windows oder Yu Mincho auf japanischem Windows galt also als fehlend und wurde in einem GDI-Ersatz gezeichnet; seit v2.768.18 wird ein Face, das unter anderem Namen zurückkommt, ebenfalls in der name-Tabelle des Fonts nachgeschlagen, und solche Fonts werden gefunden. Metrische Kompatibilität ist nur für die Familien Helvetica, Times und Courier garantiert; Symbol mappt auf Symbol und ZapfDingbats auf Wingdings, was ein Notbehelf statt einer Entsprechung ist. Der Preflight oben sieht außerdem nur Fonts im /Resources-Dictionary jeder Seite, nicht die, die aus Form XObjects heraus referenziert werden. Lässt sich ein Code immer noch nicht zeichnen, meldet ihn das Tracking nicht aufgelöster Glyphen zur Zeichenzeit — ein besseres Signal als der Blick auf Thumbnails

Die dauerhafte Kurve sitzt auf der Autor-Seite. HotPDF selbst schreibt mit FontEmbedding standardmäßig auf True, ersetzt also einen eingebetteten Arial sogar dann, wenn der Code SetFont mit Helvetica aufruft, und eingebetteter Text läuft durch den eingebetteten Font-Glyphen-Renderer statt durch eines der Rateverfahren oben. Eine billige Absicherung für eingehende Dateien ist eine Warnung vor dem Rendern, wenn eine gemappte Familie nicht in der Screen-Font-Liste steht:

// VCL: Screen.Fonts listet installierte Familiennamen auf (Forms-Unit)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

Für die vollständige Komponente, inklusive Seiten-Rendering, Textextraktion und Font-Subsetting auf der Schreibseite, siehe die HotPDF Delphi PDF component Produktseite