[WIP] [Text] GlyphTypeface.GetGlyphOutline implementation#21406
Open
Gillibald wants to merge 2 commits into
Open
[WIP] [Text] GlyphTypeface.GetGlyphOutline implementation#21406Gillibald wants to merge 2 commits into
Gillibald wants to merge 2 commits into
Conversation
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.
3 tasks
Contributor
There was a problem hiding this comment.
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 theglyftable 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_VALUESis 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) | ||
| { |
|
You can test this PR using the following package version. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does the pull request do?
Adds a public API on
GlyphTypefaceto retrieve a glyph's vector outline as an AvaloniaGeometry, parsed directly from the font'sglyfandlocatables: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 ofglyphId, withtransformapplied (callers typically passMatrix.CreateScale(1, -1)to flip from font-space Y-up to drawing Y-down).Returns
nullwhen:glyftable (e.g. CFF/CFF2 — out of scope for this PR), orThe
FontVariationSettings? variationparameter 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 thelocatable in both the short (uint16offsets, ×2) and long (uint32offsets) variants, selected based on thehead.indexToLocFormatflag. ProvidesTryGetGlyphRange(glyphIndex, out offset, out length).Glyf/GlyfTable— parses theglyftable on demand usingLocaTableranges.TryBuildGlyphGeometry(glyphIndex, transform, IGeometryContext)is the entry point used byGetGlyphOutline; it handles both simple and composite glyphs.Glyf/SimpleGlyph— endpoint counts, instruction bytes, flag run, x/y coordinate decoding (delta-encoded withSAME_*/*_SHORT_VECTORflag 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, andOVERLAP_COMPOUND/USE_MY_METRICSflag handling.Glyf/GlyphDecycler— pooled (viaObjectPool<T>)Decycler<int>instance that guards composite-glyph recursion against cycles and a depth limit of 64.GlyphTypefacechanges:_glyfTableonce in the constructor whenheadis present (so the cost is paid up front, not per outline call)._renderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>()to avoid the locator lookup on everyGetGlyphOutlinecall.GetGlyphOutlineopens a stream geometry via the render interface, runsGlyfTable.TryBuildGlyphGeometryinto the returnedIGeometryContext, and wraps the result in aPlatformGeometry.Checklist
Breaking changes
None. New public API; existing behavior is unchanged.
Obsoletions / Deprecations
None.
Fixed issues
Depends on: #21405