Ein Designer wählt eine Schriftart mit einem einstöckigen a für Überschriften, einer durchgestrichenen Null für Tabellen oder einem Satz von Zierschriften (Swash Capitals) für ein Cover. Diese Glyphen sind bereits in der Schriftart vorhanden. Sie sind lediglich nicht die Standardeinstellung. Das Standard-a wird über die cmap-Tabelle auf eine Glyphe abgebildet, und die Alternative liegt ein paar Glyphen-IDs entfernt, erreichbar nur über eine Ersetzungsregel. Diese Alternative in einem PDF zu erzeugen bedeutet, die Regel zu lesen und die Ersatzglyphe im Inhaltsstrom auszugeben. In diesem Artikel geht es darum, diese Regeln – konkret die einfache Ersetzung (Single Substitution) – in Object Pascal zu lesen, ohne dass eine native Shaping-Bibliothek darunter liegt.
Der Rahmen ist bewusst eng gesteckt. Stilistische Sets und Alternativen sind Ersetzungen nach dem Prinzip "eine Glyphe rein, eine Glyphe raus". Sie sind der Teil des OpenType-Layouts, den Sie mit einem kleinen, deterministischen Tabellendurchlauf auflösen können, wodurch sie sich hervorragend für eine Pascal-Engine eignen, die frei von C-Abhängigkeiten bleiben soll.
Warum reines Delphi statt HarfBuzz
HarfBuzz ist die naheliegende Antwort auf die Frage "diesen Text formen", und für vollständiges bidirektionales, indisches oder arabisches Shaping ist es auch die richtige Antwort. Es ist jedoch eine C-Bibliothek. Die Einbindung in ein Delphi- oder C++Builder-Produkt bedeutet, dass für jede Zielplattform und -architektur ein natives Objekt ausgeliefert, die Aufrufkonvention eingehalten, der Release-Zyklus verfolgt und die Lizenzbedingungen mit den eigenen abgeglichen werden müssen. Nichts davon ist für sich genommen schwierig. Aber all das erzeugt Reibung, die niemals verschwindet, und bringt keinen Vorteil, wenn die eigentliche Anforderung lediglich lautet: "Gib mir die ss01-Form dieses Buchstabens."
Die einfache Ersetzung benötigt keine Shaping-Engine. Sie erfordert einen Parser für eine Handvoll GSUB-Untertabellenformate und ein oder zwei binäre Suchen. Dies in Pascal zu schreiben, hält die gesamte Toolchain innerhalb eines Compilers. Die ehrliche Einschränkung ist, dass dieser Ansatz ausschließlich Glyphenersetzungs-Lookups verarbeitet und sonst nichts. Es ist keine Bidi-Auflösung, keine indische Neuordnung und kein automatisches kontextuelles Shaping. Wo diese benötigt werden, sind sie unersetzlich, und eine einfache Ersetzungsabfrage kann sie nicht ersetzen.
Die GSUB-Hierarchie von oben nach unten
Die Glyph Substitution-Tabelle (GSUB) ist als Kette von Indirektionen organisiert, und eine Ersetzungsabfrage durchläuft die Kette von oben nach unten. Ganz oben befindet sich die ScriptList. Ein Script-Tag wie latn wählt einen Eintrag aus, und das spezielle Tag DFLT is das Standardskript, das angewendet wird, wenn kein spezifischereres Skript übereinstimmt. Der Skripteintrag verweist auf ein LangSys (Sprachsystem), mit einem Standard-LangSys für den Regelfall und optionalen benannten Systemen für Sprachen, die ein anderes Verhalten erfordern. Das übliche Beispiel ist Türkisch, wo das i mit und ohne Punkt eine eigene Behandlung verlangt.
Das LangSys benennt eine Reihe von Feature-Indizes. Jeder Index verweist auf die FeatureList, in der ein Feature-Datensatz ein Vier-Byte-Tag (darunter ss01) und eine Liste von Lookup-Indizes trägt. Diese Indizes verweisen schließlich auf die LookupList, in der die eigentlichen Ersetzungs-Untertabellen liegen. Das Auflösen von ss01 bedeutet also: Finde das Skript, finde sein LangSys, finde das Feature mit dem Tag ss01, sammle die darin genannten Lookups und wende sie an. HotPDF verwendet standardmäßig das DFLT-Skript und das Standard-LangSys, womit die überwiegende Mehrheit der lateinischen Textdesigns ausgeliefert wird, und bietet eine Möglichkeit, das Script-Tag zu überschreiben, falls eine Schriftart ihre Features stattdessen unter einem spezifischen Skript anordnet.
Coverage-Tabellen entscheiden über die Teilnahme
Jede Ersetzungs-Untertabelle beginnt mit derselben Frage: Nimmt diese Eingabeglyphe an dieser Regel teil, und wenn ja, wo befindet sie sich in der Indexierung der Regel selbst? Diese Frage wird von einer Coverage-Tabelle beantwortet, und die Antwort ist ein Coverage-Index – eine kleine Ordnungszahl, die der Rest der Untertabelle 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 über eine binäre Suche, und ihre Position in the Liste ist ihr Coverage-Index. Format 2 ist eine Liste von Bereichsdatensätzen (Range Records), von denen jeder eine Startglyphe, eine Endglyphe und den Coverage-Index enthält, auf den die Startglyphe abgebildet wird. Eine Glyphe innerhalb eines Bereichs erhält ihren Coverage-Index durch Versatz vom Anfang des Bereichs. Format 1 ist kompakt, wenn die beteiligten Glyphen verstreut sind, Format 2, wenn sie in zusammenhängenden Läufen auftreten. Beide sind sortiert, sodass beide in logarithmischer Zeit durchsucht werden und entweder einen Coverage-Index oder ein klares "nicht abgedeckt" zurückgeben, was der Engine erlaubt, die Glyphe unverändert zu lassen.
Einfache Ersetzung, die zwei Formate
Die einfache Ersetzung (Single Substitution) entspricht dem LookupType 1 und bildet eine Glyphe auf genau einen Ersatz ab. Sie hat ebenfalls zwei Formate, und die Aufteilung ist eine Speicherplatzoptimierung. Format 1 speichert ein einzelnes vorzeichenbehaftetes Delta. Die Ausgangs-Glyphen-ID ist die Eingangs-Glyphen-ID plus dieses Delta, modulo 65536. Auf diese Weise kodiert eine Schriftart eine Ersetzung, bei der jede beteiligte Glyphe denselben festen Versatz zu ihrer Alternative aufweist, beispielsweise ein Block von Versalziffern (Lining Figures), die in konstantem Abstand zu den entsprechenden Mediävalziffern (Oldstyle Figures) platziert sind. Die Coverage-Tabelle gibt an, welche Glyphen qualifiziert sind, und das eine Delta bedient alle.
Format 2 speichert ein explizites Array von Ersatz-Glyphen-IDs. Der Coverage-Index aus der Coverage-Tabelle ist der Index in dieses Array, sodass die Glyphe am Coverage-Index 0 zum ersten Array-Eintrag wird, der Coverage-Index 1 zum zweiten und so weiter. Format 2 wird verwendet, wenn die Alternativen keinen einheitlichen Versatz aufweisen, was bei handgefertigten stilistischen Sets der Regelfall ist. Die Abfrage ist auf der Seite des Aufrufers in beiden Fällen gleich. Nehmen Sie die Eingabeglyphe, führen Sie sie durch die Coverage-Prüfung, 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, []);
// Default glyph for 'a' through the font's cmap.
BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));
// Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');
// AltGID = BaseGID means the feature did not touch this glyph.
if AltGID <> BaseGID then
{ emit AltGID in the content stream };
finally
Pdf.Free;
end;
end;
Der bemerkenswerte Vertrag ist das einfache Durchreichen. GetSingleSubstituteGlyph gibt bei jedem Fehler die Eingangs-Glyphen-ID unverändert zurück: wenn keine Schriftart, keine GSUB-Tabelle, kein passendes Feature oder kein Coverage-Treffer vorhanden ist. Das bedeutet, dass der Aufruf bedenkenlos bedingungslos durchgeführt werden kann. Sie fragen nach der Alternative, und wenn es keine gibt, erhalten Sie genau das zurück, was Sie übergeben haben. Der aufrufende Code muss also niemals eine Schriftart, der das Feature fehlt, gesondert behandeln.
Was die Tags für stilistische Features bedeuten
Das Feature-Tag definiert, nach welcher Alternative Sie fragen, und die für stilistische Arbeiten relevanten Tags bilden eine kurze Liste. Das wichtigste Paar ist salt (Stylistic Alternates), der allgemeine Zugriff auf die alternativen Formen einer Glyphe, und ss01 bis ss20, die zwanzig nummerierten stilistischen Sets, die eine Schriftart definieren kann – jedes ein benanntes Paket von Ersetzungen, die der Designer zusammenfasst. Eine Schriftart könnte beispielsweise ein einstöckiges a und ein R mit geradem Bein unter ss03 ablegen, sodass die Aktivierung dieses einen Sets beide umgestaltet.
Darüber hinaus gibt es weitere Tags für einfache Ersetzungen. aalt (Access All Alternates) ist die Vereinigung aller Alternativen, die eine Glyphe besitzt, üblicherweise als Glyphenpaletten-Feature dargestellt. titl wählt Titelsatz-Versalien (Titling Capitals) aus, die für große Schriftgrößen optimiert sind. subs und sups tauschen echte tief- und hochgestellte Ziffern ein, anstatt verkleinerte Standardziffern zu verwenden. ordn erzeugt Ordinalformen, wie die hochgestellten Buchstaben in 1st und 2nd. frac baut Brüche auf, obwohl vollständige diagonale Brüche auch auf Ligatur- und Kontextlogik beruhen, die über die einfache Ersetzung hinausgeht. Für die Fälle mit einzelnen Glyphen ist der Mechanismus identisch mit ss01: Übergeben Sie das Tag an die Ersetzungsabfrage und lesen Sie die alternative Glyphe aus.
// Try a stylistic-set feature, then fall back to plain alternates.
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');
// Still BaseGID if neither feature covers this glyph.
end;
cmap-Format 12 und die ergänzenden Ebenen
Bevor eine Ersetzung ausgeführt werden kann, muss ein Zeichen in eine Glyphe umgewandelt werden, und das ist die Aufgabe der cmap-Tabelle. Die Ersetzungsabfrage beginnt bei einer Glyphen-ID, sodass der Pfad immer vom Zeichen zur Glyphe über cmap und dann von der Glyphe zur Alternative über GSUB verläuft. Der interessante Teil von cmap ist seine Reichweite. Eine Format-4-Untertabelle deckt die Basic Multilingual Plane (die ersten 65.536 Codepunkte) ab, was für die meisten lateinischen Texte ausreicht. Sie reicht jedoch nicht für Codepunkte ab U+10000 (die ergänzenden Ebenen bzw. Supplementary Planes) aus, wo sich heute mathematische Alphanumerik, viele Symbole und verschiedene lebende Schriften befinden.
Format 12 ist die Untertabelle, die den gesamten Bereich von U+0000 bis U+10FFFF abdeckt. Es handelt sich um eine sortierte Liste von Gruppen, wobei jede Gruppe einen Start-Codepunkt, einen End-Codepunkt und eine Start-Glyphen-ID enthält, sodass ein zusammenhängender Lauf von Codepunkten auf einen zusammenhängenden Lauf von Glyphen abgebildet wird. HotPDF löst Codepunkte mit einer Hybridstrategie auf, die der Struktur der Daten entspricht. Codepunkte in der BMP werden aus einem direkten Array bedient, das über den Codepunkt indiziert ist – ein einzelner Lookup ohne Suche. Codepunkte in den ergänzenden Ebenen werden aus einer dünn besetzten Tabelle bedient, die nach Codepunkten sortiert ist und über eine binäre Suche durchsucht wird. Das Ergebnis ist, dass GetUnicodeGlyphForCodepoint einen vollständigen Cardinal-Wert entgegennimmt und über den gesamten Bereich korrekt antwortet, wobei die Glyphen-ID 0 (die .notdef-Glyphe) für jeden Codepunkt zurückgegeben wird, den die Schriftart nicht abbildet.
var
Pdf: THotPDF;
Cp: Cardinal;
GID, StyledGID: Word;
begin
// A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
Cp := $1D49C;
GID := Pdf.GetUnicodeGlyphForCodepoint(Cp); // format 12 lookup
if GID <> 0 then
StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
else
StyledGID := 0; // font has no glyph for this code point
end;
Wo diese Abfragen enden
Die APIs für einfache Ersetzungen beantworten eine bestimmte Form von Frage, und es ist wichtig zu klären, was sie nicht beantworten. LookupType 1 ist einer von acht Ersetzungstypen. Die Abfrage verarbeitet weder LookupType 2 (Mehrfachersetzung, bei der aus einer Glyphe mehrere werden) noch LookupType 4 (Ligaturersetzung, bei der aus mehreren Glyphen eine wird). Sie verarbeitet nicht die kontextuellen und verketteten kontextuellen Typen (LookupTypes 5 und 6), die nur ausgelöst werden, wenn eine Glyphe in einer bestimmten Umgebung erscheint, und auch nicht die Erweiterungs- und Umkehrverkettungstypen. Ein diagonaler Bruch, ein Devanagari-Konjunkt oder eine arabische Initial-Medial-Final-Kaskade ist ein Sequenzproblem, und ein zeichenweiser einfacher Ersetzungs-Lookup kann dies nicht ausdrücken.
Sie führt auch kein automatisches Shaping durch. Nichts hier untersucht einen Textlauf, entscheidet, welche Features aktiviert werden sollen, und wendet sie in der vom Skript geforderten Reihenfolge an. Der Aufrufer wählt das Feature-Tag aus und wendet es Glyphe für Glyphe an. Dies ist genau das richtige Werkzeug für stilistische Sets und Alternativen, die optional und lokal sind, und genau das falsche Werkzeug für ein Skript, das eine Neuordnung erfordert. Die Grenze scharf zu halten sorgt dafür, dass der Ersetzungspfad klein und vorhersehbar bleibt.
Für Fälle, die eine Verarbeitung auf Sequenzebene erfordern, wird das Thema komplexe Skripte in unserem Artikel über Textgestaltung für komplexe Skripte in Delphi aufgegriffen. Wenn Ihre Ersetzungen Teil eines größeren Berichtsauftrags sind, der auch Bilder und andere Schriftarten auf der Seite platziert, beschreibt der Leitfaden zur Berichtsausgabe mit Schriftarten und Bildern, wie diese Teile zusammenpassen. Alle diese Funktionen laufen auf derselben Engine, der HotPDF-Komponente für Delphi und C++Builder, die die GSUB-Ersetzungsabfragen neben den APIs für Schrifteinbettung, Subsetting und Text bereitstellt, die an anderer Stelle in diesem Blog behandelt werden.