Technischer Artikel

Webfonts in PDF: WOFF2 und variable Achsen in Delphi

PDF Library for Delphi akzeptiert die Schriften, die ein Webprojekt bereits mitbringt. Sie importiert WOFF2-Container, zählt die Designachsen einer variablen Schrift als JSON auf und instanziiert einen gewählten Satz Achsenkoordinaten zu einer statischen Schrift, die die gewöhnlichen Einbettungs-, Subsetting- und Extraktionspfade handhaben, ohne irgendetwas über Variationen zu wissen. Sowohl TrueType- als auch CFF2-Umrisse werden abgedeckt

Die Situation, die dies adressiert, ist häufig und ärgerlich. Die Typografie einer Marke ist in einem Design-System definiert, das Design-System liefert variable WOFF2-Dateien, und der PDF-Generator kann weder den Container noch die Variationsdaten lesen. Der übliche Workaround — die Schriftgießerei um statische TTF-Instanzen jedes tatsächlich verwendeten Schriftschnitts zu bitten — dauert Wochen und erzeugt Dateien, die niemand synchron hält

Warum ist WOFF2 nicht einfach eine komprimierte Schrift?

Behandelt man WOFF2 als bloßen Brotli-Wrapper und dekomprimiert ihn, erhält man Bytes, die kein gültiges sfnt sind. Das Format erlaubt außerdem transformierte und umsortierte Tabellen: glyf, loca und hmtx können in umgeordneter Form gespeichert sein, die rückgängig gemacht werden muss, bevor die Schrift parsbar ist. Ein Allzweck-Dekompressor kann das nicht

Unter Windows stellt die Plattform seit Windows 10 Version 1703 Container-Analyse und -Entpackung bereit, daher verwendet die Bibliothek die Systemimplementierung, statt einen weiteren Decoder mitzuliefern. Diese Entscheidung hält die Bereitstellung frei von einer zusätzlichen DLL und bedeutet, dass die Transformationsumkehr vom selben Code gehandhabt wird, den der eigene Textstack des Betriebssystems verwendet. Auf anderen Plattformen läuft WOFF1 weiterhin über einen reinen Pascal-Pfad, und WOFF2 wird explizit als nicht unterstützt gemeldet, was eine ehrliche Antwort ist statt einer still beschädigten Schrift

Der Importpfad validiert vor und nach dem Dekodieren: Der 48-Byte-Header, die Containerlänge, die reservierten Felder, die deklarierte sfnt-Größe und eine Obergrenze von 512 MiB werden alle zuerst geprüft, und die dekodierte Stream-Größe wird anschließend mit der Deklaration verglichen. Ein Webfont ist heruntergeladener Inhalt, und die zwei Fehlermodi, die es zu blockieren lohnt, sind eine Größenabweichung und eine Dekompressionsbombe

Dieses Pipeline-Diagramm zeigt, wie die Delphi-PDF-Bibliothek einen WOFF2-Webfont importiert: Header-Validierung, Systemdekodierung, Umkehr der sfnt-Transformation und eine Größenprüfung nach dem Dekodieren
WOFF2-Container kommen als heruntergeladener Inhalt an, daher validiert der Import Header und deklarierte Größen vor und nach dem Dekodieren
uses
  PDFlibrary;

var
  Lib: TPDFlib;
  FontID, InstanceID: Integer;
  AxesJSON: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    // WOFF2, WOFF1, TTF, OTF und TTC laufen alle über denselben Aufruf
    FontID := Lib.AddOpenTypeFontFromFile('Inter.woff2', 0);
    if FontID <= 0 then
      raise Exception.Create('font could not be imported');

    // Was kann diese Schrift tatsächlich?
    if Lib.GetFontVariationAxes(FontID, AxesJSON) = 1 then
      Log(AxesJSON);   // Tags, Minimum, Standard, Maximum je Achse

    // Eine Design-Position in eine statische Schrift backen
    InstanceID := Lib.CreateOpenTypeFontVariation(FontID,
      'wght=620,wdth=87.5');

    Lib.DrawOpenTypeText(56, 760, InstanceID, 24, 'Quarterly results', 0);
    Lib.SaveToFile('report.pdf');
  finally
    Lib.Free;
  end;
