PDFlibPas löst Zeichen, die die gewählte Schriftart nicht darstellen kann, auf, indem es Cluster für Cluster eine Fallback-Kette installierter Schriftschnitte durchsucht, wobei Shaping und die Reihenfolge bidirektionaler Läufe erhalten bleiben. Aktiviert wird das mit SetAutomaticFontFallback, die Kette wird mit AddFontFallback erweitert, und eingebettet werden nur die Fallback-Schriftarten, die tatsächlich für die Ausgabe verwendet wurden
Das gelöste Problem begegnet jedem Dokumentengenerator, sobald zum ersten Mal ein Kundenname in einer Schrift ankommt, mit der die Vorlagen-Schriftart nie gerechnet hat. Der Ausfall geschieht leise, und genau das macht ihn teuer
Warum verschwindet nicht unterstützter Text, statt einen Fehler auszulösen?
Weil PDF kein Konzept für eine Schriftart kennt, die ein Zeichen nicht darstellen kann. Eine einfache Schriftart bildet Byte-Codes über eine Kodierung auf Glyphennamen ab; eine zusammengesetzte Schriftart bildet Codes über eine CMap auf Glyphenindizes ab. Wird ein Glyph angefordert, den der Schriftschnitt nicht enthält, erhält man Glyphenindex null, .notdef, was die meisten Schriftschnitte als nichts oder als leeres Kästchen zeichnen. Die Datei ist strukturell gültig, der Textoperator ist wohlgeformt, und die Seite wird gerendert. Sie ist nur leer, wo der Name stehen sollte
Nichts in ISO 32000-1 verlangt, dass ein Erzeuger das bemerkt. Ein Generator, der Text schreibt, ohne die Abdeckung zu prüfen, erzeugt ein technisch konformes PDF, das stillschweigend Inhalt verloren hat, und der Verlust taucht Wochen später auf dem Bildschirm eines Kunden auf. Deshalb werden die Fallback-Funktion und der Bericht über fehlende Glyphen zusammen ausgeliefert: Zu lösen, was sich lösen lässt, ist nur die halbe Aufgabe, und zu melden, was sich nicht lösen ließ, ist die andere Hälfte
Fallback erfolgt pro Cluster, nicht pro Code-Punkt
Granularität ist das Detail, das eine funktionierende Implementierung von einer bloß plausiblen unterscheidet. Text ist keine Folge unabhängiger Zeichen. Eine Devanagari-Silbe, ein Emoji mit Hautton-Modifikator, ein Grundbuchstabe mit kombinierenden Zeichen: Jedes davon ist ein Cluster, das von einer einzigen Schriftart gerendert werden muss, weil die darin enthaltenen Shaping-Entscheidungen von Tabellen in diesem Schriftschnitt abhängen
PDFlibPas löst Cluster auf, sodass ein Cluster, den ein Fallback-Schriftschnitt abdeckt, vollständig von diesem Schriftschnitt gezeichnet wird. Eine Aufteilung mitten im Cluster, bei der eine Hälfte aus der primären Schriftart und die andere aus einem Fallback gezeichnet würde, ergäbe ein technisch vorhandenes, aber sichtbar defektes Ergebnis, das man durchaus als schlimmer ansehen kann als die Lücke, mit der man begonnen hat. Auch die Laufreihenfolge bleibt erhalten, sodass ein Fallback innerhalb eines Rechts-nach-links-Laufs den umgebenden Text nicht neu ordnet; dieselbe Mechanik liegt dem in vertikalen Schreiben für Japanisch und Chinesisch beschriebenen Layout zugrunde
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetAutomaticFontFallback(1);
// Reihenfolge der Suche: der erste Treffer gewinnt, daher die breitesten Schriftschnitte zuletzt
Lib.AddFontFallback('Microsoft YaHei'); // Vereinfachtes Chinesisch
Lib.AddFontFallback('Meiryo'); // Japanisch
Lib.AddFontFallback('Segoe UI Symbol');
Lib.AddFontFallback('Segoe UI Emoji');
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);
Lib.AddTrueTypeFont('Arial', 1); // 1 = Schriftschnitt einbetten
Lib.SetTextSize(11);
Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
Lib.DrawText(72, 700, 'Delivery status: on time');
Lib.SaveToFile('invoice.pdf');
finally
Lib.Free;
end;
end;
Die Kette sollte bewusst geordnet werden. Die Auflösung nimmt den ersten Schriftschnitt, der den Cluster abdeckt, sodass eine breite pan-Unicode-Schriftart an erster Stelle fast alles gewinnt und die sorgfältig gewählten schriftspezifischen Schriftschnitte nie konsultiert werden. Die spezifischen Schriftschnitte gehören an den Anfang, der Auffangschriftschnitt ans Ende
Melden oder abbrechen: welchen Fehlerfall wollen Sie?
SetMissingGlyphPolicy akzeptiert PDF_MISSING_GLYPH_REPORT, die kompatible Vorgabe, oder PDF_MISSING_GLYPH_ABORT. Bei der Report-Richtlinie läuft die Textoperation weiter, nicht auflösbare Code-Punkte werden wie bisher verworfen, und jeder Einzelfall wird protokolliert. Bei der Abort-Richtlinie wird die Textoperation abgelehnt, bevor überhaupt Inhalt geschrieben wird, und LastErrorCode wird auf 521 gesetzt
Die Wahl richtet sich nach dem Verwendungszweck des Dokuments. Ein Stapel interner Berichte sollte weiter rendern und die Lücken protokollieren, denn ein heute leicht unvollständiger Bericht ist besser als gar keiner. Ein rechtsverbindlicher Vertrag, eine Rechnung oder alles mit einem Namen darauf sollte abbrechen, weil ein stillschweigend verworfenes Zeichen im Namen einer Vertragspartei ein Defekt ist, den man lieber im eigenen Prozess entdeckt als in einem Streitfall. Die Abort-Richtlinie schlägt vor dem Schreiben fehl, sodass kein halbfertiger Content-Stream zurückbleibt
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
// ... Dokument aufbauen ...
if Lib.DrawText(72, 660, CustomerName) <> 1 then
if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
begin
Report := Lib.GetMissingGlyphReportJSON;
// {"valid":false,"policy":1,"eventCount":1,"events":[
// {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
// "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
// "fontType":"TrueType","operation":"DrawText"}]}
EscalateToOperator(Report);
end;
finally
Lib.Free;
end;
end;
Der Bericht ist bewusst maschinenlesbar und begrenzt gehalten. Jedes Ereignis trägt die Seite, den UTF-16-Index innerhalb der Zeichenkette, den Code-Punkt sowohl numerisch als auch in U+XXXX-Form, die gewählte Schriftart, deren Typ sowie die Operation, bei der das Problem auftrat, sodass ein Support-Ticket das genaue Zeichen benennen kann, statt nur ein Symptom zu beschreiben. Der Tracker behält die letzten 256 Ereignisse, was genug ist, um ein Dokument zu diagnostizieren, und klein genug, dass ein pathologischer Lauf die Diagnose nicht zu einem Speicherproblem machen kann
Messung und Zeichnen müssen übereinstimmen
Die Breitenmessung verwendet dieselben clusterbewussten Fallback-Entscheidungen wie das Zeichnen. Das klingt selbstverständlich, ist aber genau das, was die meisten selbstgebauten Fallback-Schichten falsch machen: Sie flicken den Zeichenpfad, lassen die Messung auf der primären Schriftart und am Ende werden jedes Textfeld, jede Rechtsausrichtung und jede Tabellenspalte aus Breiten berechnet, die nicht zu dem passen, was tatsächlich gerendert wurde
Weil beide Pfade dieselbe Auflösung teilen, belegt eine vor dem Zeichnen gemessene Zeichenkette genau die Breite, mit der sie gemessen wurde, einschließlich der Fallback-Läufe. Das macht es sicher, Fallback global zu aktivieren, statt nur an den Stellen, die man von Hand geprüft hat
Eingebettet wird nur, was tatsächlich verwendet wurde
Fallback-Schriftarten werden verzögert eingebettet: Ein Schriftschnitt in der Kette, der nie einen Cluster aufgelöst hat, trägt nichts zur Ausgabe bei. Ein Dokument mit einem chinesischen Zeichen und 5.000 lateinischen enthält keinen vollständigen CJK-Schriftschnitt, sondern nur das, was der Subsetting-Durchlauf für dieses eine Glyph erzeugt hat, was dem in Dateigrößenoptimierung und Font-Subsetting beschriebenen Verhalten entspricht
Diese Verzögerung macht es günstig, eine breite Kette zu konfigurieren. Registrieren Sie die Schriftschnitte, die Ihr Dokumentenbestand über alle bedienten Locales hinweg benötigen könnte, und jedes einzelne PDF zahlt nur für das, was es tatsächlich verwendet hat. Für Dokumente, die Sie nicht selbst erzeugt haben und in denen die fehlenden Schriftschnitte bereits in einer bestehenden Datei stecken, ist der Reparaturweg ein anderer und wird in Einbetten fehlender Schriftarten in ein bestehendes PDF behandelt
Ein Vorbehalt für den Betrieb sollte klar benannt werden: Fallback löst gegen Schriftschnitte auf, die auf der Maschine installiert sind, auf der der Code läuft. Ein Server ohne installierte CJK-Schriftarten hat nichts, worauf er zurückfallen könnte, und der Bericht sagt Ihnen das bereits beim ersten Dokument, statt erst nach der ersten Beschwerde. Liefern Sie die Schriftarten aus, auf die Sie angewiesen sind, und klären Sie die Lizenzierung für deren Einbettung
PDFlibPas ist eine Delphi-, C++Builder- und Lazarus-PDF-Bibliothek mit passenden DLL- und ActiveX-Schnittstellen, sodass die Fallback- und Missing-Glyph-APIs auch für Aufrufer außerhalb von Pascal verfügbar sind. Die vollständige Dokumentation steht auf der PDFlibPas Delphi PDF-Bibliothek-Seite