Technisch artikel

Niet-ingesloten PDF-fonts met systeemfonts renderen

Als een PDF een font niet insluit, rendert het HotPDF-component die tekst met een geïnstalleerd Windows-font dat HPDFMapBaseFontToSystem kiest: hij decodeert de /BaseFont-naam, haalt het stijldeel eraf, probeert meerdere spellingen tot GDI bevestigt dat de familie geïnstalleerd is, meet ontbrekende standard 14-breedtes vanuit metrisch compatibele fonts, en zet single-byte codes om naar Unicode vóór het tekenen. Elk van die stappen bestaat omdat de naieve versie op echte bestanden faalde. De RenderLoadedPageToBitmap-paginarender doet ingesloten programma's goed; dit is het verhaal van de fonts die helemaal niet in het bestand zitten

Waarom tekent GDI stilletjes het verkeerde lettertype voor een niet-ingesloten font?

GDI meldt nooit een ontbrekend font: geef CreateFontIndirect een facenaam die hij niet kent en hij kiest stilletjes een vervanger, vaak een andere serif zonder bold gewicht. De vroege renderer gaf de PDF-naam bijna letterlijk door, dus TimesNewRoman,Bold, TimesNewRomanPS-BoldMT en SegoeUI-Semibold matchten allemaal niets en kwamen eruit in wat GDI maar koos. Namen kunnen erger. ISO 32000-1 §7.3.5 laat een naam elke byte als #xx schrijven, en CJK-producenten spellen fontnamen routinematig als escaped UTF-8 of legacy code-page-bytes; vóór v2.766.69 werden de escapes zelf de facenaam. HPDFMapBaseFontToSystem decodeert de escapes nu eerst, geeft een geldige UTF-8-bytereeks terug als zijn tekens, en leest overige hoge bytes in de systeemcodepagina

Het stijldeel eraf halen is waar de heuristiek bijt. Een komma beëindigt altijd de familie (Arial,Bold geeft Arial), maar een koppelteken doet dat alleen als het woord erna een stijl is: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra of Condensed. Die regel houdt MS-Mincho heel terwijl Calibri-Light tot Calibri wordt. HotPDF probeert daarna spellingen van de familie, gevolgd door spellingen van de familie met een PSMT-, MT- of PS-achtervoegsel eraf. Sinds v2.768.18 dekken die spellingen elke keuze van spaties op de plekken waar een woord kan beginnen: vóór een hoofdletter die op een kleine letter volgt (MyriadPro wordt Myriad Pro), bij de laatste hoofdletter van een reeks waarop een kleine letter volgt (UIGothic), en na een voorloop-MS (MSPGothic), van de volledig gespatieerde vorm tot aan de naam zoals geschreven; voorbij vier van zulke plekken worden alleen de volledig gespatieerde en de aaneengeschreven naam geprobeerd. Spaties kunnen niet blind worden toegepast, want Windows houdt sommige woorden aan elkaar: SimSun is geïnstalleerd onder precies die spelling, terwijl MicrosoftYaHei, MicrosoftJhengHei en MSPGothic bij Microsoft YaHei, Microsoft JhengHei en MS PGothic horen. Vóór v2.768.18 zette de mapper een spatie vóór elke binnenhoofdletter, dus MicrosoftYaHei werd opgezocht als Microsoft Ya Hei en nooit gevonden. Een kandidaat telt als geïnstalleerd wanneer CreateFontIndirect gevolgd door GetTextFace de gevraagde naam teruggeeft of, sinds v2.768.18, wanneer de name-tabel van het gekozen font hem als familie vermeldt, als full of typographic family name in welke taal dan ook; het antwoord wordt per naam gecached, dus documenten met veel fonts die niet geïnstalleerd zijn bevragen Windows niet langer per naam op elke pagina

De HotPDF-pijplijn die niet-ingesloten PDF-fonts met systeemfonts in Delphi rendert: HPDFMapBaseFontToSystem decodeert #xx-escaped bytes in de /BaseFont-naam, haalt Bold-, Italic- en Light-stijlachtervoegsels eraf terwijl MS-Mincho heel blijft, bouwt kandidaatspellingen zoals Myriad Pro en Microsoft YaHei, en accepteert er één pas als GetTextFace of de font name table de geïnstalleerde naam bevestigt
GDI meldt nooit een ontbrekend font, hij substitueert stilletjes — de mapping probeert zijn kandidaten op volgorde en vertrouwt alleen een naam die GDI teruggeeft of die de name table van het gekozen font vermeldt