end;

Was Instancing neu aufbauen muss, und was es entfernt

Eine variable Schrift speichert einen Standardumriss pro Glyphe plus Deltas, indiziert nach Position im Designraum. Eine statische Instanz zu erzeugen bedeutet, diese Deltas an den angeforderten Koordinaten zu interpolieren, die Punkte zu verschieben und dann alles davon Abgeleitete neu zu berechnen

Für TrueType-Umrisse bedeutet das, fvar, avar, gvar, HVAR und MVAR zu parsen, Tupel-Deltas mit gemeinsamer und privater Punktnummerierung anzuwenden und die Punkte zu interpolieren, die kein explizites Delta tragen. Horizontale Phantompunkte bestimmen zusammen mit HVAR Vorschubbreite und linke Seitenlast. Bei zusammengesetzten Glyphen müssen zuerst ihre Komponentenabhängigkeiten und Punktzuordnungen aufgelöst werden, dann Translation, Skalierung, Matrix und Anbindungs-Offsets angewendet werden — mit Tiefenlimits und Zyklenerkennung, denn ein bösartiger Komponentengraph würde sonst endlos rekursieren

Metadaten wandern ebenfalls mit, und das ist der Teil, den die meisten Implementierungen auslassen. MVAR aktualisiert die globalen Metriken in hhea, OS/2 und post, sodass Zeilenabstand, Unterstreichungsposition und Kursivwinkel zur Instanz statt zum Standard passen. Die Achsen wght, wdth, ital und slnt aktualisieren zusätzlich Gewichtsklasse, Breitenklasse, Auswahl-Flags, macStyle und Kursivwinkel, sodass ein die statische Schrift untersuchendes Werkzeug den angeforderten Stil meldet. Eine Schrift, deren Geometrie fett ist und deren Metadaten regulär sagen, wird überall nachgelagert falsch beschriftet

Die Ausgabe glättet dann zusammengesetzte Umrisse und entfernt die nicht mehr geltenden Tabellen: die Variationstabellen, Hints, Device-Tabellen und jede Signatur. Glyphengrenzen, hhea-Zusammenfassungsmetriken, die maxp-Hint-Obergrenzen, Prüfsummen pro Tabelle und die Gesamtschrift-Prüfsumme werden alle neu berechnet

Dieses Diagramm zeigt, wie variables Font-Instancing einen TrueType-Schriftschnitt neu aufbaut, indem es gvar-Deltas interpoliert, zusammengesetzte Glyphen expandiert, Vorschübe neu berechnet und Stilmetadaten umschreibt, bevor die Variationstabellen entfernt werden
Instancing baut alles neu auf, was die Deltas berührt haben, und entfernt dann die Variationsmechanik, sodass gewöhnliches Einbetten eine schlichte statische Schrift sieht

CFF2 nimmt einen anderen Weg zum selben Ziel

CFF2 kann nicht direkt als PDF-Font-Programm eingebettet werden: Seine Charstrings tragen keine Breiten, und es verlässt sich für Glyphenreihenfolge und Metriken auf die umgebenden OpenType-Tabellen. Das Instanziieren einer CFF2-Schrift erzeugt daher stattdessen eine CID-verschlüsselte CFF-OpenType-Schrift, die die bestehenden Zeichen-, Einbettungs- und Subsetting-Pfade bereits handhaben

Das Laden einer CFF2-Schrift erzeugt sofort eine statische Instanz mit Standardkoordinaten für das aktuelle Dokument, während das ursprüngliche CFF2-sfnt am Schriftobjekt erhalten bleibt, sodass Achsenaufzählung und spätere Instanziierung weiterhin den vollen Designraum sehen. Jede Glyphe wird beim Instanziieren einzeln de-subroutinisiert, was die Variationsarithmetik vollständig entfernt und den bestehenden Subsetter unverändert arbeiten lässt. Der Kompromiss besteht darin, dass eine statische Instanz etwas größer sein kann als eine, die gemeinsame Subroutinen behalten hat — ein fairer Preis dafür, keine zweite Subsetting-Implementierung zu benötigen

Eine Instanz verifizieren, bevor Sie ihr vertrauen

