Zum Inhalt springen

Text: Shaping-Naht, CJK und Run-Handling

Das Textmodul bildet die Shaping-Naht: eine schlanke Schnittstelle, die einen UTF-8-Run in positionierte Glyphen umwandelt, ein echtes OpenType-Backend nutzt, sobald eines verfügbar ist, ein deterministisches Fallback bereitstellt, wenn keines vorhanden ist, und eine Registry für Script-spezifische Shaper anbietet.

Terminal-Fenster
composer require nextpdf/core:^3

ShaperInterface ist die Naht zwischen der Text-Layout-Pipeline und einer OpenType-Shaping-Engine. Die Schnittstelle ist bewusst minimal gehalten: eine einzige shape()-Methode, die ein ShaperInput entgegennimmt und ein ShapingResult zurückgibt. Der Rückgabetyp ist die einzige für Konsumenten sichtbare Ausgabe — Implementierungen dürfen keine Interna der Shaping-Engine nach außen tragen, und der typisierte Rückgabewert erzwingt das strukturell. ShapingResult enthält die Liste der GlyphRun-Datensätze, den zurückgespiegelten Quelltext, das Script und die Richtung sowie ein shaperImpl-Tag, das angibt, welches Backend das Ergebnis erzeugt hat.

Die Backend-Auswahl ist explizit und bildet den tatsächlichen Funktionsumfang ehrlich ab. ShaperFactory führt einmalig eine Capability-Probe aus: Hat der Host eine funktionierende HarfBuzz-Bindung, liefert create() den HarfBuzz-gestützten Shaper. Andernfalls liefert die Factory NullShaper. NullShaper ist ein durchreichender Fallback-Shaper. Für jeden Unicode-Codepoint gibt er genau eine synthetische Glyphe mit Advances von null und Offsets von null aus. Er markiert das Ergebnis so, dass die Observability das Fallback erkennt. Die Auflösung der Advances überlässt er dem Font-Metrics-Modul. Das ist ein dokumentierter degradierter Pfad, kein vollständiges Shaping: Substitution, Ligaturen, Mark-Positioning und kontextuelle Formen benötigen das echte Backend. wouldUseRealShaper() ist ein diagnostisches Prädikat. Produktionscode sollte stattdessen auf dem shaperImpl-Tag des Ergebnisses verzweigen.

Script-spezifisches Shaping ist ein SPI, keine gebündelte Implementierung. ScriptShaperRegistry ist eine Registry im PSR-11-Stil, die anhand des ISO 15924-Script-Tags ein MongolianShaperInterface oder TibetanShaperInterface auflöst. Die Registry speichert Schlüssel ohne Berücksichtigung der Groß-/Kleinschreibung und überlässt die Zulässigkeitsprüfung von Script-Codes einer einzigen verlässlichen Quelle. Die Registry und die Script-Shaper-Interfaces sind ein eingefrorener Vertrag, damit eine Erweiterung einen Phase-12-Provider registrieren kann, ohne die Aufrufstellen ändern zu müssen. Die Engine stellt die Naht bereit, Provider für komplexe Schriften werden vom Konsumenten beigesteuert.

Das CJK-Run-Handling liegt an der Encoding-Naht der Typografie. Eine eingebettete CJK-TrueType-Schrift wird als Type-0-Schrift mit einer Identity-H-CMap und einem CIDFontType2-Nachfahren ausgegeben — ISO 32000-2 §9.7.4 (RAG-Digest durch das Lizenz-Cap abgeschnitten; festgehalten in _downgraded-claims-o3.md). Wenn das TrueType-Programm eingebettet ist, bildet der Type-2-CIDFont Character-Identifier über den CIDToGIDMap-Eintrag auf Glyph-Indizes ab — ISO 32000-2 §9 (der Digest ist durch die B1-Vertragsseite gepinnt). Der Subsetter bewahrt die ursprüngliche Glyph-Nummerierung präzise, sodass ein /CIDToGIDMap /Identity für das Subset gültig bleibt. CjkFontValidator ist das Diagnosewerkzeug, das prüft, ob eine Kandidatenschrift die Unicode-Blöcke abdeckt, die ein Script benötigt, bevor diese Schrift gewählt wird.

