Technischer Artikel

Variable Schriften in PDF: Statische Instanziierung in Delphi

PDF kennt kein Konzept einer variablen Schrift. Eine in eine PDF-Datei eingebettete Schrift ist ein fester Satz von Outlines mit festen Metriken, daher muss eine variable Schrift auf eine statische Instanz reduziert werden, bevor sie in ein Dokument gelangen kann. HotPDF führt diese Instanziierung intern durch: Man prüft die Achsen einer variablen Schrift, wählt Koordinaten wie Gewicht 620 oder Breite 87,5, und die Bibliothek brennt diese Werte in ein vollständiges, in sich geschlossenes Schriftprogramm ein, das jeder konforme PDF-Reader darstellen kann

Der Grund, warum dies wichtig ist, ist praktisch und nicht theoretisch. Schriftgießereien liefern zunehmend eine variable Datei statt eines Dutzends statischer Schnitte, und Designteams wählen Werte, die keine benannte Instanz bereitstellt. Ohne Instanziierung greift ein Berichtsgenerator entweder auf die Standardinstanz zurück, was die Designentscheidung verwirft, oder er bettet die gesamte variable Schrift ein und hofft, dass der Viewer Achskoordinaten beachtet, von denen er nichts wissen kann – wozu kein Reader verpflichtet ist

Was muss die Instanziierung eigentlich neu aufbauen?

Eine OpenType-Variable-Schrift speichert einen Standard-Outline pro Glyphe zuzüglich eines Satzes von Deltas, indiziert nach Position im Designraum. Eine Achskoordinate anzuwenden ist keine Frage, eine Zahl in einen Header zu schreiben; es bedeutet, die gvar-Tabelle zu durchlaufen, Deltas für die angeforderte Position zu interpolieren, Punkte zu verschieben und dann alles neu zu berechnen, was aus diesen Punkten abgeleitet wurde. HotPDF baut die Glyphen-Outlines, die lange loca-Tabelle, die vollständigen horizontalen und vertikalen Metriken, die globale Schrift-Bounding-Box und die sfnt-Prüfsummenanpassung neu auf

Ebenso wichtig ist, was entfernt wird. Eine statische Instanz darf fvar, avar, gvar, HVAR, VVAR, MVAR, STAT oder cvar nicht behalten, und eine veraltete DSIG muss ebenfalls verschwinden, da die signierten Bytes nicht mehr existieren. Bleibt eines davon zurück, entsteht eine Schrift, die vorgibt, variabel zu sein, aber Outlines trägt, die bereits verschoben wurden, und Reader, die Variationen tatsächlich anwenden, würden sie dann ein zweites Mal anwenden

Phantompunkte und die Falle der doppelten Anwendung

Die subtilste Regel im gesamten Prozess betrifft die Metriken. In gvar umfasst die Punktanzahl einer Glyphe die Outline-Punkte, beziehungsweise die Komponentenpunkte bei einer zusammengesetzten Glyphe, zuzüglich vier Phantompunkte, die die linke Seitenlast, die Vorschubbreite und deren vertikale Entsprechungen kodieren. Diese Phantompunkte unterliegen selbst Deltas

Wenn eine Schrift also eine gvar-Tabelle besitzt, leitet HotPDF horizontale und vertikale Metriken aus den interpolierten Phantompunkten ab und wendet HVAR oder VVAR nicht zusätzlich an. Beides hinzuzufügen ist der klassische Fehler: Dieselbe Variation wird zweimal angewendet, und jede Vorschubbreite fällt geringfügig zu breit aus, was sich als Text zeigt, der in einer Blocksatzzeile allmählich nach rechts driftet. Nur wenn eine Schrift kein gvar besitzt, brennt die Bibliothek den Metrik-Variationsspeicher direkt in hmtx oder vmtx ein

Zwei weitere Details halten die Geometrie korrekt. Phantompunkte nehmen nie an der Konturinterpolation teil, daher werden bei einer einfachen Glyphe nicht explizit gelistete Punkte per IUP je Kontur abgeleitet, wobei die Phantompunkte ausgeschlossen bleiben. Und bei zusammengesetzten Glyphen werden die Deltas auf Komponenten-Offsets angewendet, die XY-Parameter nutzen, wonach die Kind-Bounds rekursiv neu berechnet werden. Diese Rekursion ist in der Tiefe begrenzt und auf Zyklen geprüft, da ein bösartiger oder schlicht defekter Komponentengraph sonst endlos rekursieren könnte

Den Designraum vor der Auswahl untersuchen

Der erste Aufruf in jedem Instanziierungs-Workflow ist InspectVariableFont, der die Achsen und die von der Gießerei definierten benannten Instanzen meldet. Achsdatensätze tragen das Vier-Byte-Tag, den Minimal-, Standard- und Maximalwert, Flags und eine Namens-ID; benannte Instanzen tragen eine Subfamilien-Namens-ID, Flags, eine optionale PostScript-Namens-ID und eine Koordinate pro Achse:

var
  Pdf: THotPDF;
  Axes: THPDFVariableFontAxisArray;
  Instances: THPDFVariableFontNamedInstanceArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.InspectVariableFont('C:\Fonts\Inter.ttf', Axes, Instances) then
    begin
      for I := 0 to High(Axes) do
        Writeln(Format('%s  min=%.1f default=%.1f max=%.1f',
          [string(Axes[I].Tag), Axes[I].MinimumValue,
           Axes[I].DefaultValue, Axes[I].MaximumValue]));
      Writeln(Format('%d named instance(s) defined', [Length(Instances)]));
    end
    else
      Writeln('not a variable font - embed it as an ordinary TrueType face');
  finally
    Pdf.Free;
  end;
end;

Den Achsbereich zu melden ist wichtig, weil Achswerte auf den von der Schrift deklarierten Bereich begrenzt werden, nicht auf den Bereich, den die eigene Oberfläche anbietet. Ein Schieberegler, der einem Nutzer erlaubt, Gewicht 1000 bei einer Schrift anzufordern, deren wght-Achse bei 900 endet, sollte in der Oberfläche korrigiert werden, nicht stillschweigend auf der Schriftebene, sonst weicht die gedruckte Ausgabe von der Vorschau ab

Koordinaten auswählen und das Dokument ausgeben

Die Achsauswahl ist zustandsbehaftet und gilt für danach registrierte Schriften. SetVariableFontAxis nimmt ein vierstelliges druckbares ASCII-Tag und einen endlichen Wert entgegen und weist alles andere mit einer Exception zurück, statt es stillschweigend zu ignorieren. ClearVariableFontAxes setzt die Auswahl zurück, und GetVariableFontAxisSelections meldet, was aktuell ansteht, was sich in Berichts-Engines zu protokollieren lohnt, in denen mehrere Codepfade dasselbe Dokumentobjekt berührt haben könnten. Die Familie selbst wird per Name über SetFont ausgewählt, genau wie jede andere eingebettete TrueType-Schrift:

begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;

    Pdf.SetVariableFontAxis('wght', 620);   // Semibold, keine benannte Instanz
    Pdf.SetVariableFontAxis('wdth', 87.5);  // leicht schmal (condensed)
    Pdf.CurrentPage.SetFont('Inter', [], 11);
    Pdf.CurrentPage.TextOut(72, 720, 0, 'Quarterly results');

    Pdf.ClearVariableFontAxes;              // zurück zur Standardinstanz
    Pdf.CurrentPage.SetFont('Inter', [], 10);
    Pdf.CurrentPage.TextOut(72, 700, 0, 'Prepared by the finance team');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Das Ereignis OnVariableFontInstance löst aus, sobald jede Instanz erzeugt wird, und meldet die verwendeten Achswerte, was der günstigste Weg ist, in einem Protokoll nachzuweisen, was ein gegebenes PDF tatsächlich enthält. Da jeder unterschiedliche Koordinatensatz ein eigenes Schriftprogramm ergibt, sollte man Achsauswahlen als Teil des Schrift-Cache-Schlüssels behandeln; die Caching-Mechanik wird in dem persistenten Schrift-Subset-Cache beschrieben

Wie interagiert Instanziierung mit Subsetting und Shaping?

Die Instanziierung läuft vor dem Subsetting, und diese Reihenfolge ist die richtige. Die instanziierte Schrift ist eine normale statische TrueType-Schrift, daher behandelt sie der gewöhnliche Subsetter wie jede andere: Er berechnet die Glyphen-Closure, behält die vom Dokument tatsächlich verwendeten Glyphen und verwirft den Rest. Zu beachten ist die Wechselwirkung, dass zwei unterschiedliche Achsauswahlen derselben Familie zwei unterschiedliche Schriftprogramme sind, sodass ein Dokument, das Gewicht 400 und Gewicht 620 mischt, zwei Subsets einbettet, nicht eine gemeinsame Schrift mit zwei Instanzen

Shaping ist grundsätzlich nicht betroffen und lohnt sich in der Praxis zu prüfen. Layout-Features leben in GSUB und GPOS, welche die Instanziierung erhält, sodass Ligaturen und stilistische Alternativen weiterhin funktionieren, wie in OpenType-GSUB-Stilalternativen beschrieben. Was sich ändert, ist die Positionierung: Eine schmale (condensed) Instanz hat schmalere Vorschübe als die Standardinstanz, daher hat jedes Layout, das Text vor der Instanziierung vermessen hat, die falschen Breiten vermessen. Mit derselben Achsauswahl vermessen, mit der auch gerendert wird, und die Abweichung verschwindet

Eine letzte defensive Anmerkung aus der Implementierung, nützlich für alle, die diesen Pfad erweitern. Schriften ohne vertikale Metriken werten dynamische Array-Argumente an der Delphi-Aufrufstelle trotzdem aus, daher werden Arrays zur Parsezeit stets zugewiesen, statt sich auf eine HasVerticalMetrics-Prüfung zu verlassen, um an einem leeren Index vorbeizuspringen. Es ist genau die Art von sprachtechnischem Detail, das einen scheinbar abgesicherten Zweig ausgerechnet bei den Schriften, mit denen nicht getestet wurde, zu einer Zugriffsverletzung macht

Die Unterstützung variabler Schriften fügt sich in dieselbe Schrift-Pipeline wie Einbettung, Subsetting und Glyphen-Closure ein, die ausführlicher in Schrift-Subset-Closure und geformte Glyphen beschrieben wird. Der vollständige Typografie-Funktionsumfang für Delphi und C++Builder ist auf der HotPDF-Delphi-PDF-Komponentenseite aufgeführt