Omdat de mapping-functie publiek is in de unit HPDFRenderFontMetrics, kan een preflight-rapport tonen met welke geïnstalleerde familie elk niet-ingesloten font gaat renderen, met de font-enumeratie die THotPDF al aanbiedt voor geladen documenten:

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;

Hoe meet HotPDF standard 14-fonts zonder /Widths?

HotPDF meet de ontbrekende advances op het geïnstalleerde font met dezelfde metrics, want ISO 32000-1 §9.6.2.2 staat de standard 14-fonts toe om /Widths weg te laten en de library scheept geen AFM-tabellen in. Arial draagt de metrics van Helvetica, Times New Roman die van Times en Courier New die van Courier, dus HPDFMeasureBaseFontWidths maakt het bijpassende face aan op lfHeight = -1000 en roept GetCharWidth32W aan; op die hoogte zit het resultaat al in de 1/1000 em-eenheden die PDF-breedtes gebruiken. De renderer zet eerst elke code om naar Unicode via /Encoding, /BaseEncoding en /Differences, met StandardEncoding als default. SVG-export en tekstextractie lopen tegen nog een valkuil aan: een standard Type 1-font zonder enige /Encoding produceerde een decoder zonder encoding-informatie, SVG-export registreerde hem nooit, en elke gemeten breedte bleef ongebruikt. De impliciete StandardEncoding meeleveren loste het op, op voorwaarde dat hij als predefined encoding gemarkeerd is; hem via het CMap-naampad sturen decodeert elke code als 0 en elke breedte volgt

Bold, italic en een off-by-one in de font descriptor-flags

De /Flags-entry van een font descriptor nummert zijn bits vanaf 1, niet vanaf 0, dus ForceBold is bit 19 ($40000) en Italic is bit 7 ($40), volgens ISO 32000-1 Table 123. De oude code testte $20000, wat bit 18 is, SmallCap. De fout overleefde van v2.345.0 tot v2.766.53 omdat /FontDescriptor bijna altijd een indirecte referentie is en de fontbouwer alleen directe objecten las, dus de hele flags-tak draaide nooit, en dezelfde blindheid negeerde /Widths 12 0 R en zette tekst neer met een fallback advance van 500 units. Toen v2.766.53 indirecte referenties door de renderer ging oplossen, moest de bit in dezelfde wijziging gecorrigeerd worden, anders zou elk small-caps-face plotseling bold gerenderd hebben:

const
  // ISO 32000-1 Table 123 telt bitposities vanaf 1
  FD_ITALIC     = $00040;  // bit 7
  FD_SMALLCAP   = $20000;  // bit 18, geen gewicht
  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;
Bitnummering van de /Flags-entry van de PDF font descriptor: ISO 32000-1 Table 123 telt vanaf bit 1, waardoor Italic $40 op bit 7 is, SmallCap $20000 op bit 18 en ForceBold $40000 op bit 19, dus de descriptor-test van HotPDF op $20000 richtte zich op SmallCap en bleef alleen onschadelijk zolang indirecte /FontDescriptor-referenties nooit werden opgelost
De flags-tak was veertig versies lang dode code omdat de descriptor indirect was — zodra referenties opgelost werden, werd de off-by-one-bit zichtbaar als small-caps-tekst

Waarom krijgen CJK-fonts met een UCS2 CMap de verkeerde breedtes?

