HotXLS, die Delphi- und C++Builder-Excel-Komponente, verringert die Größe eingebetteter PDF-Schriften durch TrueType-Font-Subsetting: Beim PDF-Export ruft es die Funktion CreateFontPackage der Windows-Systembibliothek fontsub.dll auf, um eine eingebettete TrueType-Schrift ausschließlich um die Unicode-Codepunkte herum neu aufzubauen, die ein Arbeitsblatt tatsächlich verwendet hat, statt die gesamte Schriftartdatei auszuliefern. Ein Report mit zweihundert Zeilen chinesischer Produktnamen braucht vielleicht nur ein paar hundert unterschiedliche Han-Zeichen, doch die CJK-Schriften, die Windows mitliefert, kommen routinemäßig auf jeweils 5 bis 20 MB. Bettet man eine davon vollständig ein, kann die Schrift allein schwerer wiegen als jedes andere Objekt im PDF zusammen
fontsub.dll ist keine Bibliothek, von der die meisten Delphi-Entwickler je gehört haben, und dafür gibt es einen Grund: Microsoft liefert sie als kleines, spärlich dokumentiertes Utility-DLL statt als prominente Win32-API. HotXLS behandelt sie als optionale Fähigkeit, nicht als harte Abhängigkeit, sodass die Art und Weise, wie der Exporter sie lädt, aufruft und bei Fehlen zurückfällt, ebenso viel über defensive Windows-Programmierung aussagt wie über Schriftformate, und beide Hälften dieser Geschichte lohnt es sich durchzugehen
Warum bläht Unicode-Text einen HotXLS-PDF-Export auf?
HotXLSs PDF-Exporter greift nur dann zu einer eingebetteten TrueType-Schrift, wenn Arbeitsblatttext außerhalb von WinAnsi liegt, und bleibt den Rest der Zeit bei der eingebauten Helvetica-Familie, dem Standardpfad, den der Durchgang zum Arbeitsblatt-zu-PDF-Export ausführlich behandelt. WinAnsi deckt westeuropäischen Text gut genug ab, dass viele Arbeitsmappen nie überhaupt eine Schrifteinbettung auslösen: Das PDF referenziert Helvetica einfach per Namen, und der Reader liefert sie lokal, sodass die Datei klein bleibt. In dem Moment, in dem eine Zelle etwas enthält, das WinAnsi nicht darstellen kann, einen chinesischen Produktnamen, eine koreanische Notiz, ein verirrtes Symbol in einem Kommentar, muss der Exporter ein echtes Font-Programm einbetten, denn ein PDF-Reader hat keine Fallback-Glyphenquelle für Zeichen außerhalb der 14 Standardschriften
HotXLS lokalisiert diese Schrift automatisch, indem es den Windows-Fonts-Ordner nach einer kurzen Liste installierter Kandidaten durchsucht, einschließlich der CJK-fähigen Schriftarten, die Windows für chinesisches und koreanisches Rendering mitliefert, sofern die Exporter-Eigenschaft UnicodeFontFile nicht bereits auf eine bestimmte Datei zeigt, und welche Schrift auch immer gewählt wird, sie wird vollständig eingebettet, bevor das Subsetting überhaupt läuft. Diese Einbettungsanforderung ist spezifisch für PDF: HotXLSs RTF- und HTML-Exportpfade halten Unicode-Text intakt, indem sie Codepunkte in den Bytestrom escapen, statt ein Font-Programm auszuliefern, weshalb das Größenproblem, das dieser Artikel behandelt, bei diesen beiden Formaten kein Gegenstück hat
uses
lxHandle, lxPDF;
var
Book: TXLSWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSWorkbook.Create;
try
Book.Open('catalog-cn.xlsx');
Exporter := TXLSPDFExport.Create;
try
// Optional: pin a specific CJK-capable font instead of the
// exporter's automatic Windows\Fonts scan.
Exporter.UnicodeFontFile := 'C:\Windows\Fonts\simhei.ttf';
Exporter.SaveAsPDF(Book.ActiveSheet, 'catalog-cn.pdf');
finally
Exporter.Free;
end;
finally
Book.Free;
end;
end;
Was ist fontsub.dll, und warum nicht einen eigenen Subsetter von Grund auf schreiben?
fontsub.dll ist eine kleine Windows-Systembibliothek, seit Windows XP ausgeliefert, die hier eine einzige relevante Funktion freilegt: CreateFontPackage. Übergibt man ihr die Bytes einer Quell-TrueType-Schrift und eine Liste zu behaltender Unicode-Codepunkte, gibt sie eine minimale Schrift zurück, die weiterhin jede Font-Format-Einschränkung erfüllt: Glyphenindizes neu nummeriert, glyf und loca nur um die beibehaltenen Umrisse herum neu aufgebaut, hmtx und cmap passend umgeschrieben. HotXLS deklariert den Funktionszeigertyp direkt gegen diesen Vertrag
const
TTFCFP_FLAGS_SUBSET = 1;
TTFMFP_SUBSET = 0;
TTFCFP_MS_PLATFORMID = 3;
TTFCFP_UNICODE_CHAR_SET = 1;
type
TCreateFontPackage = function(puchSrcBuffer: Pointer; ulSrcBufferSize: Cardinal;
var puchFontPackageBuffer: PAnsiChar; var pulFontPackageBufferSize: Cardinal;
var pulBytesWritten: Cardinal; usFlags, usTTCIndex, usSubsetFormat,
usSubsetLanguage, usSubsetPlatform, usSubsetEncoding: Word;
pusSubsetKeepList: PWordArray; usSubsetKeepListCount: Word;
lpfnAllocate, lpfnReAllocate, lpfnFree, reserved: Pointer): Cardinal; cdecl;
Die Aufgabe von CreateFontPackage von Hand nachzubauen, statt sie aufzurufen, würde bedeuten, einen korrekten TrueType-Subsetter zu implementieren: zusammengesetzte Glyphen durchlaufen, um jede Komponentenglyphe einzubeziehen, auf die eine beibehaltene Glyphe verweist, loca-Offsets neu aufbauen, nachdem Umrisse verworfen wurden, die Einbettungs-Erlaubnis-Bits in der OS/2-Tabelle einer Schrift respektieren, und all das über jede noch so ausgefallene Schriftart hinweg korrekt hinbekommen, die zufällig auf dem Rechner eines Kunden installiert ist. Microsoft hat dieses Problem bereits gelöst und liefert die Lösung als Teil von Windows selbst, sodass der Aufruf einer System-DLL, die Microsoft wartet, gegen den eigenen Font-Rendering-Stack testet und kostenlos an jede Maschine verteilt, HotXLS nur ein dynamisches Laden und einen Funktionszeiger kostet; dieselbe Logik neu zu implementieren würde bedeuten, einen Parser für ein Binärformat mit Jahrzehnten von Sonderfällen zu besitzen, für ein Feature, das nur zählt, wenn eine Schrift zufällig groß ist
Die Keep-Liste aus tatsächlich gerenderten Glyphen aufbauen
HotXLS baut die Subsetting-Keep-Liste aus einer Map auf, die es aus einem anderen Grund bereits pflegte, sodass die Buchführung nichts extra kostet. Jedes Mal, wenn der Seiten-Rendering-Code ein Zeichen zeichnet, das die eingebettete Unicode-Schrift benötigt, schlägt er den Glyphenindex dieses Zeichens nach und protokolliert die Paarung in FUnicodeGlyphMap, einer Glyphe-zu-Codepunkt-Tabelle, die auch die PDF-ToUnicode-CMap antreibt, damit Kopieren-und-Einfügen aus dem fertigen Dokument den ursprünglichen Text zurückgibt statt roher Glyphen-IDs. Wenn die Seiten-Content-Streams fertig sind, listet diese Map bereits exakt die Menge der im Dokument verwendeten Unicode-Codepunkte, nicht mehr und nicht weniger
var
keepList: array of Word;
keepCount, i: Integer;
codePoint: LongWord;
begin
SetLength(keepList, FUnicodeGlyphMap.Count);
keepCount := 0;
for i := 0 to FUnicodeGlyphMap.Count - 1 do
begin
codePoint := LongWord(StrToIntDef('$' + FUnicodeGlyphMap.ValueFromIndex[i], 0));
if codePoint > 0 then
begin
keepList[keepCount] := Word(codePoint);
Inc(keepCount);
end;
end;
end;
Beim Finalisieren durchläuft HotXLS dieselbe Map ein zweites Mal, um die Keep-Liste aufzubauen, die CreateFontPackage erwartet, ein einfaches Array der zu behaltenden Unicode-Codepunkte in der 16-Bit-Form, die das Keep-Listen-Argument der API verlangt. Weil dieses Argument ein Array von 16-Bit-Wörtern ist, adressiert es die Basic Multilingual Plane sauber, was gewöhnlichen CJK-, kyrillischen, griechischen und arabischen Text ohne Komplikation abdeckt; ein Arbeitsblatt, das sich auf Zeichen der Supplementary Plane stützt, bestimmte Emojis oder seltene historische Schriften, liegt außerhalb dessen, was ein einzelner Keep-Listen-Eintrag direkt benennen kann, was eine Grenze ist, die man kennen sollte, kein Defekt, da die überwiegende Mehrheit Unicode-lastiger Geschäftstabellen dieser Ebene ohnehin nie nahekommt
Was passiert, wenn fontsub.dll fehlt?
HotXLS nimmt nie an, dass fontsub.dll vorhanden ist, und der PDF-Export scheitert nie, weil sie es nicht ist. Die Bibliothek wird dynamisch in dem Moment geladen, in dem ein Subset benötigt wird, mit SafeLoadLibrary und GetProcAddress statt einem statischen Import, genau deshalb, weil fontsub.dll keine dokumentierte, garantiert vorhandene öffentliche API ist, so wie es kernel32.dll ist: Es ist gebündeltes Font-Embedding-Tooling, und nichts in Microsofts Vertrag verspricht, dass es auf jedem SKU, jedem Servicing-Branch oder jeder Kompatibilitätsschicht überlebt, die versucht, Windows zu emulieren
var
hFontSub: HMODULE;
CreateFontPackage: TCreateFontPackage;
begin
hFontSub := SafeLoadLibrary('FontSub.dll');
if hFontSub = 0 then
Exit; // no subsetting available - keep the full embedded font
try
@CreateFontPackage := GetProcAddress(hFontSub, 'CreateFontPackage');
if not Assigned(CreateFontPackage) then
Exit;
// ... call CreateFontPackage, check its return code ...
finally
FreeLibrary(hFontSub);
end;
end;
Jeder Fehlerpfad faltet sich zurück auf dasselbe Ergebnis. Eine fehlende DLL, ein fehlender Export, ein von null verschiedener Rückgabecode oder eine Schrift, deren OS/2-Tabelle Subsetting über ihre Einbettungs-Erlaubnis-Bits verbietet, HotXLS behält einfach die volle Schrift, die es bereits eingebettet hatte, und macht weiter. Nichts wirft eine Exception, nichts bricht den Export ab, und der aufrufende Code muss eine Font-Optimierung nie in seine eigene Exception-Behandlung einwickeln; das exportierte PDF ist so oder so gültig, und die einzige Variable ist, ob es am Ende klein oder etwas größer ausfällt
Wie viel kleiner wird das PDF tatsächlich?
HotXLSs TrueType-Font-Subsetting verkleinert das exportierte PDF eines Unicode-lastigen Arbeitsblatts typischerweise auf irgendwo zwischen einem Zwanzigstel und einem Achtel seiner Größe ohne Subsetting, eine 8- bis 20-fache Reduktion, deren Ausmaß davon abhängt, wie viel einer vollständigen Schrift ein gegebenes Dokument tatsächlich berührt: Eine Bestellung, die um ein paar hundert unterschiedliche chinesische Zeichen herum aufgebaut ist, behält nur diese paar hundert Glyphen aus den Zehntausenden, die eine CJK-Schriftart mitbringt, während ein Blatt, das eine breitere Zeichenmischung umfasst, proportional mehr behält. HotXLS legt einen weiteren Flate-Kompressionsdurchlauf über die Subset-Font-Bytes, bevor sie in den /FontFile2-Stream des PDFs geschrieben werden, dieselbe Kompression, die der Rest der Content-Streams des Dokuments bereits durchläuft, und nichts davon verlangt dem aufrufenden Code etwas Zusätzliches ab: Ein Arbeitsblatt, das WinAnsi nie verlässt, berührt diesen Pfad nie und exportiert weiterhin über schlichtes Helvetica, während ein Arbeitsblatt, das den Unicode-Font-Pfad auslöst, automatisch Subsetting bekommt, ohne Eigenschaft zum Setzen und ohne separaten Aufruf, und die eine beteiligte Eigenschaft, UnicodeFontFile, wählt nur, welche Schrift eingebettet und subsettet wird, nicht ob Subsetting stattfindet
Font-Subsetting ist ein Detail innerhalb der breiteren PDF-Export-Oberfläche der HotXLS Delphi Excel-Komponente, zusammen mit Paginierung, Arbeitsblatt-Druckmetadaten und den CSV-, HTML- und RTF-Exportpfaden, die sie mitbringt