Ein Designer wählt eine Schriftart mit einem einstöckigen a für Überschriften, einer durchgestrichenen Null für Tabellen oder einem Set von Schwung-Großbuchstaben (Swash Capitals) für ein Cover. Diese Glyphen sind bereits in der Schriftart enthalten. Sie sind lediglich nicht der Standard. Das standardmäßige a wird vom Zeichen über die cmap-Tabelle auf eine Glyphe abgebildet, und die Alternative befindet sich ein paar Glyphen-IDs entfernt und ist nur über eine Substitutionsregel erreichbar. Diese Alternative in einem PDF zu erzeugen bedeutet, die Regel zu lesen und die Ersatzglyphe im Content Stream auszugeben. Dieser Artikel handelt vom Lesen dieser Regeln – jener für Einzelsubstitutionen (Single-Substitution) – in Object Pascal, ohne dass eine native Shaping-Bibliothek zugrunde liegt.
Der Anwendungsbereich ist absichtlich eng gefasst. Stilistische Sets und Alternativen sind Single-Glyph-in-, Single-Glyph-out-Substitutionen (eine Glyphe rein, eine Glyphe raus). Sie sind der Teil des OpenType-Layouts, den Sie mit einem kleinen, deterministischen Tabellendurchlauf auflösen können, was sie zu einer guten Lösung für eine Pascal-Engine macht, die frei von C-Abhängigkeiten bleiben möchte.
Warum reines Delphi anstelle von HarfBuzz
HarfBuzz ist die offensichtliche Antwort auf "Formatiere (shape) diesen Text", und für vollständiges bidirektionales, indisches oder arabisches Shaping ist es die richtige Antwort. Es ist aber auch eine C-Bibliothek. Die Einbindung in ein Delphi- oder C++Builder-Produkt bedeutet, dass für jede Zielplattform und Architektur ein natives Objekt ausgeliefert werden muss, dessen Aufrufkonvention (Calling Convention) eingehalten, sein Veröffentlichungsrhythmus verfolgt und seine Lizenzbedingungen mit den eigenen abgeglichen werden müssen. Nichts davon ist für sich genommen schwer. Alles zusammen ist jedoch Reibung, die nie verschwindet, und es bringt nichts, wenn die eigentliche Anforderung lautet: "Gib mir die ss01-Form dieses Buchstabens".
Einzelsubstitution benötigt keine Shaping-Engine. Sie benötigt einen Parser für eine Handvoll GSUB-Subtabellen-Formate und ein oder zwei binäre Suchen. Wenn man das in Pascal schreibt, bleibt die gesamte Toolchain innerhalb eines einzigen Compilers. Die ehrliche Grenze ist, dass dieser Ansatz nur Glyphen-Substitutions-Lookups (Glyph Substitution Lookups) handhabt und sonst nichts. Es ist keine Bidi-Auflösung, es ist keine indische Umordnung (Indic Reordering), und es ist kein automatisches kontextbezogenes Shaping. Wo diese benötigt werden, werden sie benötigt, und eine Einzelsubstitutions-Abfrage kann sie nicht ersetzen.
Die GSUB-Hierarchie, von oben nach unten
Die GSUB-Tabelle (Glyph Substitution) ist als eine Kette von Indirektionen organisiert, und eine Substitutions-Abfrage durchläuft die Kette von oben. Ganz oben befindet sich die ScriptList. Ein Skript-Tag wie latn wählt einen Eintrag aus, und das spezielle Tag DFLT ist das Standardskript, das greift, wenn kein spezifischeres Skript passt. Der Skripteintrag verweist auf ein LangSys, das Sprachsystem, mit einem Standard-LangSys für den häufigsten Fall und optionalen benannten für Sprachen, die ein anderes Verhalten benötigen. Türkisch ist das übliche Beispiel, bei dem das gepunktete und punktlose i eine eigene Behandlung erfordern.
Das LangSys benennt ein Set von Feature-Indizes. Jeder Index zeigt in die FeatureList, wo ein Feature-Record ein Vier-Byte-Tag trägt, darunter ss01, sowie eine Liste von Lookup-Indizes. Diese Indizes verweisen schließlich in die LookupList, in der sich die eigentlichen Substitutions-Subtabellen befinden. Die Auflösung von ss01 bedeutet also: Finden Sie das Skript, finden Sie sein LangSys, finden Sie das Feature, dessen Tag ss01 ist, sammeln Sie die Lookups, die es benennt, und wenden Sie sie an. HotPDF verwendet standardmäßig das DFLT-Skript und das Standard-LangSys, womit die überwiegende Mehrheit der Designs für lateinische Texte ausgeliefert wird, und es bietet eine Möglichkeit, das Skript-Tag zu überschreiben, falls eine Schriftart ihre Features stattdessen unter einem bestimmten Skript verknüpft.
Coverage-Tabellen entscheiden, wer teilnimmt
Jede Substitutions-Subtabelle beginnt mit derselben Frage: Nimmt diese Eingabeglyphe an dieser Regel teil, und wenn ja, wo befindet sie sich in der regelspezifischen Indizierung? Diese Frage wird durch eine Coverage-Tabelle beantwortet, und die Antwort ist ein Coverage-Index, eine kleine Ordinalzahl, die der Rest der Subtabelle verwendet, um nachzuschlagen, was aus der Glyphe wird.
Coverage gibt es in zwei Formaten. Format 1 ist eine Liste von Glyphen-IDs, die in aufsteigender Reihenfolge sortiert sind. Sie finden eine Glyphe mit einer binären Suche, und ihre Position in der Liste ist ihr Coverage-Index. Format 2 ist eine Liste von Bereichsdatensätzen (Range Records), jeweils mit einer Startglyphe, einer Endglyphe und dem Coverage-Index, auf den die Startglyphe abgebildet wird. Eine Glyphe innerhalb eines Bereichs erhält ihren Coverage-Index durch einen Offset vom Start des Bereichs. Format 1 ist kompakt, wenn die teilnehmenden Glyphen verstreut sind, Format 2, wenn sie in aufeinanderfolgenden Reihenfolgen (Contiguous Runs) auftreten. Beide sind sortiert, also werden beide in logarithmischer Zeit durchsucht, und beide geben entweder einen Coverage-Index oder ein sauberes "nicht abgedeckt" (not covered) zurück, wodurch die Engine die Glyphe in Ruhe lässt.
Einzelsubstitution, die beiden Formate
Einzelsubstitution (Single Substitution) ist LookupType 1 und ordnet eine Glyphe genau einem Ersatz zu. Sie verfügt ebenfalls über zwei Formate, und die Aufteilung ist eine Speicherplatzoptimierung. Format 1 speichert ein einzelnes vorzeichenbehaftetes Delta. Die Ausgabe-Glyphen-ID ist die Eingabe-Glyphen-ID plus dieses Delta, modulo 65536. So codiert eine Schriftart eine Substitution, bei der jede teilnehmende Glyphe am gleichen festen Offset von ihrer Alternative sitzt, zum Beispiel ein Block von Versalziffern (Lining Figures), der in einem konstanten Abstand zu den passenden Mediävalziffern (Oldstyle Figures) platziert ist. Die Coverage-Tabelle sagt aus, welche Glyphen sich qualifizieren, und das eine Delta bedient sie alle.
Format 2 speichert ein explizites Array von Ersatz-Glyphen-IDs. Der Coverage-Index aus der Coverage-Tabelle ist der Index in dieses Array, also wird die Glyphe an Coverage-Index 0 zum ersten Array-Eintrag, Coverage-Index 1 zum zweiten und so weiter. Format 2 wird verwendet, wenn sich die Alternativen nicht in einem einheitlichen Abstand (Offset) befinden, was der übliche Fall für handgemachte stilistische Sets ist. Die Abfrage ist aus Sicht des Aufrufers in beiden Fällen gleich. Nehmen Sie die Eingabeglyphe, lassen Sie sie durch Coverage laufen, und wenn sie abgedeckt ist, wenden Sie das Delta an oder lesen Sie den Array-Slot aus.
var
Pdf: THotPDF;
BaseGID, AltGID: Word;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
Pdf.SetFont('My Stylistic Face', 12, []);
// Standard-Glyphe für 'a' durch die cmap der Schriftart.
BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));
// Stilistisches Set 1: Auflösen der Alternative über GSUB LookupType 1.
AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');
// AltGID = BaseGID bedeutet, dass das Feature diese Glyphe nicht berührt hat.
if AltGID <> BaseGID then
{ // AltGID im Content Stream ausgeben };
finally
Pdf.Free;
end;
end;
Der Vertrag, den es sich zu beachten lohnt, ist der Pass-Through (Durchlauf). GetSingleSubstituteGlyph gibt die Eingabe-Glyphen-ID bei jedem Fehlschlag unverändert zurück: keine Schriftart, keine GSUB-Tabelle, kein passendes Feature, kein Coverage-Treffer. Das bedeutet, dass der Aufruf bedenkenlos bedingungslos gemacht werden kann. Sie fragen nach der Alternative, und wenn es keine gibt, erhalten Sie genau das zurück, was Sie hineingegeben haben, sodass der aufrufende Code niemals eine Sonderbehandlung (Special-Case) für eine Schriftart benötigt, der das Feature fehlt.
Was die stilistischen Feature-Tags bedeuten
Das Feature-Tag stellt das gesamte Vokabular dafür dar, nach welcher Alternative Sie fragen, und die für stilistische Arbeit relevanten Tags bilden eine kurze Liste. Das wichtigste Paar ist salt (stylistic alternates), der Catch-All-Zugriff auf die alternativen Formen einer Glyphe, und ss01 bis ss20, die zwanzig nummerierten stilistischen Sets, die eine Schriftart definieren kann, wobei jedes ein benanntes Bündel von Substitutionen ist, die der Designer zusammenfasst. Eine Schriftart könnte zum Beispiel ein einstöckiges a und ein R mit geradem Bein unter ss03 ablegen, sodass die Aktivierung dieses einen Sets beide umgestaltet.
Darum herum gruppieren sich mehrere weitere Single-Substitution-Tags. aalt steht für access-all-alternates, die Vereinigung jeder Alternative, die eine Glyphe besitzt, üblicherweise als Glyphen-Paletten-Feature präsentiert. titl wählt Titel-Großbuchstaben (Titling Capitals), die für große Größen zugeschnitten sind. subs und sups wechseln zu echten tief- und hochgestellten Ziffern, anstatt verkleinerte Standardziffern zu verwenden. ordn erzeugt Ordinalformen, die hochgestellten Buchstaben in 1st und 2nd. frac baut Brüche, obwohl vollständige diagonale Brüche sich auch auf Ligaturen und kontextbezogene Logik stützen, die über einfache Einzelsubstitution hinausgehen. Für Single-Glyph-Fälle ist der Mechanismus identisch mit ss01: Übergeben Sie das Tag an die Substitutions-Abfrage und lesen Sie die alternative Glyphe zurück.
// Versuchen Sie ein Stilset-Feature (stylistic-set), und greifen Sie dann auf einfache Alternativen zurück.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
const PreferredTag: AnsiString): Word;
begin
Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
if Result = BaseGID then
Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
// Immer noch BaseGID, wenn keines der Features diese Glyphe abdeckt.
end;
cmap Format 12 und die ergänzenden Ebenen (Supplementary Planes)
Bevor eine Substitution ablaufen kann, muss ein Zeichen zu einer Glyphe werden, und das ist die Aufgabe der cmap-Tabelle. Die Substitutions-Abfrage beginnt bei einer Glyphen-ID, also verläuft der Pfad immer von Zeichen zu Glyphe durch cmap, und dann von Glyphe zu Alternative durch GSUB. Der interessante Teil von cmap ist seine Reichweite. Eine Format-4-Subtabelle deckt die Basic Multilingual Plane ab, die ersten 65536 Codepoints, und das reicht für den meisten lateinischen Text aus. Es reicht jedoch nicht für Codepoints ab U+10000 aufwärts, die Supplementary Planes, in denen heute mathematische Alphanumerik, viele Symbole und mehrere lebendige Skripte untergebracht sind.
Format 12 ist die Subtabelle, die den gesamten Bereich von U+0000 bis U+10FFFF abdeckt. Es handelt sich um eine sortierte Liste von Gruppen, jede Gruppe bestehend aus einem Start-Codepoint, einem End-Codepoint und einer Start-Glyphen-ID, sodass eine aufeinanderfolgende Reihe von Codepoints auf eine aufeinanderfolgende Reihe von Glyphen abbildet. HotPDF löst Codepoints mit einer hybriden Strategie auf, die der Struktur der Daten entspricht. Codepoints in der BMP werden aus einem direkten Array bedient, das durch den Codepoint indiziert ist – ein einzelner Lookup ohne Suche. Codepoints in den Supplementary Planes werden aus einer spärlichen (sparse) Tabelle bedient, die nach Codepoint sortiert ist und mit einer binären Suche durchsucht wird. Das Ergebnis ist, dass GetUnicodeGlyphForCodepoint einen vollständigen Cardinal entgegennimmt und über den gesamten Bereich korrekt antwortet, wobei es die Glyphen-ID 0 (die .notdef-Glyphe) für jeden Codepoint zurückgibt, den die Schriftart nicht abbildet.
var
Pdf: THotPDF;
Cp: Cardinal;
GID, StyledGID: Word;
begin
// Ein Codepoint aus der Supplementary Plane: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
Cp := $1D49C;
GID := Pdf.GetUnicodeGlyphForCodepoint(Cp); // Lookup über Format 12
if GID <> 0 then
StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
else
StyledGID := 0; // Schriftart hat keine Glyphe für diesen Codepoint
end;
Wo diese Abfragen enden
Die Single-Substitution-APIs beantworten eine bestimmte Art von Frage, und man sollte sich darüber im Klaren sein, was sie nicht beantworten. LookupType 1 ist einer von acht Substitutionstypen. Die Abfrage handhabt weder die Mehrfachsubstitution (Multiple Substitution) von LookupType 2, bei der aus einer Glyphe mehrere werden, noch die Ligatur-Substitution von LookupType 4, bei der aus mehreren Glyphen eine wird. Sie handhabt auch nicht die kontextuellen und verkettend-kontextuellen (chaining-contextual) Typen, LookupTypes 5 und 6, die nur ausgelöst werden, wenn eine Glyphe in einer bestimmten Umgebung (Neighbourhood) erscheint, und ebensowenig die Erweiterungs- (Extension) und Rückwärtsverkettungs-Typen (Reverse-Chaining). Ein diagonaler Bruch, ein indischer Konjunkt (Devanagari conjunct) oder eine arabische Initial-Medial-Final-Kaskade ist ein Sequenzproblem, und ein Single-Substitution-Lookup pro Glyphe kann dies nicht ausdrücken.
Es führt auch kein automatisches Shaping durch. Nichts hier inspiziert einen Textdurchlauf, entscheidet, welche Features aktiviert werden sollen, und wendet sie in der vom Skript vorgegebenen Reihenfolge an. Der Aufrufer wählt das Feature-Tag und wendet es Glyphe für Glyphe an. Das ist genau das richtige Werkzeug für stilistische Sets und Alternativen, die opt-in (freiwillig wählbar) und lokal sind, und genau das falsche Werkzeug für ein Skript, das eine Umordnung (Reordering) erfordert. Diese scharfe Abgrenzung ermöglicht es dem Substitutionspfad, klein und vorhersehbar zu bleiben.
Für Fälle, die Arbeit auf Sequenzebene erfordern, wird das Thema Complex-Script in unserem Artikel über Complex-Script-Text-Shaping in Delphi aufgegriffen. Wenn Ihre Substitutionen Teil eines größeren Berichtsauftrags sind, der auch Bilder und andere Schriftarten auf der Seite platziert, behandelt der Leitfaden zur Berichtsausgabe mit Schriftarten und Bildern, wie diese Teile zusammenpassen. All dies läuft auf derselben Engine, der HotPDF Component für Delphi und C++Builder, die die GSUB-Substitutionsabfragen neben den an anderer Stelle in diesem Blog behandelten APIs für Schriftarteneinbettung, Subsetting und Text beinhaltet.