CJK-tekst met een predefined UCS2 CMap tekent de juiste glyphs maar de verkeerde spatiëring als een renderer de code als de CID behandelt, want /W is geïndexeerd op CID, niet op character code. Met STSong-Light en UniGB-UCS2-H is de code toevallig gelijk aan de Unicode-waarde, dus GDI tekent de juiste tekens en de bug schuilt in de advances: kleine letters komen binnen als codes 97 en hoger, vallen buiten een /W-entry zoals [1 95 500] en krijgen allemaal de default breedte /DW van 1000. Sinds v2.766.56 leest de HotPDF-renderer codes via de CMap codespace-bereiken (ISO 32000-1 §9.7.6.2) en mapt ze naar CIDs voordat hij breedtes opzoekt. Alleen de ingebouwde UCS2- en UTF16-tabellen en ingesloten CMap-streams worden gebruikt; een identity-benadering voor zoiets als GBK-EUC-H zou er alleen maar ondersteund uitzien terwijl hij verkeerde uitvoer produceert, dus de renderer doet niet alsof

Waarom CJK-tekst met een UCS2 CMap de juiste glyphs op de verkeerde advances tekent: /W is geïndexeerd op CID terwijl de codes Unicode-waarden zijn, dus bij STSong-Light en UniGB-UCS2-H missen kleine-lettercodes 97 en hoger de /W-entry [1 95 500] en pakken de /DW-default, opgelost in HotPDF door codes via CMap codespace-bereiken naar CIDs te mappen
De bug schuilt omdat code hier gelijk is aan Unicode — glyphs lijken goed terwijl elke advance stilletjes naar de default valt, dus beoordeel CJK-rendering op de spatiëring, niet op de vormen

Waarom veranderen letters met accenten in vraagtekens op Chinese Windows?

Single-byte codes mogen nooit bij de ANSI ("A")-functies van GDI uitkomen, want GetGlyphOutlineA en GetGlyphIndicesA interpreteren bytes in de systeemcodepagina terwijl TextOutA de character set van het gekozen font gebruikt. Op een Chinees systeem werd de Arial-byte $A9 (het copyright-teken in Windows-1252) een GBK-leadbyte en renderde als "?", een valkuil waar het unhinted outline-pad uit v2.766.83 er recht in liep. v2.767.3 vraagt het gerealiseerde font met GetTextCharset naar zijn character set, zet die via TranslateCharsetInfo om naar een codepagina, haalt de byte door MultiByteToWideChar en roept de W-functies aan; symbol fonts gebruiken in plaats daarvan U+F000 plus de code. Encodings die niet met Windows-1252 overeenkomen — /Differences, StandardEncoding, MacRomanEncoding — worden naar Unicode gemapt voordat een systeemfont ze te zien krijgt

Wat zijn de grenzen van tekenen met systeemfonts?

Renderen met systeemfonts is een benadering, en het HotPDF-component is eerlijk over waar hij stopt. Vóór v2.768.18 vergeleek de installatiecontrole alleen de naam die GetTextFace teruggeeft, en op gelokaliseerde Windows meldt die functie de familienaam in de systeemtaal, dus Microsoft YaHei op Chinese Windows of Yu Mincho op Japanse Windows werd als ontbrekend beoordeeld en in een GDI-vervanger getekend; sinds v2.768.18 wordt een face die onder een andere naam terugkomt ook in de name-tabel van het font opgezocht, en zulke fonts worden gevonden. Metrische compatibiliteit is gegarandeerd alleen voor de families Helvetica, Times en Courier; Symbol mapt naar Symbol en ZapfDingbats naar Wingdings, wat een noodmaatregel is in plaats van een match. De preflight hierboven ziet ook alleen fonts in de /Resources-dictionary van elke pagina, niet die waarnaar van binnenuit form XObjects verwezen wordt. Als een code nog steeds niet getekend kan worden, meldt de draw-time unresolved glyph tracking dat, wat een beter signaal is dan miniaturen met het blote oog beoordelen

De duurzame fix zit aan de authoringkant. HotPDF zelf schrijft met FontEmbedding standaard op True, en substitueert een ingesloten Arial zelfs wanneer code SetFont met Helvetica aanroept, en ingesloten tekst gaat via de embedded font glyph renderer in plaats van langs een van het gegok hierboven. Een goedkoop vangnet voor inkomende bestanden is waarschuwen vóór het renderen wanneer een gemapte familie niet in de schermfontlijst staat:

// VCL: Screen.Fonts somt geïnstalleerde familienamen op (Forms-unit)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

Voor het volledige component, inclusief paginarendering, tekstextractie en font subsetting aan de schrijfkant, zie de productpagina van de HotPDF Delphi PDF component