Skip to content

Add public glyph-run output API#4

Open
wieslawsoltes wants to merge 9 commits into
mainfrom
feature/pretext-glyph-output-api
Open

Add public glyph-run output API#4
wieslawsoltes wants to merge 9 commits into
mainfrom
feature/pretext-glyph-output-api

Conversation

@wieslawsoltes
Copy link
Copy Markdown
Owner

@wieslawsoltes wieslawsoltes commented May 2, 2026

PR Summary: Add Pretext Glyph Output API

Title

Add public glyph-run output API for Pretext shaping-capable backends

Overview

This PR extends PretextSharp beyond measurement-only layout by adding a public glyph-output API that renderers can use to build positioned glyph runs or text blobs directly. The existing Prepare, PrepareWithSegments, layout, rich-inline, and measurement APIs remain unchanged.

The new API exposes glyph ids, clusters, advances, offsets, absolute glyph positions, aggregate advance, and font-run metadata. Backends report whether output is fully platform-shaped or only mapped glyph output, so consumers can make correctness-sensitive decisions for complex scripts.

Why

Previously, Pretext backends could measure text and, in some cases, internally use shaping engines, but that shaped data was not available to callers. Consumers such as custom Skia renderers still had to draw materialized text fragments with string or grapheme APIs.

This PR makes the backend glyph data available through Pretext itself:

  • text layout users can keep using Pretext as before;
  • rendering users can call PretextLayout.ShapeText(...);
  • repeated wrapping/rendering users can prepare once with ShapePreparedText(...) and cache shaped line output by layout cursor range;
  • complex renderers can require PretextGlyphRunKind.Shaped;
  • simple renderers can opt into PretextGlyphRunKind.Mapped when raw glyph mapping is sufficient.

Main Changes

Public Contracts

Adds src/Pretext.Contracts/TextShapingContracts.cs with:

  • PretextGlyphRunKind
  • PretextTextDirection
  • PretextShapeOptions
  • PretextShapedGlyph
  • PretextShapedFontRun
  • PretextShapedRun
  • IPretextTextShaper
  • IPretextTextShaperFactory
  • PretextTextShaperFactoryAttribute

PretextGlyphRunKind.Shaped means backend-shaped glyph positions are returned. PretextGlyphRunKind.Mapped means glyph ids/positions are exposed, but the output should not be treated as full complex-script shaping.

Core API

Adds:

PretextLayout.ShapeText(string text, string font, PretextShapeOptions? options = null)
PretextLayout.TryShapeText(string text, string font, out PretextShapedRun? shapedRun, PretextShapeOptions? options = null)
PretextLayout.SetTextShaperFactory(IPretextTextShaperFactory? textShaperFactory)
PretextLayout.ShapePreparedText(PreparedTextWithSegments prepared, PretextShapeOptions? options = null)
PretextLayout.LayoutNextShapedLine(PreparedShapedText prepared, LayoutCursor start, double maxWidth)
PretextLayout.MaterializeShapedLineRange(PreparedShapedText prepared, LayoutLineRange line)
PretextLayout.MaterializeShapedLineRange(PreparedTextWithSegments prepared, LayoutLineRange line, PretextShapeOptions? options = null)
PretextLayout.MaterializeShapedRichInlineLineRange(PreparedRichInline prepared, RichInlineLineRange line, PretextShapeOptions? options = null)

The new shaper cache is cleared with the existing ClearCache() path. Backend discovery mirrors measurer discovery through PretextTextShaperFactoryAttribute.

The prepared shaping path adds a prepared-object cache keyed by shaping direction, plus a line-run cache keyed by LayoutCursor start/end ranges. Whole-segment ranges are sliced from the full prepared shaped run. Unsafe intra-segment ranges, soft-hyphen materializations, and ranges that start or end at invisible break boundaries are shaped from the exact line text once, then reused from the range cache.

Backend Implementations

CoreText:

  • Implements IPretextTextShaperFactory.
  • Uses CTLineCreateWithAttributedString, CTLineGetGlyphRuns, CTRunGetGlyphs, CTRunGetPositions, CTRunGetAdvances, and CTRunGetStringIndices.
  • Returns PretextGlyphRunKind.Shaped.
  • Reports font runs using CoreText run font identity where available.
  • Falls back to the CoreText run string range when per-glyph string indices are unavailable or invalid.
  • Applies explicit PretextShapeOptions.Direction values through cached CoreText paragraph styles with base writing direction set to left-to-right or right-to-left. Auto keeps the native CoreText default.

FreeType:

  • Implements IPretextTextShaperFactory.
  • Exposes native HarfBuzz glyph ids, clusters, advances, offsets, and positions from the existing FreeType/HarfBuzz path.
  • Adds explicit direction support via PretextShapeOptions.Direction.
  • Returns PretextGlyphRunKind.Shaped when HarfBuzz returns a complete primary-face shaped run.
  • Falls back to PretextGlyphRunKind.Mapped with FreeType/fontconfig fallback font runs when HarfBuzz is unavailable or the shaped run contains missing glyphs.

SkiaSharp:

  • Implements IPretextTextShaperFactory.
  • Returns mapped glyph ids and positions from SkiaSharp.
  • Marks output as PretextGlyphRunKind.Mapped because this path is useful for simple glyph rendering but is not a full complex-script shaper.
  • Reports glyph clusters as UTF-16 source text indices, including inputs with surrogate pairs.

DirectWrite:

  • Remains measurement-only in this PR.
  • The current DirectWrite backend does not yet contain glyph-run extraction plumbing, so shaping support is intentionally not claimed there.

