stability: Experimental
Complex-script shaping support
At a glance
Section titled “At a glance”Opt-in preview. Complex-script shaping is default-off. When it is off, the engine renders through the existing codepoint-to-cmap path — byte-identical to a build without the feature. Turn it on only when you have libharfbuzz and a shaping-capable font, and validate the result.
The HTML renderer adds an opt-in complex-script shaper for Tibetan and Mongolian. When the shaper is on, a detected in-scope Tibetan or Mongolian run is shaped through libharfbuzz and emitted as Identity-H glyph codes. The shaper covers horizontal Tibetan in TrueType and CFF/OTTO faces, including wrapping, and vertical Mongolian set top-to-bottom (TTB).
Install
Section titled “Install”composer require nextpdf/core:^3The shaper ships in the core package. The CssFeatureFlags::$complexTextShaping
opt-in is @since 6.1.0. libharfbuzz is a runtime requirement when the flag
is on — the shaper calls into libharfbuzz through PHP’s FFI extension. When the
flag is off, the library has no libharfbuzz dependency.
Conceptual overview
Section titled “Conceptual overview”Complex scripts reorder, substitute, and reposition glyphs by context. A naive codepoint-to-glyph mapping renders them visibly wrong. The shaper hands an in-scope run to libharfbuzz, which applies the font’s OpenType shaping tables, and the engine emits the resulting glyph sequence as a composite Type 0 font with Identity-H encoding (ISO 32000-2 §9.7.4 — the shown string is two-byte CIDs).
Scope is deliberate. The shaper recognizes Tibetan and Mongolian runs and shapes them; it does not claim general complex-script coverage. Horizontal Tibetan is shaped in TrueType and CFF/OTTO faces, with line wrapping. Mongolian is shaped vertically, top to bottom.
Fail-closed boundary — hard and typed
Section titled “Fail-closed boundary — hard and typed”The shaper never emits unshaped, visually broken glyphs as a fallback. A run that cannot be shaped faithfully raises a typed exception instead:
ComplexScriptShapingException— the run cannot be shaped faithfully: the font lacks the needed glyphs (a.notdefwould result), a CFF face is asked to shape in the vertical path, the run contains a link, or a Mongolian column needs wrapping (an out-of-scope case).HarfBuzzUnavailableException— the flag is on but libharfbuzz is not reachable through FFI at runtime.
With the flag off, an in-scope run renders through the existing codepoint-to-cmap path. That is a documented limitation, not a shaping claim: the off path does not apply OpenType shaping, so contextual forms are not guaranteed. Do not describe off-path output as “shaped”.
Shaping fidelity
Section titled “Shaping fidelity”Shaping fidelity is validated objectively against HarfBuzz: the emitted glyph identifiers, cluster mapping, and glyph positions match the HarfBuzz reference output (glyph, cluster, and position parity).
API surface
Section titled “API surface”| Symbol | Location | Role |
|---|---|---|
CssFeatureFlags::$complexTextShaping | src/Html/CssFeatureFlags.php | Opt-in flag for the Tibetan/Mongolian shaper (default false). |
Config::withCssFeatureFlags(CssFeatureFlags $flags): self | src/Core/Config.php | Attaches the flag set to a document configuration. |
ComplexScriptShapingException | src/Font/Shaper/ComplexScriptShapingException.php | Thrown when an in-scope run cannot be shaped faithfully. |
HarfBuzzUnavailableException | src/Font/Shaper/HarfBuzzUnavailableException.php | Thrown when the flag is on but libharfbuzz is unavailable. |
Code sample — Quick start
Section titled “Code sample — Quick start”<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;use NextPDF\Html\Css\CssFeatureFlags;
$config = (new Config())->withCssFeatureFlags( new CssFeatureFlags(complexTextShaping: true),);
$doc = Document::createStandalone($config);$doc->addPage();$doc->writeHtml( '<div style="font-family: NotoSerifTibetan;">བོད་སྐད་</div>',);$doc->save(__DIR__ . '/tibetan.pdf');Code sample — Production
Section titled “Code sample — Production”Register a shaping-capable font, opt into the shaper, and handle the two typed failure modes explicitly. A faithful render or a clear exception — never a silently broken glyph run.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\DocumentFactory;use NextPDF\Exception\ComplexScriptShapingException;use NextPDF\Exception\HarfBuzzUnavailableException;use NextPDF\Graphics\ImageRegistry;use NextPDF\Html\Css\CssFeatureFlags;use NextPDF\Typography\FontRegistry;
$fontRegistry = new FontRegistry();$fontRegistry->register('/path/to/NotoSerifTibetan-Regular.ttf', alias: 'NotoSerifTibetan');
$config = (new Config())->withCssFeatureFlags( new CssFeatureFlags(complexTextShaping: true),);
$factory = new DocumentFactory($fontRegistry, new ImageRegistry(maxCacheBytes: 0));$doc = $factory->create($config);$doc->setLanguage('bo');$doc->addPage();
try { $doc->writeHtml('<div style="font-family: NotoSerifTibetan;">བོད་སྐད་</div>');} catch (HarfBuzzUnavailableException $e) { // The flag is on but libharfbuzz is not reachable. Install it, or turn the // flag off to fall back to the unshaped cmap path. throw $e;} catch (ComplexScriptShapingException $e) { // The run cannot be shaped faithfully (missing glyphs, link in run, // out-of-scope case). Fix the font or the content; do not ship broken glyphs. throw $e;}
$doc->save($out);Edge cases & gotchas
Section titled “Edge cases & gotchas”- libharfbuzz is required when on. With the flag on and libharfbuzz absent,
the engine throws
HarfBuzzUnavailableException. It does not silently degrade. - Off is not “shaped”. With the flag off, an in-scope run renders through the cmap path without OpenType shaping. This is a documented limitation; do not call it shaped output.
- Scope is Tibetan and Mongolian. Other complex scripts are out of scope for this slice.
- A link in the run fails closed. A run that contains a link annotation
raises
ComplexScriptShapingException, because the link rectangle cannot follow shaped reordering.
Performance
Section titled “Performance”Shaping adds one libharfbuzz call per in-scope run, plus the glyph-emission pass,
linear in glyph count. The budget (wall_ms: 2000, peak_mb: 128) follows the
CJK/complex-script profile, because shaping fonts are large and font handling
dominates the cost.
Security notes
Section titled “Security notes”Enabling the shaper introduces an FFI call into libharfbuzz, a native library. Font files remain untrusted binary input handled by the typography layer’s existing validation before they reach the shaper. The shaper consumes already registered, already validated faces. Treat the provenance of end-user-supplied fonts as untrusted, and provision libharfbuzz from a trusted source.
Conformance
Section titled “Conformance”| Statement | Spec | Clause |
|---|---|---|
| Shaped runs are emitted as Identity-H two-byte CIDs in a composite Type 0 font. | ISO 32000-2 | §9.7.4 |
| The shaper applies the font’s OpenType glyph substitution and positioning. | OpenType Specification | GSUB / GPOS |
| Cluster formation follows the script properties for Tibetan and Mongolian. | Unicode Standard Annex | Tibetan and Mongolian |
This is a preview implementation scoped to Tibetan and Mongolian, validated for objective HarfBuzz parity. No standards text is reproduced.