Typografie: Font-Registry, Subsetting, CMap, Encoding, BiDi
Auf einen Blick
Abschnitt betitelt „Auf einen Blick“Das Typografie-Modul verwandelt eine Font-Datei und einen Unicode-String in die Bytes, die ein PDF-Content-Stream benötigt. Es übernimmt das Parsen von Fonts, die prozesslebenslange Registry, das Glyph-Subsetting, die ToUnicode-CMap, cmap-bewusste Encoding-Strategien und die Unicode-Bidi-Engine.
Installation
Abschnitt betitelt „Installation“composer require nextpdf/core:^3Konzeptioneller Überblick
Abschnitt betitelt „Konzeptioneller Überblick“FontRegistry ist der prozesslebenslange Font-Speicher und implementiert FontRegistryInterface. Sie parst eine TrueType-, OpenType-, TTC- oder Type-1-Datei (PFB und AFM) einmal und gibt ein unveränderliches FontInfo zurück. Die Registry ist für langlaufende Worker ausgelegt: Laden Sie einen Font-Satz beim Boot vor und rufen Sie lock() auf. Danach weist die Registry jede Mutation zurück, während Lookups weiterhin Anfragen bedienen. Sie hält ausschließlich PHP-Daten — geparste Metadaten und die rohen Font-Bytes —, sodass sich ein Worker-Pool eine einzige Instanz teilen kann. registerFromBinary() nimmt rohe Font-Bytes entgegen, die die HTML-@font-face-Brücke für einen Font verwendet, der aus einer entfernten Quelle oder einer Daten-URI geladen wurde.
Die Engine bettet jeden verwendeten Font ein und subsettet ihn. Ein eingebettetes Font-Programm wird im PDF mitgeführt, sodass das Dokument in jedem Viewer gleich rendert, unabhängig von den installierten Systemfonts — ISO 32000-2 §9. Ein Subset enthält nur die Glyphen, die das Dokument referenziert, was für CJK- oder Unicode-reiche Inhalte entscheidend ist — ISO 32000-2 §9. FontSubsetter parst das ursprüngliche Tabellenverzeichnis, extrahiert die cmap, löst die Abhängigkeiten zusammengesetzter Glyphen als transitive Hülle auf und baut die Tabellen head, hhea, maxp, cmap, loca, glyf und hmtx neu auf. Es bewahrt die ursprüngliche Glyph-Identifier-Nummerierung und füllt ungenutzte Slots mit Nullen auf, sodass ein CIDToGIDMap vom Typ /Identity gültig bleibt. Es gibt den ursprünglichen Font unverändert zurück, wenn das Subset weniger als zehn Prozent einsparen würde, und vermeidet damit Arbeit, die sich nicht auszahlt. CffSubsetter führt die äquivalente Operation für OpenType-Fonts aus, die eine Compact-Font-Format-Outline-Tabelle enthalten.
Die Textausgabe ist eine dreistufige Übersetzung: Unicode-Codepunkt, dann Zeichencode im Content-Stream, dann Glyph-Identifier innerhalb des Fonts. Das Modul modelliert diesen Ablauf als einen einzigen expliziten Kollaborator. FontInfo::encodeText() ist die Fassade; FontEncodingStrategyResolver leitet je Font weiter. Ein eingebetteter TrueType- oder OpenType-Font, der eine Unicode-cmap trägt, wird an TrueTypeCmapStrategy geleitet, die einen Zwei-Byte-Identity-H-Hex-Stream ausgibt. Das ist die Form, die ein Type-0-Font mit einer Identity-H-CMap und einem CIDFontType2-Nachfahren benötigt (ISO 32000-2 §9.7.4; der passende RAG-Chunk-Digest wurde durch das Lizenz-Cap abgeschnitten zurückgegeben, festgehalten in _downgraded-claims-o3.md). Jeder andere Font — Base-14-Standardfonts, Type-1-PFB und -AFM — wird an Base14EncodingStrategy geleitet, die einen Ein-Byte-WinAnsi-Literal-String ausgibt. Dieser Stream umfasst das vollständige WinAnsiEncoding-Repertoire (Windows-Codepage 1252) — lateinische Zeichen mit Akzenten, das Euro-Zeichen und gängige typografische Satzzeichen. Codepunkte außerhalb davon werden aus dem Ein-Byte-Stream verworfen und nutzen einen Font-Fallback pro Cluster, sofern ein abdeckender Font registriert ist (ISO 32000-2 Annex D.2). Der Resolver ist über dem FontInfo-Wertebereich total; es gibt keinen nullbaren Pfad. ToUnicodeCMapBuilder baut die /ToUnicode-Ressource, mit der ein Reader aus einem Identity-H-Font den ursprünglichen Unicode-Text wiederherstellen kann. Er wendet ein gieriges bfrange-Coalescing und ein Block-Cap von 100 Einträgen an.
BidiEngine ist der Grenzdienst für den Unicode-Bidirectional-Algorithmus (UAX #9, Unicode 16). Bei deaktivierter Isolate-Unterstützung delegiert sie an den Legacy-Resolver, sodass bestehende Aufrufer unberührt bleiben. Bei aktivierter Unterstützung durchläuft sie die isolate-bewusste Pipeline: den Stack für explizite Isolate mit einer maximalen Tiefe von 125, die Durchläufe für schwache Typen, die Durchläufe für neutrale Typen einschließlich der Auflösung gepaarter Klammern sowie die Durchläufe für implizite Ebenen und Zeilen-Reordering. Die CJK-Glyph-Abdeckung für einen Kandidaten-Font ist eine eigene Diagnose: CjkFontValidator tastet die erforderlichen Unicode-Blöcke pro Schriftsystem ab und meldet einen Abdeckungsprozentsatz.
API-Oberfläche
Abschnitt betitelt „API-Oberfläche“| Typ | Art | Wichtige Mitglieder | Stabilität | Seit |
|---|---|---|---|---|
FontRegistry | final class | register(), registerType1(), registerFromBinary(), registerFromDirectory(), get(), has(), all(), warmup(), lock(), isLocked(), memoryUsage() | stable | 1.7.0 |
FontInfo | final readonly class | $family, $type, $widths, $unicodeMap, $cmapForward, getKey(), encodeText() | stable | 1.0.0 |
FontSubsetter | final class | subset(string, array<int>, int): string | stable | 1.0.0 |
CffSubsetter | final class | OpenType/CFF-Outline-Subsetting | stable | 1.0.0 |
FontEncodingStrategyResolver | final class | resolve(FontInfo): FontEncodingStrategy | stable | 2.7.0 |
ToUnicodeCMapBuilder | final class | buildFromRun(), buildFromMap(), encodeUnicodeUtf16Be() | stable | 2.7.0 |
BidiEngine | final class | UAX #9 isolate-bewusste Auflösung | stable | 3.1.0 |
CjkFontValidator | final class | validateCoverage(), detectScript(), isCjkCodepoint() | stable | 1.0.0 |
FontInfo ist unveränderlich: Seine Konstruktorsignatur und seine öffentlichen Eigenschaften sind eingefroren. Die Encoding-Strategien sind reine Funktionen von (FontInfo, UTF-8 text) — gleiche Eingabe, gleiches EncodedGlyphRun, bei jedem Aufruf.
Code-Beispiel — Schnellstart
Abschnitt betitelt „Code-Beispiel — Schnellstart“<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Typography\Encoding\EncodingMode;use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();$cjkFont = $registry->register('/path/to/NotoSansTC-Regular.ttf', alias: 'NotoSansTC');
$encoded = $cjkFont->encodeText('PDF 2.0 引擎 — 使用 CMap 編碼');
// An embedded CJK TrueType face resolves to the two-byte Identity-H path.assert($encoded->mode === EncodingMode::TwoByteCid);register() parst den Font einmal und gibt ein unveränderliches FontInfo zurück. encodeText() führt über den Resolver und gibt ein EncodedGlyphRun zurück, das den Byte-Stream, den PDF-String-Operanden, die Advance-Breiten pro Glyph und die GID-zu-Unicode-Map enthält, die von einer /ToUnicode-CMap konsumiert wird.
Code-Beispiel — Produktion
Abschnitt betitelt „Code-Beispiel — Produktion“<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Exception\NextPdfException;use NextPDF\Typography\FontRegistry;use Psr\Log\LoggerInterface;
final readonly class FontBootstrap{ public function __construct( private FontRegistry $registry, private LoggerInterface $logger, ) {}
/** * Warm a font set at worker boot, then lock the registry for the * lifetime of the process. * * @param list<string> $fontFiles Absolute paths to font files. */ public function boot(array $fontFiles): void { try { $this->registry->warmup($fontFiles); $this->registry->lock(); } catch (NextPdfException $e) { $this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e; }
$report = $this->registry->memoryUsage(); $this->logger->info('Font cache primed', [ 'fonts' => $report->entryCount, 'bytes' => $report->currentBytes, ]); }}warmup() gefolgt von lock() ist die Boot-Sequenz des Workers. Nach lock() wirft jede Mutation. Lookups bedienen weiterhin Anfragen. memoryUsage() gibt einen MemoryReport zurück, sodass ein Worker den Font-Cache anhand seines Budgets überwachen kann.
Sonderfälle & Stolperfallen
Abschnitt betitelt „Sonderfälle & Stolperfallen“- Eine gesperrte Registry weist
register(),registerFromBinary(),addFontDirectory()undwarmup()zurück. Führen Sie das Warmup beim Boot aus und sperren Sie die Registry danach; registrieren Sie niemals während der Request-Verarbeitung. FontSubsetter::subset()gibt die ursprünglichen Bytes unverändert zurück, wenn die Ersparnis unter zehn Prozent läge oder wenn eine essenzielle Tabelle fehlt. Ein zurückgegebener Font, der der Eingabe gleicht, ist der dokumentierte Kein-Gewinn-Pfad, kein Fehler.- Der Subsetter bewahrt die ursprüngliche Glyph-Identifier-Nummerierung und füllt ungenutzte Glyphen mit Nullen auf. Das hält
CIDToGIDMap /Identitygültig; gehen Sie nicht davon aus, dass Glyph-Identifier auf einen zusammenhängenden Bereich neu nummeriert werden. registerFromBinary()schreibt die Bytes zum Parsen in eine temporäre Datei und löscht sowohl die Extension-Datei als auch dietempnam()-Basisdatei in einemfinally-Block. Nicht vertrauenswürdige Font-Daten sind eine Angriffsfläche beim Parsen — prüfen Sie sie, bevor sie den Parser erreichen (siehe Sicherheitshinweise).BidiEnginedelegiert unverändert an den Legacy-Resolver, wenn die Isolate-Unterstützung deaktiviert ist. Isolate-Formatierungszeichen werden dann als grenzneutral durchgereicht. Schalten Sie die Isolate-Unterstützung über die Konformitäts-Policy ein, um das volle UAX-#9-Verhalten zu erhalten.CjkFontValidatortastet Codepunkte in einem Schritt ab, statt jeden einzelnen zu testen, sodass seine Abdeckungszahl eine statistisch hinreichende Schätzung ist, keine vollständige Zählung.
Performance
Abschnitt betitelt „Performance“Das Parsen von Fonts dominiert die erste Nutzung; die Registry amortisiert diesen Aufwand auf einmal pro Prozess. Nach dem Warmup sind get() und has() O(1)-Map-Lookups. Die Subsetting-Kosten skalieren mit der Glyphenanzahl, die das Dokument tatsächlich nutzt, nicht mit der vollständigen Glyphentabelle des Fonts. Deshalb ist Subsetting für CJK-Inhalte sowohl ein Größen- als auch ein Geschwindigkeitsgewinn: Der Subsetter verarbeitet Fonts mit über 20,000 Glyphen durch binäre Suche, vorab allokierte Puffer und Bulk-String-Operationen. Die Auflösung zusammengesetzter Glyphen ist beschränkt — sie kappt bei 100 Hülleniterationen, um sich gegen zirkuläre Komponentenreferenzen zu verteidigen. Der cmap-Format-12-Parser kappt Gruppen- und Eintragsanzahlen, um den Speicher gegenüber einem feindlichen Font zu begrenzen. Das performance_budget von 1500 ms Wall-Zeit und 64 MB Peak deckt einen typischen Font-Warmup samt Dokument-Rendering ab.
Sicherheitshinweise
Abschnitt betitelt „Sicherheitshinweise“Zwei Oberflächen sind sicherheitsrelevant. Die erste ist die Font-Eingabe. register() und registerFromBinary() parsen beliebige Bytes. registerFromBinary() materialisiert eine temporäre Datei. Stream-Wrapper und Null-Bytes in Pfaden werden an der Grenze zurückgewiesen. Nicht vertrauenswürdige Font-Daten müssen eine External-Resource-Policy durchlaufen, die Dateigröße und Glyphenanzahl beschränkt, bevor sie den Parser erreichen. Die Binär-Reader des Subsetters prüfen jeden Offset gegen seine Grenzen. Die cmap-Parser kappen Gruppen-, Eintrags- und Tabellenanzahlen (numGroups > 31000 und ein Eintrags-Cap von 200,000 in Format 12), sodass ein präparierter Font keine unbeschränkte Allokation erzwingen kann. Die zweite Oberfläche ist die Textwiederherstellung: ToUnicodeCMapBuilder validiert, dass jeder Zeichencode innerhalb des 16-Bit-Codespace liegt und jeder Unicode-Wert ein gültiger Skalar ist — Surrogathälften werden zurückgewiesen —, sodass eine fehlerhafte Map keine korrupte Extraktionsressource erzeugen kann. Behandeln Sie jeden extern bereitgestellten Font oder Text als nicht vertrauenswürdig.
Konformität
Abschnitt betitelt „Konformität“| Behauptung | Standard | Abschnitt | Nachweis |
|---|---|---|---|
| Jeder vom Dokument verwendete Font wird eingebettet, sodass das Dokument ohne Rückgriff auf Systemfonts rendert. | ISO 32000-2 | §9 | |
| Der eingebettete Font wird auf die Glyphen subsettet, die das Dokument referenziert. | ISO 32000-2 | §9 | |
Ein eingebetteter CJK-TrueType-Schnitt wird als Type-0-Font mit einer Identity-H-CMap und einem CIDFontType2-Nachfahren ausgegeben. | ISO 32000-2 | §9.7.4 | RAG-Digest durch Lizenz-Cap abgeschnitten; Präfix 7a5258772f508e3b, siehe _downgraded-claims-o3.md |
Die ersten beiden Klauseln sind paraphrasiert und per Digest gepinnt. Der vollständige RAG-Digest der dritten Klausel wurde nicht zurückgegeben (Abschneiden durch das Lizenz-Cap); sie wird durch ADR-013 und den Entwicklerüberblick zum cmap-Encoder bestätigt und als herabgestuft festgehalten. NextPDF gibt keinen normativen Text wieder. Die PDF/A-4- und PDF/UA-2-Konformität für CJK-Inhalte hängt am Writer-seitigen Subsetting und der dort verfolgten /ToUnicode-Verdrahtung.
Kommerzieller Kontext
Abschnitt betitelt „Kommerzieller Kontext“Ein kommerzielles OpenType-Feature-Pack und Premium-Font-Fallback-Ketten bauen auf der Core-Registry und dem Encoding-Seam auf. Das Core-Typografie-Modul bettet jeden Font ein, subsettet ihn und kodiert ihn ohne Lizenz; das kostenpflichtige Pack ergänzt eine kuratierte Fallback-Auflösung. Das Weglassen eines Conversion-Links ist beabsichtigt — dies ist Dokumentation, kein Verkaufspfad.
Siehe auch
Abschnitt betitelt „Siehe auch“- Font: TrueType, OpenType und CID-Registry — Font-Werttypen, Einbettung und Fallback.
- Text: Shaping, Umbruch, BiDi — der Seam für Run-Handling und Shaping, der kodierte Glyphen konsumiert.
- Contracts / Typography — die
FontRegistryInterface- und Textpräprozessor-Contracts. - HTML-Rendering-Engine — die
@font-face-Brücke, dieregisterFromBinary()aufruft.