Tests

Adds PretextShapingTests covering:

  • ShapeText using a configured shaping-capable measurer factory.
  • ShapeText cache reuse across repeated calls and cache invalidation via ClearCache.
  • ShapeText forwarding explicit direction options to the configured shaper.
  • SkiaSharp mapped glyph output preserving UTF-16 cluster indices after surrogate pairs.
  • ShapePreparedText prepared-object reuse and shaped line-run cache reuse.
  • Safe full-range slicing from prepared shaped output.
  • Unsafe intra-segment reshaping with cache reuse for repeated line materialization.
  • Exact-line reshaping at invisible break boundaries so hard breaks and zero-width break opportunities cannot accidentally reuse ligatures shaped across omitted break markers.
  • TryShapeText failure behavior when no usable shaper exists.
  • Explicit shaper factory preservation when a measurer is configured later.
  • PretextShapeOptions.Default returning fresh non-shared mutable options.
  • Auto-discovery of a shaping-capable backend for the current OS.

Updates existing test cleanup to reset both measurer and shaper global factory state.

Documentation and Packaging

Updates README and package metadata to describe:

  • glyph-output support;
  • ShapeText and TryShapeText;
  • prepared shaping and shaped line materialization;
  • safe prepared-run slicing versus exact-line fallback shaping;
  • shaped versus mapped output;
  • backend behavior for CoreText, FreeType, and SkiaSharp;
  • FreeType mapped fallback behavior.

Validation

Run locally:

dotnet build PretextSamples.slnx
dotnet test PretextSamples.slnx --no-build
dotnet test tests/Pretext.Uno.Tests/Pretext.Uno.Tests.csproj --no-build
git diff --check

Results:

  • Build succeeded with 0 warnings and 0 errors.
  • Tests passed: 102 passed, 0 failed, 0 skipped.
  • git diff --check passed.

Commit Breakdown

  1. Add public text shaping API

    • Adds shaping contracts.
    • Adds PretextLayout.ShapeText, TryShapeText, and shaper factory discovery/cache integration.
  2. Implement glyph output backends

    • Adds CoreText shaped glyph-run extraction.
    • Adds FreeType/HarfBuzz shaped glyph output and mapped fallback.
    • Adds SkiaSharp mapped glyph output.
  3. Cover glyph output API behavior

    • Adds focused shaping tests.
    • Updates test helpers and cleanup around global factory state.
  4. Document glyph output support

    • Updates README and package metadata.
  5. Cache prepared shaped line output

    • Adds prepared shaped text and shaped line materialization APIs.
    • Reuses shaped line runs by layout cursor range.
    • Slices safe full-segment line ranges from prepared shaped output.
    • Falls back to exact-line shaping for unsafe intra-segment, soft-hyphen, and invisible-break-boundary ranges.
  6. Fix shaped line break-boundary correctness

    • Prevents prepared-run slicing across invisible break boundaries.
    • Adds a hard-break ligature regression test.
    • Avoids an unnecessary discarded empty run allocation on fallback line shaping.
    • Hardens CoreText cluster extraction against invalid string indices.
  7. Preserve sliced shaped-run advances

    • Preserves negative AdvanceX and AdvanceY values when slicing shaped runs.
    • Keeps per-font-run glyph counts while building the slice to avoid an extra counting pass.
    • Adds a regression test for RTL-style negative advances.
  8. Honor CoreText shaping direction

    • Passes PretextShapeOptions into the CoreText shaping runtime.
    • Uses cached CoreText paragraph styles to set explicit LTR/RTL base writing direction without adding per-call paragraph-style creation overhead.
    • Adds a regression test that verifies explicit direction options reach the configured shaping backend.
  9. Use UTF-16 clusters for Skia glyph output

    • Computes Skia mapped glyph clusters from source UTF-16 text indices instead of glyph ordinals.
    • Keeps the ASCII/simple path allocation-free when glyph count equals text length.
    • Adds a surrogate-pair regression test for SkiaSharp mapped glyph output.

Compatibility Notes

This is additive API surface. Existing measurement and layout callers should not need code changes.

Consumers that need full complex-script correctness should check:

if (shaped.Kind == PretextGlyphRunKind.Shaped && !shaped.HasMissingGlyphs)
{
    // Safe to consume as shaped backend output.
}

Consumers that can render simple glyph ids directly may also accept PretextGlyphRunKind.Mapped.

Follow-Up Work

  • Add DirectWrite glyph-run extraction.
  • Add OpenType feature, script, and language options to PretextShapeOptions.
  • Add parity tests against HarfBuzzSharp for ligatures, combining marks, Arabic, Indic scripts, emoji ZWJ sequences, variation selectors, and fallback font runs.
  • Add allocation/performance benchmarks for ShapeText on warm and cold paths.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1618e4129

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Pretext/PretextLayout.Shaping.cs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f3bc3a2e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Pretext.CoreText/CoreTextTextMeasurerFactory.cs
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 943480331c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Pretext.SkiaSharp/SkiaSharpTextMeasurerFactory.cs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad86176eab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +114 to +118
catch (InvalidOperationException)
{
shapedRun = null;
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict TryShapeText fallback to unavailable-backend failures

TryShapeText currently swallows every InvalidOperationException, but this exception type is also used by backend/runtime code for real shaping failures (for example failed font initialization or shaper-specific runtime faults). In those cases the method returns false as if shaping were simply unavailable, which hides actionable errors and can silently downgrade rendering behavior. Narrow this catch to the specific "no shaper configured" path (or a dedicated exception) so genuine backend failures still surface.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant