Skip to content

Conversation

@Gillibald
Copy link
Contributor

@Gillibald Gillibald commented Oct 16, 2025

What does the pull request do?

This PR implements a universal GlyphTypeface implementation that abstracts platform-specific font handling through new interfaces (IPlatformTypeface, ITextShaperTypeface, IFontMemory). The changes consolidate glyph typeface functionality, moving from direct platform implementations to a unified architecture.

Key changes:

  • Make IGlyphTypeface NotClientImplementable
  • Introduced new interfaces to separate platform-specific font operations from the core IGlyphTypeface interface
  • Replaced direct method calls like GetGlyph() with a CharacterToGlyphMap dictionary lookup
  • Implemented OpenType table parsing classes for CMAP, metrics, and other font tables
  • Use real fonts for headless testing

What is the current behavior?

Currently, we rely on the platform glyph typeface implementation to get certain text metrics, etc. That makes it hard to get a consistent behavior.

What is the updated/expected behavior with this PR?

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

Checklist

Breaking changes

Obsoletions / Deprecations

Fixed issues

Fixes: #20190

@maxkatz6 maxkatz6 added enhancement breaking-change area-textprocessing needs-api-review The PR adds new public APIs that should be reviewed. labels Oct 18, 2025
@Gillibald
Copy link
Contributor Author

Gillibald commented Oct 22, 2025

API diff between 12.0.999-cibuild0060659-alpha and 12.0.999

Avalonia.Base (net10.0, net8.0)

  namespace Avalonia.Media
  {
      public sealed class GlyphMetrics
      {
- public int Height { get; init; }
+ public ushort Height { get; init; }
-         public int Width { get; init; }
+         public ushort Width { get; init; }
      }
      public interface IGlyphTypeface
      {
-         ushort GetGlyph(uint codepoint);
-         int GetGlyphAdvance(ushort glyph);
+         ushort GetGlyphAdvance(ushort glyph);
-         int[] GetGlyphAdvances(System.ReadOnlySpan<ushort> glyphs);
-         ushort[] GetGlyphs(System.ReadOnlySpan<uint> codepoints);
-         bool TryGetGlyph(uint codepoint, out ushort glyph);
-         bool TryGetTable(uint tag, out byte[] table);
+         System.Collections.Generic.IReadOnlyDictionary<int, ushort> CharacterToGlyphMap { get; }
+         System.Collections.Generic.IReadOnlyDictionary<System.Globalization.CultureInfo, string> FaceNames { get; }
+         System.Collections.Generic.IReadOnlyDictionary<System.Globalization.CultureInfo, string> FamilyNames { get; }
+         Avalonia.Media.IPlatformTypeface PlatformTypeface { get; }
+         System.Collections.Generic.IReadOnlyList<Avalonia.Media.Fonts.OpenTypeTag> SupportedFeatures { get; }
+         Avalonia.Media.ITextShaperTypeface TextShaperTypeface { get; }
+         string TypographicFamilyName { get; }
      }
+     public sealed class GlyphTypeface : Avalonia.Media.IGlyphTypeface
+     {
+         public GlyphTypeface(Avalonia.Media.IPlatformTypeface typeface, Avalonia.Media.FontSimulations fontSimulations = 0);
+         public void Dispose();
+         public ushort GetGlyphAdvance(ushort glyphId);
+         public bool TryGetGlyphMetrics(ushort glyph, out Avalonia.Media.GlyphMetrics metrics);
+         public System.Collections.Generic.IReadOnlyDictionary<int, ushort> CharacterToGlyphMap { get; }
+         public System.Collections.Generic.IReadOnlyDictionary<System.Globalization.CultureInfo, string> FaceNames { get; }
+         public string FamilyName { get; }
+         public System.Collections.Generic.IReadOnlyDictionary<System.Globalization.CultureInfo, string> FamilyNames { get; }
+         public Avalonia.Media.FontSimulations FontSimulations { get; }
+         public int GlyphCount { get; }
+         public Avalonia.Media.FontMetrics Metrics { get; }
+         public Avalonia.Media.IPlatformTypeface PlatformTypeface { get; }
+         public Avalonia.Media.FontStretch Stretch { get; }
+         public Avalonia.Media.FontStyle Style { get; }
+         public System.Collections.Generic.IReadOnlyList<Avalonia.Media.Fonts.OpenTypeTag> SupportedFeatures { get; }
+         public Avalonia.Media.ITextShaperTypeface TextShaperTypeface { get; }
+         public string TypographicFamilyName { get; }
+         public Avalonia.Media.FontWeight Weight { get; }
+     }
+     public interface IFontMemory
+     {
+         bool TryGetTable(Avalonia.Media.Fonts.OpenTypeTag tag, out System.ReadOnlyMemory<byte> table);
+     }
+     public interface IPlatformTypeface : Avalonia.Media.IFontMemory
+     {
+         bool? TryGetStream(out System.IO.Stream? stream);
+         string FamilyName { get; }
+         Avalonia.Media.FontSimulations FontSimulations { get; }
+         Avalonia.Media.FontStretch Stretch { get; }
+         Avalonia.Media.FontStyle Style { get; }
+         Avalonia.Media.FontWeight Weight { get; }
+     }
+     public interface ITextShaperTypeface
+     {
+     }
  }
  namespace Avalonia.Media.Fonts
  {
      public abstract class FontCollectionBase : Avalonia.Media.Fonts.IFontCollection
      {
+         public bool TryAddGlyphTypeface(Avalonia.Media.IGlyphTypeface glyphTypeface, Avalonia.Media.Fonts.FontCollectionKey key);
      }
      public interface IFontCollection
      {
+         bool TryCreateSyntheticGlyphTypeface(Avalonia.Media.IGlyphTypeface glyphTypeface, Avalonia.Media.FontStyle style, Avalonia.Media.FontWeight weight, Avalonia.Media.FontStretch stretch, out Avalonia.Media.IGlyphTypeface? syntheticGlyphTypeface);
+         bool TryGetFamilyTypefaces(string familyName, out System.Collections.Generic.IReadOnlyList<Avalonia.Media.Typeface?>? familyTypefaces);
+         bool TryGetNearestMatch(string familyName, Avalonia.Media.FontStyle style, Avalonia.Media.FontWeight weight, Avalonia.Media.FontStretch stretch, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
      }
+     public static class FontCollectionKeyExtensions
+     {
+         public static Avalonia.Media.Fonts.FontCollectionKey ToFontCollectionKey(this Avalonia.Media.IGlyphTypeface glyphTypeface);
+         public static Avalonia.Media.Fonts.FontCollectionKey ToFontCollectionKey(this Avalonia.Media.IPlatformTypeface platformTypeface);
+         public static Avalonia.Media.Fonts.FontCollectionKey ToFontCollectionKey(this Avalonia.Media.Typeface typeface);
+     }
+     public sealed class OpenTypeTag
+     {
+         public static readonly Avalonia.Media.Fonts.OpenTypeTag Max;
+         public static readonly Avalonia.Media.Fonts.OpenTypeTag MaxSigned;
+         public static readonly Avalonia.Media.Fonts.OpenTypeTag None;
+         public OpenTypeTag(char c1, char c2, char c3, char c4);
+         public OpenTypeTag(uint value);
+         public bool Equals(Avalonia.Media.Fonts.OpenTypeTag other);
+         public override bool Equals(object obj);
+         public override int GetHashCode();
+         public static bool operator ==(Avalonia.Media.Fonts.OpenTypeTag left, Avalonia.Media.Fonts.OpenTypeTag right);
+         public static implicit operator uint(Avalonia.Media.Fonts.OpenTypeTag tag);
+         public static implicit operator Avalonia.Media.Fonts.OpenTypeTag(uint tag);
+         public static bool operator !=(Avalonia.Media.Fonts.OpenTypeTag left, Avalonia.Media.Fonts.OpenTypeTag right);
+         public static Avalonia.Media.Fonts.OpenTypeTag Parse(string tag);
+         public override string ToString();
+     }
  }
  namespace Avalonia.Media.TextFormatting
  {
      public sealed class TextShaperOptions
      {
-         public Avalonia.Media.IGlyphTypeface Typeface { get; }
+         public Avalonia.Media.IGlyphTypeface GlyphTypeface { get; }
      }
  }
  namespace Avalonia.Platform
  {
      public interface IFontManagerImpl
      {
-         bool TryCreateGlyphTypeface(System.IO.Stream stream, Avalonia.Media.FontSimulations fontSimulations, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
-         bool TryCreateGlyphTypeface(string familyName, Avalonia.Media.FontStyle style, Avalonia.Media.FontWeight weight, Avalonia.Media.FontStretch stretch, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
-         bool? TryMatchCharacter(int? codepoint, Avalonia.Media.FontStyle? fontStyle, Avalonia.Media.FontWeight? fontWeight, Avalonia.Media.FontStretch? fontStretch, string? familyName, System.Globalization.CultureInfo? culture, out Avalonia.Media.Typeface? typeface);
+         bool TryCreateGlyphTypeface(System.IO.Stream stream, Avalonia.Media.FontSimulations fontSimulations, out Avalonia.Media.IPlatformTypeface? platformTypeface);
+         bool TryCreateGlyphTypeface(string familyName, Avalonia.Media.FontStyle style, Avalonia.Media.FontWeight weight, Avalonia.Media.FontStretch stretch, out Avalonia.Media.IPlatformTypeface? platformTypeface);
+         bool TryGetFamilyTypefaces(string familyName, out System.Collections.Generic.IReadOnlyList<Avalonia.Media.Typeface?>? familyTypefaces);
+         bool? TryMatchCharacter(int? codepoint, Avalonia.Media.FontStyle? fontStyle, Avalonia.Media.FontWeight? fontWeight, Avalonia.Media.FontStretch? fontStretch, string? familyName, System.Globalization.CultureInfo? culture, out Avalonia.Media.IPlatformTypeface? platformTypeface);
      }
      public interface IGlyphRunImpl
      {
-         Avalonia.Media.IGlyphTypeface GlyphTypeface { get; }
      }
      public interface ITextShaperImpl
      {
+         Avalonia.Media.ITextShaperTypeface CreateTypeface(Avalonia.Media.IGlyphTypeface glyphTypeface);
      }
  }

@Gillibald Gillibald requested a review from Copilot October 22, 2025 13:00
Copy link
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

This PR implements a universal GlyphTypeface implementation that abstracts platform-specific font handling through new interfaces (IPlatformTypeface, ITextShaperTypeface, IFontMemory). The changes consolidate glyph typeface functionality, moving from direct platform implementations to a unified architecture.

Key changes:

  • Introduced new interfaces to separate platform-specific font operations from the core IGlyphTypeface interface
  • Replaced direct method calls like GetGlyph() with a CharacterToGlyphMap dictionary lookup
  • Implemented OpenType table parsing classes for CMAP, metrics, and other font tables

Reviewed Changes

Copilot reviewed 68 out of 70 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Avalonia.Base/Media/IGlyphTypeface.cs Expanded interface with new properties and removed deprecated methods; added IPlatformTypeface, ITextShaperTypeface, IFontMemory interfaces
src/Avalonia.Base/Media/GlyphTypeface.cs New universal implementation using platform typeface abstraction and OpenType table parsing
src/Avalonia.Base/Media/Fonts/Tables/Cmap/*.cs Added CMAP table parsing for character-to-glyph mapping
src/Skia/Avalonia.Skia/SkiaTypeface.cs New platform-specific implementation for Skia
src/Windows/Avalonia.Direct2D1/Media/DWriteTypeface.cs New platform-specific implementation for DirectWrite
src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs Converted from stream-based to span-based for better performance
tests/* Updated to use CharacterToGlyphMap instead of GetGlyph()

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@avaloniaui-bot
Copy link

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

@Gillibald Gillibald changed the title [WIP] Universal GlyphTypeface implementation Universal GlyphTypeface implementation Nov 12, 2025
@MrJul
Copy link
Member

MrJul commented Nov 12, 2025

Notes from the API review meeting:

  • Mark IGlyphTypeface as NonClientImplementable
  • Consider making TryGetStream not part of the public API of IPlatformTypeface
  • Make OpenTypeTag.None/Max/MaxSigned internal
  • IGlyphTypeFace.IGlyphCount should be an int

@MrJul MrJul added api-change-requested The new public APIs need some changes. and removed needs-api-review The PR adds new public APIs that should be reviewed. labels Nov 12, 2025
@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0060677-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.

[CRASH] Android app initialization throws in base.OnCreate

4 participants