Skip to content

[WIP] [Text] GlyphTypeface.GetGlyphOutline implementation#21406

Open
Gillibald wants to merge 2 commits into
AvaloniaUI:masterfrom
Gillibald:pr2/glyph-outlines
Open

[WIP] [Text] GlyphTypeface.GetGlyphOutline implementation#21406
Gillibald wants to merge 2 commits into
AvaloniaUI:masterfrom
Gillibald:pr2/glyph-outlines

Conversation

@Gillibald
Copy link
Copy Markdown
Contributor

@Gillibald Gillibald commented May 21, 2026

What does the pull request do?

Adds a public API on GlyphTypeface to retrieve a glyph's vector outline as an Avalonia Geometry, parsed directly from the font's glyf and loca tables:

public Geometry? GetGlyphOutline(ushort glyphId,
                                 Matrix transform,
                                 FontVariationSettings? variation = null);

This is the foundation for higher-level features that need per-glyph geometry — custom path effects, exporting glyph paths, and (in the follow-up COLR PR) drawing the outline layers of color glyphs.

This PR is stacked on top of the font-parsing-infrastructure PR (ObjectPool<T>, Decycler<T>, FontVariationSettings, etc.). Please merge that one first.

What is the updated/expected behavior with this PR?

GlyphTypeface.GetGlyphOutline(glyphId, transform) returns the vector outline of glyphId, with transform applied (callers typically pass Matrix.CreateScale(1, -1) to flip from font-space Y-up to drawing Y-down).

Returns null when:

  • the glyph ID is out of range, or
  • the font has no glyf table (e.g. CFF/CFF2 — out of scope for this PR), or
  • the glyph data cannot be parsed.

The FontVariationSettings? variation parameter is part of the signature for forward compatibility but is not applied yet — variable-font outline deformation will be wired up in a later change.