TypArtWichtige MemberStabilitätSeit
ShaperInterfaceInterfaceshape(ShaperInput): ShapingResultstabil3.2.0
ShaperFactoryfinal classdefault(), create(), wouldUseRealShaper()stabil3.2.0
NullShaperfinal readonly classdurchreichender Fallback-Shaperstabil3.2.0
ShapingResultfinal readonly class$glyphRuns, $originalText, $script, $direction, $shaperImplstabil3.2.0
ScriptShaperRegistryfinal classregisterMongolian(), getMongolian(), hasMongolian(), und die tibetischen Entsprechungenstabil3.1.0
CjkFontValidatorfinal classvalidateCoverage(), detectScript(), isCjkCodepoint()stabil1.0.0

Die register*-, get*- und has*-Formen von ScriptShaperRegistry und den Script-Shaper-Interfaces sind ein eingefrorener Vertrag. ShapingResult ist designbedingt die einzige für Konsumenten sichtbare Shaper-Ausgabe.

examples/text/shaper-factory.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Font\Shaper\ShaperFactory;
use NextPDF\Font\Shaper\ShaperImpl;
$factory = ShaperFactory::default();
$shaper = $factory->create();
// Branch on the result tag, not on the concrete class.
$wouldShape = $factory->wouldUseRealShaper()
? 'HarfBuzz backend available'
: 'NullShaper fallback (degraded — no substitution or positioning)';
echo $wouldShape, "\n";

ShaperFactory::default() richtet die Capability-Probe für die Produktion ein. create() merkt sich das gewählte Backend für die Lebensdauer der Factory. Die verlässliche Aussage zum Funktionsumfang liefern wouldUseRealShaper() und das shaperImpl-Tag jedes Ergebnisses.

examples/text/script-shaper-registry.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Text\Shaping\MongolianShaperInterface;
use NextPDF\Text\Shaping\ScriptShaperRegistry;
final readonly class ComplexScriptBootstrap
{
public function __construct(private ScriptShaperRegistry $registry) {}
/**
* Register a consumer-supplied Mongolian shaper provider at boot so
* the layout pipeline can resolve it by ISO 15924 script tag.
*/
public function register(MongolianShaperInterface $mongolian): void
{
$this->registry->registerMongolian($mongolian);
}
public function hasMongolian(): bool
{
return $this->registry->hasMongolian();
}
}

Die Registry ist der Integrationspunkt für Provider komplexer Schriften. Die Engine stellt die Naht und die eingefrorene Accessor-Form bereit. Die mongolische und die tibetische Implementierung werden vom Konsumenten beigesteuert.

  • Ein NullShaper-Ergebnis hat Advances von null und Offsets von null. Speisen Sie diese Positionen nicht direkt in ein Text-Layout ein — lösen Sie die Advances über das Font-Metrics-Modul auf und erkennen Sie das Fallback über das shaperImpl-Tag.
  • Leere Eingabe erzeugt eine leere glyphRuns-Liste, keinen leeren Run. Der Iterationscode des Konsumenten braucht keinen Sonderfall für Runs der Länge null.
  • ScriptShaperRegistry implementiert Psr\Container\ContainerInterface nicht direkt, damit die typisierten Accessoren ihren eingeengten Rückgabetyp in der statischen Analyse behalten. Verwenden Sie getMongolian() und getTibetan(), nicht ein generisches get().
  • Script-Tags werden anhand des kanonischen ISO 15924-Alpha-4-Werts abgeglichen und ohne Berücksichtigung der Groß-/Kleinschreibung gespeichert. Übergeben Sie Mong oder Tibt. Die Groß-/Kleinschreibung spielt für die Suche keine Rolle.
  • Zeichen aus CJK Extension B liegen in Unicode-Plane 2 und erzwingen im Subset eine cmap-Subtabelle im Format 12. Der Encoding-Pfad behandelt dies. Gehen Sie nicht davon aus, dass die Basic Multilingual Plane bei CJK die ganze Geschichte ist.

Die Capability-Probe läuft einmal pro ShaperFactory-Instanz, und das Backend wird zwischengespeichert, sodass wiederholte create()-Aufrufe ohne zusätzlichen Aufwand bleiben. NullShaper ist linear zur Anzahl der Codepoints im Eingabe-Run und arbeitet ohne I/O. Die Auflösung in ScriptShaperRegistry ist ein schlüsselbasierter Lookup in konstanter Zeit. CjkFontValidator prüft Codepoints in einem Stride, statt jeden einzelnen zu testen; dadurch bleiben Coverage-Prüfungen selbst bei einer CJK-Schrift mit 20,000 Glyphen effizient. Das performance_budget von 1500 ms Wall und 64 MB Peak deckt einen typischen Run ab. Der dominante Kostenfaktor beim echten Shaping ist das OpenType-Backend selbst; wenn das Fallback aktiv ist, liegt es außerhalb des Prozess-Scopes.

Die Shaper-Naht verarbeitet einen UTF-8-String. NullShaper toleriert fehlerhaftes UTF-8 durch Best-Effort-Splitting, statt einen Fehler auszulösen, weil der dokumentierte Vertrag für das Fallback ohnehin „kein echtes Shaping“ lautet. Der Aufrufer ist auf eine Ausgabe geringerer Qualität vorbereitet. Der Vertrag für die Byte-Offset-Cluster verwendet eine byteorientierte Länge, was für mehrbyteige Eingaben korrekt ist und einen Off-by-Codepoint-Defekt beim Cluster-Mapping vermeidet. Das echte Backend ist, sofern vorhanden, eine native Drittanbieter-Bibliothek. Behandeln Sie dessen Eingabe als nicht vertrauenswürdig und begrenzen Sie die Run-Länge auf vorgelagerter Ebene. Die Script-Shaper-Registry speichert vom Konsumenten beigesteuerte Provider — die Vertrauensgrenze für diese Implementierungen liegt beim Konsumenten, nicht bei der Engine.

AussageStandardKlauselBeleg
Eine eingebettete CJK-TrueType-Schrift wird als Type-0-Schrift mit einer Identity-H-CMap und einem CIDFontType2-Nachfahren ausgegeben.ISO 32000-2§9.7.4RAG-Digest durch das Lizenz-Cap abgeschnitten; Präfix 7a5258772f508e3b, siehe _downgraded-claims-o3.md
Ein eingebetteter Type-2-CIDFont bildet Character-Identifier über CIDToGIDMap auf Glyph-Indizes ab.ISO 32000-2§9

Beide Klauseln sind paraphrasiert. Die zweite ist Digest-gepinnt (wiederverwendet von der B1-Vertragsseite), die erste ist durch ADR-013 und den Cmap-Encoder-Entwicklerüberblick bestätigt. NextPDF gibt keinen normativen Text wieder. Das Shaper-Backend ist unabhängig von der PDF-Konformität. Die Konformitätsaussagen hier betreffen die Emission des CJK-Font-Dictionarys durch die Encoding-Naht; sie ist in ADR-013 und dem Cmap-Encoder-Entwicklerüberblick weiter dokumentiert.

Eine fortgeschrittene Text-Preprocessing-Pipeline und Extraktionsdienste bauen auf der Core-Shaper-Naht und den Wertetypen für das Run-Handling auf. Das Core-Textmodul liefert die Naht, das Fallback und die Script-Shaper-Registry ohne Lizenz. Dass kein Konvertierungslink angegeben ist, ist beabsichtigt.