Instancing ist die Art von Feature, das auf subtile Weise falsch sein kann, ohne dass eine Exception das meldet. Drei Prüfungen fangen fast alles ab

Exportieren Sie die Instanz und öffnen Sie sie in einem Schriftwerkzeug. SaveFontToFile schreibt das erhaltene Font-Programm, sodass sich eine Instanz im Speicher ausgeben lässt, ohne zuvor ein PDF zu erzeugen. Die exportierte Datei sollte sauber parsen, keine Variationstabellen enthalten und die angeforderten Stilmetadaten melden

Vergleichen Sie Vorschübe an einer nicht-standardmäßigen Position. Kann Ihre Pipeline dieselbe Instanz auf andere Weise erzeugen, vergleichen Sie Vorschubbreiten und Kurvengrenzen Glyphe für Glyphe; die Referenzimplementierung hier stimmte mit einer unabhängigen Instanziierung einer 313-Glyphen-Zwei-Achsen-Schrift mit einer maximalen Abweichung von null über jede Glyphe hinweg überein

Und extrahieren Sie den Text aus dem erzeugten PDF zurück. Eine Instanz, die sich einbettet, aber keinen extrahierbaren Text erzeugt, hat ihre ToUnicode-Zuordnung verloren, was für Suche und Barrierefreiheit lange nach bestandener visueller Prüfung noch von Bedeutung ist. Der breitere Extraktionspfad ist unter Text-, Bild- und Schriftextraktion behandelt

Dieses Diagramm zeigt drei Verifikationsprüfungen für eine erzeugte variable Font-Instanz: das Ausgeben der exportierten Font-Datei, den Vergleich der Glyphen-Vorschübe mit einer unabhängigen Instanziierung und das Extrahieren des Texts aus dem PDF
Eine subtil falsche Instanz löst keine Exception aus, daher werden die exportierte Datei, die Glyphen-Vorschübe und der extrahierte Text nacheinander geprüft
// Eine erzeugte Instanz zur Inspektion oder Wiederverwendung ausgeben
Lib.SaveFontToFile('Inter-620-87.ttf');

// Fordern Sie Koordinaten außerhalb des deklarierten Bereichs an, werden sie geklemmt,
// lesen Sie also das Achsen-JSON, statt die Grenzen einer Schrift anzunehmen
// {"axes":[{"tag":"wght","min":100,"default":400,"max":900}, ...]}

Praktische Hinweise für eine Marken-Font-Pipeline

Cachen Sie Instanzen nach Koordinatensatz. Jeder eigenständige Koordinatensatz ist ein eigenständiges Font-Programm, sodass ein Dokument, das Schnitt 400 und Schnitt 620 mischt, zwei Subsets statt eines gemeinsamen Schriftschnitts einbettet. Dieselbe Instanz wiederholt zu erzeugen ist verschwendete Arbeit; Dutzende fast identischer Schnitte zu erzeugen ist verschwendete Dateigröße

Messen Sie mit der Instanz, mit der Sie auch zeichnen werden. Eine schmale (Condensed) Instanz hat schmalere Vorschübe als der Standard, sodass jedes Layout, das vor dem Instancing gemessen hat, die falschen Breiten gemessen hat. Das ist die mit Abstand häufigste Ursache für Text, der nur in der Produktion aus seiner Box überläuft

Halten Sie einen Fallback-Pfad für die Glyphen bereit, die Ihrer Markenschrift fehlen. Eine variable Markenschrift deckt typischerweise Latein und wenig sonst ab, und ein Bericht mit einem kyrillischen Kundennamen zeigt sonst fehlende Glyphen; der automatische Fallback-Mechanismus ist unter automatischer Font-Fallback für fehlende Glyphen beschrieben. Und wenn das Ziel eine kleinere Datei statt eines bestimmten Schnitts ist, leistet Subsetting mehr Arbeit als Instancing, wie unter PDF-Dateigrößenoptimierung und Font-Subsetting behandelt

Webfont-Import, variables Instancing, Subsetting und Fallback sind Teil derselben Font-Pipeline für Delphi, C++Builder und Free Pascal; die vollständige Funktionsliste finden Sie auf der Seite der PDF Library for Delphi