Atelier

Shaping

Shaping is the step between a string and a set of positioned glyphs: which glyph each character maps to, and where each one sits. This package ships a deliberately small implementation of it, and a seam where a real one goes.

What the built-in mapper does

One direct cmap lookup per Unicode codepoint, producing a left-to-right run. Each glyph gets its advance from the font's metrics, plus whatever letterSpacing and wordSpacing you asked for.

That is the whole algorithm. It is enough for a headline, a chart label, a watermark, or a social card in a Latin script, which is what the package was built for.

What it does not do

Missing What it would need
font fallback a font stack and a policy for choosing within it
bidirectional text the Unicode bidirectional algorithm
ligatures reading GSUB from the font
kerning reading GPOS or kern
script shaping per-script rules for Arabic, Devanagari, and others

None of these is a bug to be fixed later in this class. Each is a body of work that HarfBuzz exists to do, and pretending otherwise would produce text that looks right in English and wrong everywhere else.

A codepoint the font has no glyph for throws MissingGlyphException rather than falling back, because falling back means choosing another font, and this package has no opinion about which.

The seam

Shaping\TextShaperInterface is one method:

interface TextShaperInterface
{
    public function shape(string $text): GlyphRun;
}

Everything downstream consumes the GlyphRun it returns. The outliner, the generators, and the document builder never see the string, so replacing the mapper replaces the shaping and nothing else.

What a shaper returns

GlyphRun holds an array of PositionedGlyph and a TextDirection:

final class GlyphRun
{
    public array $glyphs;            // list<PositionedGlyph>
    public TextDirection $direction; // LeftToRight or RightToLeft
}

Each glyph carries where it came from and where it goes:

Property Type What it is
$glyphId Alto\Font\Glyph\GlyphId the glyph in the font, not the character
$metrics Alto\Font\Glyph\GlyphMetrics the font's own advance and bearings
$cluster int which part of the source string produced it
$xAdvance, $yAdvance float how far the pen moves after drawing
$xOffset, $yOffset float how far this glyph shifts from the pen
$codepoint ?int the source codepoint, or null when several produced one glyph

$cluster and the nullable $codepoint are what make a real shaper possible: a ligature is one glyph from two characters, so it has one cluster and no single codepoint. The built-in mapper never produces that case, but the contract already allows it.

TextDirection::RightToLeft exists in the enum for the same reason. Nothing in the package emits it yet.

Replacing the mapper

Implement the interface, and hand your run to the rest of the pipeline:

use Atelier\Text\Shaping\GlyphRun;
use Atelier\Text\Shaping\TextShaperInterface;

final class HarfBuzzShaper implements TextShaperInterface
{
    public function shape(string $text): GlyphRun
    {
        // Call out to a shaping engine, then build PositionedGlyph values
        // from what it returns.
    }
}

The outline side has the same shape: Outline\OutlinerInterface takes a FontText and returns a TextPath, so a different way of turning glyphs into contours plugs in the same way.

Next: Errors.