How was the solution implemented (if it's not obvious)?

The implementation lives under src/Avalonia.Base/Media/Fonts/Tables/:

  • LocaTable — parses the loca table in both the short (uint16 offsets, ×2) and long (uint32 offsets) variants, selected based on the head.indexToLocFormat flag. Provides TryGetGlyphRange(glyphIndex, out offset, out length).
  • Glyf/GlyfTable — parses the glyf table on demand using LocaTable ranges. TryBuildGlyphGeometry(glyphIndex, transform, IGeometryContext) is the entry point used by GetGlyphOutline; it handles both simple and composite glyphs.
  • Glyf/SimpleGlyph — endpoint counts, instruction bytes, flag run, x/y coordinate decoding (delta-encoded with SAME_* / *_SHORT_VECTOR flag combinations), and contour walking with implied on-curve points between consecutive off-curves.
  • Glyf/CompositeGlyph — recursive component traversal with anchor-point or x/y-offset placement, optional 2×2 transform, and OVERLAP_COMPOUND / USE_MY_METRICS flag handling.
  • Glyf/GlyphDecycler — pooled (via ObjectPool<T>) Decycler<int> instance that guards composite-glyph recursion against cycles and a depth limit of 64.

GlyphTypeface changes:

  • Loads _glyfTable once in the constructor when head is present (so the cost is paid up front, not per outline call).
  • Caches _renderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>() to avoid the locator lookup on every GetGlyphOutline call.
  • GetGlyphOutline opens a stream geometry via the render interface, runs GlyfTable.TryBuildGlyphGeometry into the returned IGeometryContext, and wraps the result in a PlatformGeometry.

Checklist

Breaking changes

None. New public API; existing behavior is unchanged.

Obsoletions / Deprecations

None.

Fixed issues

Depends on: #21405

Gillibald added 2 commits May 21, 2026 10:38
Introduces shared utility types that subsequent PRs will use to
implement GetGlyphOutline (glyf table) and GetGlyphDrawing
(COLR v0/v1) on GlyphTypeface:

- ObjectPool<T>: thread-safe object pool used by Decyclers
- Decycler<T> / CycleGuard<T> / DecyclerException: generic
  cycle-detection and depth-limiting utility for recursive
  font-table traversal (composite glyphs, paint graphs)
- FontVariationSettings: parameter type for the upcoming
  GetGlyphOutline / GetGlyphDrawing overloads
- IGlyphDrawing / GlyphDrawingType: return-type contract for color
  glyph drawings (outline, color layers, SVG, bitmap)
- CharacterToGlyphMapDictionary: lightweight, allocation-free
  IReadOnlyDictionary<int, ushort> view over the cmap, plus
  CharacterToGlyphMap.AsReadOnlyDictionary() to expose it

No behavior change; types are not yet consumed in this PR.
Implements vector outline retrieval for TrueType glyphs:

- LocaTable: parses the 'loca' table (short/long offset variants)
  to map glyph IDs to glyf offsets
- GlyfTable: parses the 'glyf' table, supporting simple and
  composite glyphs, and builds geometry into an IGeometryContext
- SimpleGlyph / CompositeGlyph / GlyphDescriptor / GlyphComponent /
  GlyphFlag / CompositeFlags: data model for the parsed glyph
  records
- GlyphDecycler: pooled cycle/depth guard for composite-glyph
  recursion (derives from the shared Decycler<int>)

GlyphTypeface now eagerly loads the glyf table when a head table is
present and exposes:

  Geometry? GetGlyphOutline(ushort glyphId, Matrix transform,
                            FontVariationSettings? variation = null)

Returns null when the font has no outline data for the glyph or the
glyph index is out of range.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds foundational font-table parsing support for extracting per-glyph vector outlines and exposes it via a new GlyphTypeface.GetGlyphOutline(...) public API, enabling future features like glyph path export and COLR outline layer rendering.

Changes:

  • Add GlyphTypeface.GetGlyphOutline(...) and cache the glyf table for outline extraction.
  • Implement on-demand parsing of TrueType loca/glyf (simple + composite glyphs) with recursion cycle protection.
  • Add supporting utilities/types (object pooling, decycler, cmap dictionary view, variation settings scaffolding).

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/Avalonia.Base/Utilities/ObjectPool.cs Adds a thread-safe object pool used for reusing traversal helpers.
src/Avalonia.Base/Media/IGlyphDrawing.cs Introduces a glyph drawing abstraction (currently has build-breaking unused usings).
src/Avalonia.Base/Media/GlyphTypeface.cs Adds GetGlyphOutline API and caches glyf parsing/render interface access.
src/Avalonia.Base/Media/GlyphDrawingType.cs Adds an enum describing glyph render formats (outline/COLR/SVG/bitmap).
src/Avalonia.Base/Media/FontVariationSettings.cs Adds a record for future variation/color/bitmap selection parameters.
src/Avalonia.Base/Media/Fonts/Tables/LocaTable.cs Adds loca parsing for glyph-to-glyf offset lookup (has unused using).
src/Avalonia.Base/Media/Fonts/Tables/Glyf/SimpleGlyph.cs Implements simple-glyph decoding (flags + delta coords) (has unused using + minor comment mismatch).
src/Avalonia.Base/Media/Fonts/Tables/Glyf/GlyphFlag.cs Defines TrueType simple glyph point flags.
src/Avalonia.Base/Media/Fonts/Tables/Glyf/GlyphDescriptor.cs Parses glyph headers and dispatches to simple/composite readers (has unused usings).
src/Avalonia.Base/Media/Fonts/Tables/Glyf/GlyphDecycler.cs Adds pooled cycle-detection for composite glyph recursion.
src/Avalonia.Base/Media/Fonts/Tables/Glyf/GlyphComponent.cs Defines composite component metadata (flags, args, transform).
src/Avalonia.Base/Media/Fonts/Tables/Glyf/GlyfTable.cs Core glyf reader + geometry builder (has unused usings; composite anchor attachment currently unimplemented; debug logging in hot path).
src/Avalonia.Base/Media/Fonts/Tables/Glyf/CompositeGlyph.cs Implements composite glyph component parsing (has unused using).
src/Avalonia.Base/Media/Fonts/Tables/Glyf/CompositeFlags.cs Defines composite glyph flags.
src/Avalonia.Base/Media/Fonts/Tables/Decycler.cs Adds generic cycle/depth guard used by glyph traversal.
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMapDictionary.cs Adds a read-only dictionary wrapper for cmap mappings.
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs Exposes cmap mappings via AsReadOnlyDictionary().
Comments suppressed due to low confidence (1)

src/Avalonia.Base/Media/Fonts/Tables/Glyf/GlyfTable.cs:353

  • Composite glyphs where ARGS_ARE_XY_VALUES is not set (i.e. Arg1/Arg2 are point indices for anchor-point attachment) are currently not positioned: CreateComponentTransform leaves tx/ty as 0 in that case. This will render many composite glyphs incorrectly (accents/marks). Consider implementing point-based attachment translation per the TrueType spec (matching component point Arg2 to parent point Arg1).
            double tx = 0, ty = 0;

            if ((flags & CompositeFlags.ArgsAreXYValues) != 0)
            {
                tx = component.Arg1;
                ty = component.Arg2;
            }

Comment on lines +562 to +567
public Geometry? GetGlyphOutline(ushort glyphId, Matrix transform, FontVariationSettings? variation = null)
{
if (glyphId > GlyphCount)
{
return null;
}
Comment on lines +549 to +563
/// Retrieves the vector outline geometry for the specified glyph, optionally applying a transformation and font
/// variation settings.
/// </summary>
/// <remarks>The returned geometry reflects any transformation and variation settings provided. If
/// the font does not contain outline data for the specified glyph, or if the glyph identifier is out of range,
/// the method returns null.</remarks>
/// <param name="glyphId">The identifier of the glyph to retrieve. Must be less than or equal to the total number of glyphs in the
/// font.</param>
/// <param name="transform">A transformation matrix to apply to the glyph outline geometry.</param>
/// <param name="variation">Optional font variation settings to use when retrieving the glyph outline. If null, default font variations
/// are used.</param>
/// <returns>A Geometry object representing the outline of the specified glyph, or null if the glyph does not exist or
/// the outline cannot be retrieved.</returns>
public Geometry? GetGlyphOutline(ushort glyphId, Matrix transform, FontVariationSettings? variation = null)
{
var geometry = _renderInterface.CreateStreamGeometry();

using (var ctx = geometry.Open())
{
private static readonly IReadOnlyDictionary<CultureInfo, string> s_emptyStringDictionary =
new Dictionary<CultureInfo, string>(0);

private static readonly IPlatformRenderInterface _renderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
Comment on lines +1 to +5
using System;
using System.Collections.Generic;
using System.Text;

namespace Avalonia.Media
Comment on lines +2 to +9
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Diagnostics;
using Avalonia.Platform;
using Avalonia.Logging;
using Avalonia.Utilities;
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
Comment on lines +472 to +552
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked = true)
{
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log))
{
log.Log(_inner, "ArcTo {0} {1} rot={2} large={3} sweep={4}", point, size, rotationAngle, isLargeArc, sweepDirection);
}

var tp = _matrix.Transform(point);

_inner.ArcTo(tp, size, rotationAngle, isLargeArc, sweepDirection, isStroked);
}

public void BeginFigure(Point startPoint, bool isFilled = true)
{
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log))
{
log.Log(_inner, "BeginFigure {0} filled={1}", startPoint, isFilled);
}

