Skip to content
getnextpdf.com

stability: Experimental

Complex-script shaping support

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).

Terminal window
composer require nextpdf/core:^3

The 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.

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.

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 .notdef would 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 is validated objectively against HarfBuzz: the emitted glyph identifiers, cluster mapping, and glyph positions match the HarfBuzz reference output (glyph, cluster, and position parity).

SymbolLocationRole
CssFeatureFlags::$complexTextShapingsrc/Html/CssFeatureFlags.phpOpt-in flag for the Tibetan/Mongolian shaper (default false).
Config::withCssFeatureFlags(CssFeatureFlags $flags): selfsrc/Core/Config.phpAttaches the flag set to a document configuration.
ComplexScriptShapingExceptionsrc/Font/Shaper/ComplexScriptShapingException.phpThrown when an in-scope run cannot be shaped faithfully.
HarfBuzzUnavailableExceptionsrc/Font/Shaper/HarfBuzzUnavailableException.phpThrown when the flag is on but libharfbuzz is unavailable.
<?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');

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);
  • 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.

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.

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.

StatementSpecClause
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 SpecificationGSUB / GPOS
Cluster formation follows the script properties for Tibetan and Mongolian.Unicode Standard AnnexTibetan and Mongolian

This is a preview implementation scoped to Tibetan and Mongolian, validated for objective HarfBuzz parity. No standards text is reproduced.