var tp = _matrix.Transform(startPoint);

_inner.BeginFigure(tp, isFilled);
}

public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked = true)
{
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log))
{
log.Log(_inner, "CubicBezierTo cp1={0} cp2={1} end={2}", controlPoint1, controlPoint2, endPoint);
}

_inner.CubicBezierTo(_matrix.Transform(controlPoint1), _matrix.Transform(controlPoint2), _matrix.Transform(endPoint), isStroked);
}

public void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked = true)
{
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log))
{
log.Log(_inner, "QuadraticBezierTo cp={0} end={1}", controlPoint, endPoint);
}

_inner.QuadraticBezierTo(_matrix.Transform(controlPoint), _matrix.Transform(endPoint), isStroked);
}

public void LineTo(Point endPoint, bool isStroked = true)
{
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log))
{
log.Log(_inner, "LineTo {0}", endPoint);
}

_inner.LineTo(_matrix.Transform(endPoint), isStroked);
}

public void EndFigure(bool isClosed)
{
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log))
{
log.Log(_inner, "EndFigure closed={0}", isClosed);
}

_inner.EndFigure(isClosed);
}

public void SetFillRule(FillRule fillRule)
{
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log))
{
log.Log(_inner, "SetFillRule {0}", fillRule);
}

_inner.SetFillRule(fillRule);
}

public void Dispose()
{
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log))
{
log.Log(_inner, "Dispose TransformingGeometryContext");
}
}
Comment on lines +21 to +23
// Rented buffers for y-coordinates
private readonly short[]? _rentedXCoords;
// Rented buffers for x-coordinates
Comment on lines +562 to +565
public Geometry? GetGlyphOutline(ushort glyphId, Matrix transform, FontVariationSettings? variation = null)
{
if (glyphId > GlyphCount)
{
@avaloniaui-bot
Copy link
Copy Markdown

You can test this PR using the following package version. 12.1.999-cibuild0065665-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants