From fa6f58eaba11b24ebcaa8a53f71f86ab37a55260 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Mon, 31 Aug 2026 02:44:41 +0800 Subject: [PATCH 1/4] feat: honor styles in local text typefaces --- .../TestStyledLocalTypefaces.cs | 105 ++++++++++++++++++ CSharpMath.Rendering/BackEnd/Fonts.cs | 10 +- CSharpMath.Rendering/BackEnd/GlyphFinder.cs | 52 ++++++++- CSharpMath.Rendering/Text/TextTypesetter.cs | 4 +- 4 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs diff --git a/CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs b/CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs new file mode 100644 index 00000000..ab69ed17 --- /dev/null +++ b/CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CSharpMath.Atom; +using CSharpMath.Display.Displays; +using CSharpMath.Rendering.BackEnd; +using CSharpMath.Rendering.Text; +using Typography.OpenFont; +using Typography.OpenFont.Extensions; +using Xunit; +using RenderingGlyph = CSharpMath.Rendering.BackEnd.Glyph; + +namespace CSharpMath.Rendering.Tests { + public class TestStyledLocalTypefaces { + sealed class OneShotTypefaceEnumerable : IEnumerable { + readonly IReadOnlyList _faces; + public OneShotTypefaceEnumerable(IReadOnlyList faces) => _faces = faces; + public int EnumerationCount { get; private set; } + public IEnumerator GetEnumerator() { + if (++EnumerationCount > 1) throw new InvalidOperationException("Local typefaces were enumerated more than once."); + return _faces.GetEnumerator(); + } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } + static string FontPath(string fileName) => Path.GetFullPath(Path.Combine( + TestRenderingFixture.ThisDirectory.FullName, "..", "Typography", "Demo", "Windows", "TestFonts", fileName)); + static Typeface Read(string fileName) { + var path = FontPath(fileName); + Assert.SkipUnless(File.Exists(path), "The redistributable Arimo fixtures are supplied by the Typography submodule."); + using var stream = File.OpenRead(path); + return new OpenFontReader().Read(stream) ?? throw new InvalidOperationException("Invalid font fixture."); + } + static RenderingGlyph GlyphFor(FontStyle style, params Typeface[] typefaces) => GlyphForText(style, "A", typefaces); + static RenderingGlyph GlyphForText(FontStyle style, string text, params Typeface[] typefaces) { + var input = new TextAtom.Style(new TextAtom.Text(text), style); + var display = TextTypesetter.Layout(input, new Fonts(typefaces, 20), float.PositiveInfinity).relative; + return ((TextRunDisplay)display.Displays.Single()).Run.Glyphs.Single(); + } + [Fact] + public void SelectsEachOrdinaryStyleFromTheLocalFamily() { + var regular = Read("Arimo-Regular.ttf"); + var bold = Read("Arimo-Bold.ttf"); + var italic = Read("Arimo-Italic.ttf"); + var boldItalic = Read("Arimo-BoldItalic.ttf"); + Assert.Same(regular, GlyphFor(FontStyle.Roman, regular, bold, italic, boldItalic).Typeface); + Assert.Same(bold, GlyphFor(FontStyle.Bold, regular, bold, italic, boldItalic).Typeface); + Assert.Same(italic, GlyphFor(FontStyle.Italic, regular, bold, italic, boldItalic).Typeface); + Assert.Same(boldItalic, GlyphFor(FontStyle.BoldItalic, regular, bold, italic, boldItalic).Typeface); + } + [Fact] + public void FallsBackToMathFontWhenStyleOrGlyphIsMissing() { + var regular = Read("Arimo-Regular.ttf"); + var localFaces = new[] { regular, Read("Arimo-Bold.ttf"), Read("Arimo-Italic.ttf"), Read("Arimo-BoldItalic.ttf") }; + var bold = GlyphFor(FontStyle.Bold, regular); + var expectedMath = Fonts.GlobalTypefaces.First(t => t.HasMathTable()); + var codepoint = Enumerable.Range(0x2200, 0xC00) + .First(cp => localFaces.All(face => face.GetGlyphIndex(cp) == 0) && expectedMath.GetGlyphIndex(cp) != 0); + var missingGlyph = GlyphForText(FontStyle.Roman, char.ConvertFromUtf32(codepoint), regular); + Assert.Same(expectedMath, bold.Typeface); + Assert.Same(expectedMath, missingGlyph.Typeface); + Assert.Equal(expectedMath.GetGlyphIndex(codepoint), missingGlyph.Info.GlyphIndex); + } + [Fact] + public void MixedStylesRetainTheirOwnMetrics() { + var regular = Read("Arimo-Regular.ttf"); + var bold = Read("Arimo-Bold.ttf"); + var italic = Read("Arimo-Italic.ttf"); + var input = new TextAtom.List(new TextAtom[] { + new TextAtom.Style(new TextAtom.Text("A"), FontStyle.Roman), + new TextAtom.Style(new TextAtom.Text("A"), FontStyle.Bold), + new TextAtom.Style(new TextAtom.Text("A"), FontStyle.Italic), + }); + var display = TextTypesetter.Layout(input, new Fonts(new[] { regular, bold, italic }, 20), float.PositiveInfinity).relative; + var runs = display.Displays.Cast>().ToArray(); + Assert.Equal(new[] { regular, bold, italic }, runs.Select(r => r.Run.Glyphs.Single().Typeface)); + Assert.All(runs, run => Assert.True(run.Ascent > 0)); + var expectedAdvances = new[] { regular, bold, italic } + .Select(face => face.GetAdvanceWidthFromGlyphIndex(face.GetGlyphIndex('A')) + * face.CalculateScaleToPixelFromPointSize(20)).ToArray(); + Assert.Equal(expectedAdvances.Sum(), display.Width, 3); + Assert.Equal(0, runs[0].Position.X, 3); + Assert.Equal(expectedAdvances[0], runs[1].Position.X, 3); + Assert.Equal(expectedAdvances[0] + expectedAdvances[1], runs[2].Position.X, 3); + Assert.Equal(runs[0].Position.Y, runs[1].Position.Y, 3); + Assert.Equal(runs[1].Position.Y, runs[2].Position.Y, 3); + Assert.Equal(runs[0].Ascent, runs[1].Ascent, 3); + Assert.Equal(runs[1].Ascent, runs[2].Ascent, 3); + Assert.Equal(runs[0].Descent, runs[1].Descent, 3); + Assert.Equal(runs[1].Descent, runs[2].Descent, 3); + } + [Fact] + public void SnapshotsOneShotLocalTypefaceCollectionsConsistently() { + var regular = Read("Arimo-Regular.ttf"); + var bold = Read("Arimo-Bold.ttf"); + var source = new OneShotTypefaceEnumerable(new[] { regular, bold }); + var fonts = new Fonts(source, 20); + Assert.Equal(1, source.EnumerationCount); + var display = TextTypesetter.Layout( + new TextAtom.Style(new TextAtom.Text("A"), FontStyle.Bold), fonts, float.PositiveInfinity).relative; + var glyph = ((TextRunDisplay)display.Displays.Single()).Run.Glyphs.Single(); + Assert.Same(bold, glyph.Typeface); + } + } +} diff --git a/CSharpMath.Rendering/BackEnd/Fonts.cs b/CSharpMath.Rendering/BackEnd/Fonts.cs index 40dffef7..489c9b63 100644 --- a/CSharpMath.Rendering/BackEnd/Fonts.cs +++ b/CSharpMath.Rendering/BackEnd/Fonts.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using CSharpMath.Atom; using Typography.OpenFont; using Typography.OpenFont.Extensions; @@ -24,16 +25,21 @@ Typeface LoadFont(string fileName) { } public Fonts(IEnumerable localTypefaces, float pointSize) { PointSize = pointSize; - Typefaces = localTypefaces.Concat(GlobalTypefaces); + var localSnapshot = (localTypefaces ?? Enumerable.Empty()).ToArray(); + LocalTypefaces = localSnapshot; + LocalStyleTypefaces = GlyphFinder.BuildLocalStyleLookup(localSnapshot); + Typefaces = localSnapshot.Concat(GlobalTypefaces); MathTypeface = Typefaces.First(t => t.HasMathTable()); MathConsts = MathTypeface.MathConsts ?? throw new Atom.InvalidCodePathException(nameof(MathTypeface) + " doesn't have " + nameof(MathConsts)); } public static readonly Typefaces GlobalTypefaces = GetGlobalTypefaces(); public float PointSize { get; } + internal IEnumerable LocalTypefaces { get; } + internal IReadOnlyDictionary> LocalStyleTypefaces { get; } public IEnumerable Typefaces { get; } public Typeface MathTypeface { get; } public Typography.OpenFont.MathGlyphs.MathConstants MathConsts { get; } public IEnumerator GetEnumerator() => Typefaces.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => Typefaces.GetEnumerator(); } -} \ No newline at end of file +} diff --git a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs index dc9509e3..aa65309a 100644 --- a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs +++ b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs @@ -1,4 +1,9 @@ +using System; +using System.Collections.Generic; using System.Linq; +using CSharpMath.Atom; +using Typography.OpenFont; +using Typography.OpenFont.Extensions; namespace CSharpMath.Rendering.BackEnd { public class GlyphFinder : Display.FrontEnd.IGlyphFinder { @@ -15,6 +20,51 @@ public Glyph Lookup(Fonts fonts, int codepoint) { } return Lookup(fonts, GlyphNotFound); } + static bool IsOrdinary(FontStyle style) => + style is FontStyle.Roman or FontStyle.Bold or FontStyle.Italic or FontStyle.BoldItalic; + static FontStyle? GetOrdinaryStyle(Typeface typeface) { + var translated = typeface.TranslateOS2FontStyle(); + var bold = (translated & TranslatedOS2FontStyle.BOLD) != 0; + var italic = (translated & (TranslatedOS2FontStyle.ITALIC | TranslatedOS2FontStyle.OBLIQUE)) != 0; + return bold && italic ? FontStyle.BoldItalic + : bold ? FontStyle.Bold + : italic ? FontStyle.Italic + : FontStyle.Roman; + } + internal static IReadOnlyDictionary> BuildLocalStyleLookup( + IReadOnlyList localTypefaces) { + var result = new Dictionary>(); + var byStyle = new Dictionary>(); + foreach (var family in localTypefaces.GroupBy(t => t.Name, StringComparer.OrdinalIgnoreCase)) { + foreach (var face in family) { + var style = GetOrdinaryStyle(face).GetValueOrDefault(); + if (!byStyle.TryGetValue(style, out var faces)) + byStyle[style] = faces = new List(); + faces.Add(face); + } + } + foreach (var pair in byStyle) + result[pair.Key] = pair.Value; + return result; + } + Glyph LookupLocalStyle(Fonts fonts, int codepoint, FontStyle style) { + if (!fonts.LocalStyleTypefaces.TryGetValue(style, out var faces)) return Glyph.Empty; + foreach (var face in faces) { + var glyph = face.GetGlyphIndex(codepoint); + if (glyph != 0) return new Glyph(face, face.GetGlyph(glyph)); + } + return Glyph.Empty; + } + /// Find ordinary text glyphs in a matching local family before applying mathematical Unicode styling. + internal System.Collections.Generic.IEnumerable FindGlyphs(Fonts fonts, string str, FontStyle style) { + var styled = Display.UnicodeFontChanger.ChangeFont(str, style); + var sourceCodepoints = Typography.OpenFont.StringUtils.GetCodepoints(str.ToCharArray()).ToArray(); + var styledCodepoints = Typography.OpenFont.StringUtils.GetCodepoints(styled.ToCharArray()).ToArray(); + for (var i = 0; i < sourceCodepoints.Length; i++) { + var local = IsOrdinary(style) ? LookupLocalStyle(fonts, sourceCodepoints[i], style) : Glyph.Empty; + yield return local.IsEmpty ? Lookup(fonts, styledCodepoints[i]) : local; + } + } public int GetCodepoint(string str, int index) => index + 1 < str.Length && char.IsHighSurrogate(str[index]) @@ -33,4 +83,4 @@ public System.Collections.Generic.IEnumerable FindGlyphs(Fonts fonts, str public bool GlyphIsEmpty(Glyph glyph) => glyph.IsEmpty; public Glyph EmptyGlyph => Glyph.Empty; } -} \ No newline at end of file +} diff --git a/CSharpMath.Rendering/Text/TextTypesetter.cs b/CSharpMath.Rendering/Text/TextTypesetter.cs index 02552cd2..a57dd5e8 100644 --- a/CSharpMath.Rendering/Text/TextTypesetter.cs +++ b/CSharpMath.Rendering/Text/TextTypesetter.cs @@ -104,7 +104,7 @@ void FinalizeInlineDisplay(float ascender, float rawDescender, } case TextAtom.Text t: var content = UnicodeFontChanger.ChangeFont(t.Content, style); - var glyphs = GlyphFinder.Instance.FindGlyphs(fonts, content); + var glyphs = GlyphFinder.Instance.FindGlyphs(fonts, t.Content, style).ToList(); //Calling Select(g => g.Typeface).Distinct() speeds up query up to 10 times, //Calling Max(Func<,>) instead of Select(Func<,>).Max() speeds up query 2 times var typefaces = glyphs.Select(g => g.Typeface).Distinct().ToList(); @@ -216,4 +216,4 @@ void FinalizeInlineDisplay(float ascender, float rawDescender, return (new Display(relativePositionList), new Display(absolutePositionList)); } } -} \ No newline at end of file +} From 8c00382b375f0da98d7ebe069427c5f13b50ef97 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Mon, 31 Aug 2026 03:09:21 +0800 Subject: [PATCH 2/4] fix: preserve live local typeface collections --- .../TestStyledLocalTypefaces.cs | 50 +++++++++++++++++++ CSharpMath.Rendering/BackEnd/Fonts.cs | 36 +++++++++---- CSharpMath.Rendering/BackEnd/GlyphFinder.cs | 26 ++++++---- .../FrontEnd/ICSharpMathAPI.cs | 7 ++- CSharpMath.Rendering/FrontEnd/Painter.cs | 3 +- CSharpMath.Xaml/Views.cs | 3 +- 6 files changed, 104 insertions(+), 21 deletions(-) diff --git a/CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs b/CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs index ab69ed17..d8c501f4 100644 --- a/CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs +++ b/CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs @@ -23,6 +23,26 @@ public IEnumerator GetEnumerator() { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } + sealed class CountingTypefaceCollection : ICollection { + readonly List _faces; + public CountingTypefaceCollection(params Typeface[] faces) => _faces = new List(faces); + public int CaptureCount { get; private set; } + public int Count => _faces.Count; + public bool IsReadOnly => false; + public void Add(Typeface item) => _faces.Add(item); + public void Clear() => _faces.Clear(); + public bool Contains(Typeface item) => _faces.Contains(item); + public void CopyTo(Typeface[] array, int arrayIndex) { + CaptureCount++; + _faces.CopyTo(array, arrayIndex); + } + public bool Remove(Typeface item) => _faces.Remove(item); + public IEnumerator GetEnumerator() { + CaptureCount++; + return _faces.GetEnumerator(); + } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } static string FontPath(string fileName) => Path.GetFullPath(Path.Combine( TestRenderingFixture.ThisDirectory.FullName, "..", "Typography", "Demo", "Windows", "TestFonts", fileName)); static Typeface Read(string fileName) { @@ -101,5 +121,35 @@ public void SnapshotsOneShotLocalTypefaceCollectionsConsistently() { var glyph = ((TextRunDisplay)display.Displays.Single()).Run.Glyphs.Single(); Assert.Same(bold, glyph.Typeface); } + [Fact] + public void ReflectsMutableCollectionTypefaceAdditionsAndRemovals() { + var regular = Read("Arimo-Regular.ttf"); + var bold = Read("Arimo-Bold.ttf"); + var localFaces = new List { regular }; + var fonts = new Fonts(localFaces, 20); + static Typeface FindTypeface(Fonts fonts) { + var display = TextTypesetter.Layout( + new TextAtom.Style(new TextAtom.Text("A"), FontStyle.Bold), fonts, float.PositiveInfinity).relative; + return ((TextRunDisplay)display.Displays.Single()).Run.Glyphs.Single().Typeface; + } + Assert.NotSame(bold, FindTypeface(fonts)); + localFaces.Add(bold); + Assert.Same(bold, FindTypeface(fonts)); + localFaces.Remove(bold); + Assert.NotSame(bold, FindTypeface(fonts)); + } + [Fact] + public void CapturesMutableCollectionOncePerStyledFindGlyphs() { + var regular = Read("Arimo-Regular.ttf"); + var bold = Read("Arimo-Bold.ttf"); + var source = new CountingTypefaceCollection(regular, bold); + var fonts = new Fonts(source, 20); + var captureCountBeforeFind = source.CaptureCount; + var display = TextTypesetter.Layout( + new TextAtom.Style(new TextAtom.Text("A"), FontStyle.Bold), fonts, float.PositiveInfinity).relative; + var glyph = ((TextRunDisplay)display.Displays.Single()).Run.Glyphs.Single(); + Assert.Same(bold, glyph.Typeface); + Assert.Equal(captureCountBeforeFind + 1, source.CaptureCount); + } } } diff --git a/CSharpMath.Rendering/BackEnd/Fonts.cs b/CSharpMath.Rendering/BackEnd/Fonts.cs index 489c9b63..3a735cc0 100644 --- a/CSharpMath.Rendering/BackEnd/Fonts.cs +++ b/CSharpMath.Rendering/BackEnd/Fonts.cs @@ -23,23 +23,41 @@ Typeface LoadFont(string fileName) { globalTypefaces.AddSupplement(LoadFont("cyrillic-modern-nmr10.otf")); return globalTypefaces; } + readonly IEnumerable localTypefaceSource; + readonly Typeface[] localTypefaceSnapshot; + readonly bool localTypefacesAreMutableCollection; + internal IEnumerable LocalTypefaceSource => localTypefaceSource; + // Collection mutations are observed between rendering operations; callers must not mutate + // a collection concurrently with rendering. + internal Typeface[] GetLocalTypefacesSnapshot() => localTypefacesAreMutableCollection + ? localTypefaceSource.ToArray() + : localTypefaceSnapshot; + internal Typeface[] GetTypefacesSnapshot() => GetLocalTypefacesSnapshot().Concat(GlobalTypefaces).ToArray(); + internal Typeface[] GetTypefacesSnapshot(Typeface[] localTypefaces) => localTypefaces.Concat(GlobalTypefaces).ToArray(); public Fonts(IEnumerable localTypefaces, float pointSize) { PointSize = pointSize; - var localSnapshot = (localTypefaces ?? Enumerable.Empty()).ToArray(); - LocalTypefaces = localSnapshot; - LocalStyleTypefaces = GlyphFinder.BuildLocalStyleLookup(localSnapshot); - Typefaces = localSnapshot.Concat(GlobalTypefaces); - MathTypeface = Typefaces.First(t => t.HasMathTable()); + if (localTypefaces is Fonts fonts) { + localTypefaceSource = fonts.LocalTypefaceSource; + localTypefaceSnapshot = fonts.localTypefaceSnapshot; + localTypefacesAreMutableCollection = fonts.localTypefacesAreMutableCollection; + } else if (localTypefaces is ICollection || localTypefaces is IReadOnlyCollection) { + localTypefaceSource = localTypefaces ?? Enumerable.Empty(); + localTypefaceSnapshot = null; + localTypefacesAreMutableCollection = true; + } else { + localTypefaceSnapshot = (localTypefaces ?? Enumerable.Empty()).ToArray(); + localTypefaceSource = localTypefaceSnapshot; + localTypefacesAreMutableCollection = false; + } + MathTypeface = GetTypefacesSnapshot().First(t => t.HasMathTable()); MathConsts = MathTypeface.MathConsts ?? throw new Atom.InvalidCodePathException(nameof(MathTypeface) + " doesn't have " + nameof(MathConsts)); } public static readonly Typefaces GlobalTypefaces = GetGlobalTypefaces(); public float PointSize { get; } - internal IEnumerable LocalTypefaces { get; } - internal IReadOnlyDictionary> LocalStyleTypefaces { get; } - public IEnumerable Typefaces { get; } + public IEnumerable Typefaces => GetTypefacesSnapshot(); public Typeface MathTypeface { get; } public Typography.OpenFont.MathGlyphs.MathConstants MathConsts { get; } - public IEnumerator GetEnumerator() => Typefaces.GetEnumerator(); + public IEnumerator GetEnumerator() => GetTypefacesSnapshot().AsEnumerable().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => Typefaces.GetEnumerator(); } } diff --git a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs index aa65309a..a33018b7 100644 --- a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs +++ b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs @@ -14,11 +14,14 @@ private GlyphFinder() { } public const char GlyphNotFound = '□'; public static GlyphFinder Instance { get; } = new GlyphFinder(); public Glyph Lookup(Fonts fonts, int codepoint) { - foreach (var font in fonts) { + return Lookup(fonts.GetTypefacesSnapshot(), codepoint); + } + static Glyph Lookup(IEnumerable typefaces, int codepoint) { + foreach (var font in typefaces) { var g = font.GetGlyphIndex(codepoint); if (g != 0) return new Glyph(font, font.GetGlyph(g)); } - return Lookup(fonts, GlyphNotFound); + return Lookup(typefaces, GlyphNotFound); } static bool IsOrdinary(FontStyle style) => style is FontStyle.Roman or FontStyle.Bold or FontStyle.Italic or FontStyle.BoldItalic; @@ -47,8 +50,8 @@ internal static IReadOnlyDictionary> BuildLoc result[pair.Key] = pair.Value; return result; } - Glyph LookupLocalStyle(Fonts fonts, int codepoint, FontStyle style) { - if (!fonts.LocalStyleTypefaces.TryGetValue(style, out var faces)) return Glyph.Empty; + Glyph LookupLocalStyle(IReadOnlyDictionary> localStyles, int codepoint, FontStyle style) { + if (!localStyles.TryGetValue(style, out var faces)) return Glyph.Empty; foreach (var face in faces) { var glyph = face.GetGlyphIndex(codepoint); if (glyph != 0) return new Glyph(face, face.GetGlyph(glyph)); @@ -57,12 +60,15 @@ Glyph LookupLocalStyle(Fonts fonts, int codepoint, FontStyle style) { } /// Find ordinary text glyphs in a matching local family before applying mathematical Unicode styling. internal System.Collections.Generic.IEnumerable FindGlyphs(Fonts fonts, string str, FontStyle style) { + var localTypefaces = fonts.GetLocalTypefacesSnapshot(); + var typefaces = fonts.GetTypefacesSnapshot(localTypefaces); + var localStyles = BuildLocalStyleLookup(localTypefaces); var styled = Display.UnicodeFontChanger.ChangeFont(str, style); var sourceCodepoints = Typography.OpenFont.StringUtils.GetCodepoints(str.ToCharArray()).ToArray(); var styledCodepoints = Typography.OpenFont.StringUtils.GetCodepoints(styled.ToCharArray()).ToArray(); for (var i = 0; i < sourceCodepoints.Length; i++) { - var local = IsOrdinary(style) ? LookupLocalStyle(fonts, sourceCodepoints[i], style) : Glyph.Empty; - yield return local.IsEmpty ? Lookup(fonts, styledCodepoints[i]) : local; + var local = IsOrdinary(style) ? LookupLocalStyle(localStyles, sourceCodepoints[i], style) : Glyph.Empty; + yield return local.IsEmpty ? Lookup(typefaces, styledCodepoints[i]) : local; } } public int GetCodepoint(string str, int index) => @@ -77,9 +83,11 @@ public int GetCodepoint(string str, int index) => : str[index]; public Glyph FindGlyphForCharacterAtIndex(Fonts fonts, int index, string str) => Lookup(fonts, GetCodepoint(str, index)); - public System.Collections.Generic.IEnumerable FindGlyphs(Fonts fonts, string str) => - Typography.OpenFont.StringUtils.GetCodepoints(str.ToCharArray()) - .Select(c => Lookup(fonts, c)); + public System.Collections.Generic.IEnumerable FindGlyphs(Fonts fonts, string str) { + var typefaces = fonts.GetTypefacesSnapshot(); + return Typography.OpenFont.StringUtils.GetCodepoints(str.ToCharArray()) + .Select(c => Lookup(typefaces, c)); + } public bool GlyphIsEmpty(Glyph glyph) => glyph.IsEmpty; public Glyph EmptyGlyph => Glyph.Empty; } diff --git a/CSharpMath.Rendering/FrontEnd/ICSharpMathAPI.cs b/CSharpMath.Rendering/FrontEnd/ICSharpMathAPI.cs index 76efc5b5..b7a57639 100644 --- a/CSharpMath.Rendering/FrontEnd/ICSharpMathAPI.cs +++ b/CSharpMath.Rendering/FrontEnd/ICSharpMathAPI.cs @@ -17,6 +17,11 @@ public interface ICSharpMathAPI where TContent : class { #region Display-recreating properties /// Unit of measure: points float FontSize { get; set; } + /// + /// Local typefaces used during rendering. Collection mutations between render operations are + /// observed; callers must not mutate a collection concurrently with rendering. Non-collection + /// enumerables are materialized once. + /// System.Collections.Generic.IEnumerable LocalTypefaces { get; set; } Atom.LineStyle LineStyle { get; set; } TContent? Content { get; set; } @@ -53,4 +58,4 @@ public static PointF GetDisplayPosition( return new PointF(x + offsetX, y + offsetY - height); } } -} \ No newline at end of file +} diff --git a/CSharpMath.Rendering/FrontEnd/Painter.cs b/CSharpMath.Rendering/FrontEnd/Painter.cs index 02acaa6f..278285fe 100644 --- a/CSharpMath.Rendering/FrontEnd/Painter.cs +++ b/CSharpMath.Rendering/FrontEnd/Painter.cs @@ -46,6 +46,7 @@ public Painter() { /// Unit of measure: points public float FontSize { get => Fonts.PointSize; set { Fonts = new Fonts(Fonts, value); SetRedisplay(); } } IEnumerable __localTypefaces = Array.Empty(); + /// public IEnumerable LocalTypefaces { get => __localTypefaces; set { Fonts = new Fonts(value, FontSize); __localTypefaces = value; SetRedisplay(); } } Atom.LineStyle __style = Atom.LineStyle.Display; public Atom.LineStyle LineStyle { get => __style; set { __style = value; SetRedisplay(); } } @@ -132,4 +133,4 @@ GlyphBoxColor is var (glyph, textRun) ? Nullable((WrapColor(glyph), WrapColor(te public Painter ShallowClone() => (Painter)MemberwiseClone(); #endregion Methods } -} \ No newline at end of file +} diff --git a/CSharpMath.Xaml/Views.cs b/CSharpMath.Xaml/Views.cs index 09bdd917..0afc1b96 100644 --- a/CSharpMath.Xaml/Views.cs +++ b/CSharpMath.Xaml/Views.cs @@ -289,6 +289,7 @@ protected override void RenderOverride(XCanvas canvas, Windows.Foundation.Size a /// Unit of measure: points; Defaults to . public float? ErrorFontSize { get => (float?)GetValue(ErrorFontSizeProperty); set => SetValue(ErrorFontSizeProperty, value); } public static readonly XProperty ErrorFontSizeProperty; + /// public IEnumerable LocalTypefaces { get => (IEnumerable)GetValue(LocalTypefacesProperty)!; set => SetValue(LocalTypefacesProperty, value); } public static readonly XProperty LocalTypefacesProperty; public XColor TextColor { get => (XColor)GetValue(TextColorProperty)!; set => SetValue(TextColorProperty, value); } @@ -321,4 +322,4 @@ protected override void RenderOverride(XCanvas canvas, Windows.Foundation.Size a } public partial class MathView : BaseView { } public partial class TextView : BaseView { } -} \ No newline at end of file +} From 5a204e07dfe7a3f5087672e7525c56b110955901 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Mon, 31 Aug 2026 12:41:37 +0800 Subject: [PATCH 3/4] feat: select semantic text typefaces --- .../SemanticTypefaceDescriptorTests.cs | 208 ++++++++++++++++++ CSharpMath.Rendering/BackEnd/FontSnapshot.cs | 20 ++ CSharpMath.Rendering/BackEnd/Fonts.cs | 70 ++++++ CSharpMath.Rendering/BackEnd/GlyphFinder.cs | 76 ++++++- .../BackEnd/TypefaceDescriptor.cs | 76 +++++++ .../DebugApi/PublicAPI.Unshipped.txt | 13 ++ .../ReleaseApi/PublicAPI.Unshipped.txt | 13 ++ CSharpMath.Rendering/Text/TextLaTeXParser.cs | 9 +- CSharpMath.Rendering/Text/TextTypesetter.cs | 8 +- 9 files changed, 484 insertions(+), 9 deletions(-) create mode 100644 CSharpMath.Rendering.Text.Tests/SemanticTypefaceDescriptorTests.cs create mode 100644 CSharpMath.Rendering/BackEnd/FontSnapshot.cs create mode 100644 CSharpMath.Rendering/BackEnd/TypefaceDescriptor.cs diff --git a/CSharpMath.Rendering.Text.Tests/SemanticTypefaceDescriptorTests.cs b/CSharpMath.Rendering.Text.Tests/SemanticTypefaceDescriptorTests.cs new file mode 100644 index 00000000..bb26bea4 --- /dev/null +++ b/CSharpMath.Rendering.Text.Tests/SemanticTypefaceDescriptorTests.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CSharpMath.Atom; +using CSharpMath.Display.Displays; +using CSharpMath.Rendering.BackEnd; +using CSharpMath.Rendering.Text; +using Typography.OpenFont; +using Xunit; +using BackendGlyph = CSharpMath.Rendering.BackEnd.Glyph; + +namespace CSharpMath.Rendering.Text.Tests { + public class SemanticTypefaceDescriptorTests { + static Typeface Read(string name) { + var path = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", + "Typography", "Demo", "Windows", "TestFonts", name)); + Assert.True(File.Exists(path), path); + using var stream = File.OpenRead(path); + return new OpenFontReader().Read(stream); + } + + [Fact] + public void ConfiguredSmallCapMapUsesGlyphAndRetainsSourceText() { + var source = Read("SourceSerifPro-Regular.otf"); + var descriptor = new TypefaceDescriptor(source, FontFamily.Roman, FontWeight.Regular, + FontPosture.Upright, smallCapitalsGlyphMap: new System.Collections.Generic.Dictionary { + ['a'] = 1108 + }); + var fonts = Fonts.FromDescriptors(new[] { descriptor }, 20); + var atom = new TextAtom.Style(new TextAtom.Text("a"), new TextStyleChange( + FontFamily.Roman, FontWeight.Regular, FontPosture.Upright, FontCapitals.SmallCapitals)); + var line = TextTypesetter.Layout(atom, fonts, float.PositiveInfinity).relative; + var run = Assert.IsType>(line.Displays.Single()); + Assert.Equal("a", run.Run.Text.ToString()); + Assert.Equal((ushort)1108, run.Run.GlyphInfos.Single().Glyph.Info.GlyphIndex); + Assert.Same(source, run.Run.GlyphInfos.Single().Glyph.Typeface); + Assert.Equal(589, source.GetAdvanceWidthFromGlyphIndex(1108)); + Assert.Equal(664, source.GetAdvanceWidthFromGlyphIndex(source.GetGlyphIndex('\u0041'))); + Assert.Equal(509, source.GetAdvanceWidthFromGlyphIndex(source.GetGlyphIndex('\u0061'))); + Assert.Equal(source.GetAdvanceWidthFromGlyphIndex(1108), + run.Run.GlyphInfos.Single().Glyph.Typeface.GetAdvanceWidthFromGlyphIndex(1108)); + } + + [Fact] + public void DescriptorPreservesSemanticIdentityAndStableEquality() { + var source = Read("SourceSerifPro-Regular.otf"); + var italic = new TypefaceDescriptor(source, FontFamily.Roman, FontWeight.Regular, FontPosture.Italic, + supportedFeatures: new[] { "kern", "liga", "kern" }); + var slanted = new TypefaceDescriptor(source, FontFamily.Roman, FontWeight.Regular, FontPosture.Slanted, + supportedFeatures: new[] { "liga", "kern" }); + var same = new TypefaceDescriptor(source, FontFamily.Roman, FontWeight.Regular, FontPosture.Italic, + supportedFeatures: new[] { "liga", "kern" }); + Assert.NotEqual(italic, slanted); + Assert.Equal(italic, same); + Assert.Equal(italic.GetHashCode(), same.GetHashCode()); + Assert.Equal(new[] { "kern", "liga" }, italic.SupportedFeatures); + Assert.Equal(FontFamily.Roman, italic.Family); + Assert.Equal(FontWeight.Regular, italic.Weight); + Assert.Equal(FontPosture.Italic, italic.Posture); + } + + [Fact] + public void DescriptorCollectionsAreDefensiveAndDefaultStyleRemainsCompatible() { + var source = Read("SourceSerifPro-Regular.otf"); + var map = new Dictionary { ['a'] = 1108 }; + var features = new List { "kern" }; + var descriptor = new TypefaceDescriptor(source, FontFamily.Default, FontWeight.Regular, + FontPosture.Upright, smallCapitalsGlyphMap: map, supportedFeatures: features); + map['a'] = 1109; + features.Add("liga"); + Assert.Equal((ushort)1108, descriptor.SmallCapitalsGlyphMap['a']); + Assert.Single(descriptor.SupportedFeatures); + Assert.Throws(() => ((ICollection)descriptor.SupportedFeatures).Add("liga")); + Assert.Throws(() => Fonts.FromDescriptors(null!, 20)); + Assert.Throws(() => Fonts.FromDescriptors(new TypefaceDescriptor[] { null! }, 20)); + } + + [Fact] + public void AbsoluteAndRelativeSizeRetainDescriptorGlyphSelection() { + var source = Read("SourceSerifPro-Regular.otf"); + var descriptor = new TypefaceDescriptor(source, FontFamily.Roman, FontWeight.Regular, + FontPosture.Upright, smallCapitalsGlyphMap: new Dictionary { ['a'] = 1108 }); + var fonts = Fonts.FromDescriptors(new[] { descriptor }, 20); + static TextRunDisplay Run(TextAtom atom, Fonts fonts) { + var line = TextTypesetter.Layout(atom, fonts, float.PositiveInfinity).relative; + return Assert.IsType>(line.Displays.Single()); + } + var normal = Run(new TextAtom.Style(new TextAtom.Text("a"), new TextStyleChange( + FontFamily.Roman, FontWeight.Regular, FontPosture.Upright, FontCapitals.SmallCapitals)), fonts); + var absolute = Run(new TextAtom.Size(new TextAtom.Style(new TextAtom.Text("a"), new TextStyleChange( + FontFamily.Roman, FontWeight.Regular, FontPosture.Upright, FontCapitals.SmallCapitals)), 40), fonts); + var relative = Run(new TextAtom.RelativeSize(new TextAtom.Style(new TextAtom.Text("a"), new TextStyleChange( + FontFamily.Roman, FontWeight.Regular, FontPosture.Upright, FontCapitals.SmallCapitals)), "large"), fonts); + Assert.All(new[] { normal, absolute, relative }, run => { + Assert.Same(source, run.Run.GlyphInfos.Single().Glyph.Typeface); + Assert.Equal((ushort)1108, run.Run.GlyphInfos.Single().Glyph.Info.GlyphIndex); + }); + Assert.True(absolute.Width > normal.Width); + Assert.True(relative.Width > normal.Width); + } + + [Fact] + public void MutableDescriptorCollectionIsObservedBetweenSnapshots() { + var source = Read("SourceSerifPro-Regular.otf"); + var descriptors = new List { + new(source, FontFamily.Roman, FontWeight.Regular, FontPosture.Upright) + }; + var fonts = Fonts.FromDescriptors(descriptors, 20); + var globalCount = Fonts.GlobalTypefaces.Count(); + Assert.Equal(globalCount + 1, fonts.Typefaces.Count()); + descriptors.Add(new TypefaceDescriptor(source, FontFamily.SansSerif, FontWeight.Bold, FontPosture.Italic)); + Assert.Equal(globalCount + 2, fonts.Typefaces.Count()); + descriptors.RemoveAt(1); + Assert.Equal(globalCount + 1, fonts.Typefaces.Count()); + } + + [Fact] + public void UnicodeSourceTextKeepsScalarsAndClusterStarts() { + var source = Read("SourceSerifPro-Regular.otf"); + var fonts = Fonts.FromDescriptors(new[] { new TypefaceDescriptor(source, FontFamily.Roman, + FontWeight.Regular, FontPosture.Upright) }, 20); + var text = "a,\u00E9\U0001F984"; + var line = TextTypesetter.Layout(new TextAtom.Text(text), fonts, float.PositiveInfinity).relative; + var run = Assert.IsType>(line.Displays.Single()); + Assert.Equal(text, run.Run.Text.ToString()); + Assert.Equal(4, run.Run.GlyphInfos.Count()); + } + + [Fact] + public void OneShotDescriptorEnumerableIsEnumeratedOncePerLayout() { + var source = Read("SourceSerifPro-Regular.otf"); + var descriptor = new TypefaceDescriptor(source, FontFamily.Roman, FontWeight.Regular, FontPosture.Upright); + var oneShot = new OneShotDescriptors(descriptor); + var fonts = Fonts.FromDescriptors(oneShot, 20); + var atom = new TextAtom.Style(new TextAtom.Text("A"), new TextStyleChange( + FontFamily.Roman, FontWeight.Regular, FontPosture.Upright, FontCapitals.Normal)); + var line = TextTypesetter.Layout(atom, fonts, float.PositiveInfinity).relative; + var run = Assert.IsType>(line.Displays.Single()); + Assert.Same(source, run.Run.GlyphInfos.Single().Glyph.Typeface); + Assert.Equal(1, oneShot.EnumerationCount); + } + + [Fact] + public void ExplicitSlantedFaceWinsOverEarlierItalicAndFallsBackToUpright() { + var upright = Read("SourceSerifPro-Regular.otf"); + var italic = Read("SourceSansPro-Regular.otf"); + var slanted = Read("ChulabhornLikitText-Regular.otf"); + var descriptors = new List { + new(upright, FontFamily.Roman, FontWeight.Regular, FontPosture.Upright), + new(italic, FontFamily.Roman, FontWeight.Regular, FontPosture.Italic), + new(slanted, FontFamily.Roman, FontWeight.Regular, FontPosture.Slanted) + }; + var fonts = Fonts.FromDescriptors(descriptors, 20); + static Typeface Selected(Fonts fonts, FontPosture posture) { + var atom = new TextAtom.Style(new TextAtom.Text("A"), new TextStyleChange( + FontFamily.Roman, FontWeight.Regular, posture, FontCapitals.Normal)); + var line = TextTypesetter.Layout(atom, fonts, float.PositiveInfinity).relative; + return Assert.IsType>(line.Displays.Single()).Run.GlyphInfos.Single().Glyph.Typeface; + } + Assert.Same(slanted, Selected(fonts, FontPosture.Slanted)); + descriptors.RemoveAt(2); + Assert.Same(upright, Selected(fonts, FontPosture.Slanted)); + } + + [Fact] + public void LocalNonExactFamilyDescriptorBeatsGlobalExactCandidate() { + var local = Read("SourceSansPro-Regular.otf"); + var fonts = Fonts.FromDescriptors(new[] { + new TypefaceDescriptor(local, FontFamily.SansSerif, FontWeight.Regular, FontPosture.Upright) + }, 20); + var atom = new TextAtom.Style(new TextAtom.Text("A"), new TextStyleChange( + FontFamily.SansSerif, FontWeight.Regular, FontPosture.Upright, FontCapitals.Normal)); + var line = TextTypesetter.Layout(atom, fonts, float.PositiveInfinity).relative; + var run = Assert.IsType>(line.Displays.Single()); + Assert.Same(local, run.Run.GlyphInfos.Single().Glyph.Typeface); + } + + sealed class OneShotDescriptors : IEnumerable { + readonly TypefaceDescriptor descriptor; + public int EnumerationCount { get; private set; } + public OneShotDescriptors(TypefaceDescriptor descriptor) => this.descriptor = descriptor; + public IEnumerator GetEnumerator() { + if (++EnumerationCount != 1) throw new InvalidOperationException("descriptor source enumerated twice"); + yield return descriptor; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [Fact] + public void DescriptorRejectsInvalidGlyphMapAndFeatures() { + var source = Read("SourceSerifPro-Regular.otf"); + Assert.Throws(() => new TypefaceDescriptor( + source, FontFamily.Roman, FontWeight.Regular, FontPosture.Upright, + smallCapitalsGlyphMap: new System.Collections.Generic.Dictionary { ['a'] = 0 })); + Assert.Throws(() => new TypefaceDescriptor( + source, FontFamily.Roman, FontWeight.Regular, FontPosture.Upright, + supportedFeatures: new[] { "bad" })); + Assert.Throws(() => new TypefaceDescriptor( + source, (FontFamily)99, FontWeight.Regular, FontPosture.Upright)); + Assert.Throws(() => new TypefaceDescriptor( + source, FontFamily.Roman, (FontWeight)99, FontPosture.Upright)); + Assert.Throws(() => new TypefaceDescriptor( + source, FontFamily.Roman, FontWeight.Regular, (FontPosture)99)); + } + } +} diff --git a/CSharpMath.Rendering/BackEnd/FontSnapshot.cs b/CSharpMath.Rendering/BackEnd/FontSnapshot.cs new file mode 100644 index 00000000..72bf880a --- /dev/null +++ b/CSharpMath.Rendering/BackEnd/FontSnapshot.cs @@ -0,0 +1,20 @@ +using System.Linq; +using Typography.OpenFont; + +namespace CSharpMath.Rendering.BackEnd { + internal sealed class FontSnapshot { + public FontSnapshot(TypefaceDescriptor[] localDescriptors, Typeface[] localTypefaces, + Typeface[] globalTypefaces) { + LocalDescriptors = localDescriptors; + LocalTypefaces = localTypefaces; + GlobalTypefaces = globalTypefaces; + Descriptors = localDescriptors.Concat(globalTypefaces.Select(TypefaceDescriptor.Adapt)).ToArray(); + Typefaces = localTypefaces.Concat(globalTypefaces).ToArray(); + } + public TypefaceDescriptor[] LocalDescriptors { get; } + public TypefaceDescriptor[] Descriptors { get; } + public Typeface[] LocalTypefaces { get; } + public Typeface[] GlobalTypefaces { get; } + public Typeface[] Typefaces { get; } + } +} diff --git a/CSharpMath.Rendering/BackEnd/Fonts.cs b/CSharpMath.Rendering/BackEnd/Fonts.cs index 3a735cc0..9da43cb2 100644 --- a/CSharpMath.Rendering/BackEnd/Fonts.cs +++ b/CSharpMath.Rendering/BackEnd/Fonts.cs @@ -26,6 +26,11 @@ Typeface LoadFont(string fileName) { readonly IEnumerable localTypefaceSource; readonly Typeface[] localTypefaceSnapshot; readonly bool localTypefacesAreMutableCollection; + readonly IEnumerable localDescriptorSource; + readonly TypefaceDescriptor[] localDescriptorSnapshot; + readonly bool localDescriptorsAreMutableCollection; + readonly bool hasDescriptors; + internal bool UsesDescriptors => hasDescriptors; internal IEnumerable LocalTypefaceSource => localTypefaceSource; // Collection mutations are observed between rendering operations; callers must not mutate // a collection concurrently with rendering. @@ -34,9 +39,39 @@ internal Typeface[] GetLocalTypefacesSnapshot() => localTypefacesAreMutableColle : localTypefaceSnapshot; internal Typeface[] GetTypefacesSnapshot() => GetLocalTypefacesSnapshot().Concat(GlobalTypefaces).ToArray(); internal Typeface[] GetTypefacesSnapshot(Typeface[] localTypefaces) => localTypefaces.Concat(GlobalTypefaces).ToArray(); + internal FontSnapshot CaptureSnapshot() { + if (hasDescriptors) { + // Enumerate a mutable descriptor source exactly once; all derived arrays come from it. + var localDescriptors = (localDescriptorsAreMutableCollection + ? localDescriptorSource.ToArray() : localDescriptorSnapshot).ToArray(); + ValidateDescriptors(localDescriptors, nameof(localDescriptorSource)); + return new FontSnapshot(localDescriptors, localDescriptors.Select(d => d.Typeface).ToArray(), + GlobalTypefaces.ToArray()); + } + var localTypefaces = GetLocalTypefacesSnapshot(); + return new FontSnapshot(System.Array.Empty(), localTypefaces, + GlobalTypefaces.ToArray()); + } + internal TypefaceDescriptor[] GetDescriptorsSnapshot() { + if (!hasDescriptors) return System.Array.Empty(); + var local = localDescriptorsAreMutableCollection + ? localDescriptorSource.ToArray() : localDescriptorSnapshot; + ValidateDescriptors(local, nameof(localDescriptorSource)); + return local.Concat(GlobalTypefaces.Select(TypefaceDescriptor.Adapt)).ToArray(); + } public Fonts(IEnumerable localTypefaces, float pointSize) { PointSize = pointSize; + hasDescriptors = false; + localDescriptorSource = null; + localDescriptorSnapshot = null; + localDescriptorsAreMutableCollection = false; if (localTypefaces is Fonts fonts) { + hasDescriptors = fonts.hasDescriptors; + if (hasDescriptors) { + localDescriptorSource = fonts.localDescriptorSource; + localDescriptorSnapshot = fonts.localDescriptorSnapshot; + localDescriptorsAreMutableCollection = fonts.localDescriptorsAreMutableCollection; + } localTypefaceSource = fonts.LocalTypefaceSource; localTypefaceSnapshot = fonts.localTypefaceSnapshot; localTypefacesAreMutableCollection = fonts.localTypefacesAreMutableCollection; @@ -52,6 +87,41 @@ public Fonts(IEnumerable localTypefaces, float pointSize) { MathTypeface = GetTypefacesSnapshot().First(t => t.HasMathTable()); MathConsts = MathTypeface.MathConsts ?? throw new Atom.InvalidCodePathException(nameof(MathTypeface) + " doesn't have " + nameof(MathConsts)); } + Fonts(IEnumerable descriptors, float pointSize) { + PointSize = pointSize; + hasDescriptors = true; + if (descriptors is ICollection || descriptors is IReadOnlyCollection) { + localDescriptorSource = descriptors ?? Enumerable.Empty(); + localDescriptorSnapshot = null; + localDescriptorsAreMutableCollection = true; + } else { + localDescriptorSnapshot = (descriptors ?? Enumerable.Empty()).ToArray(); + ValidateDescriptors(localDescriptorSnapshot, nameof(descriptors)); + localDescriptorSource = localDescriptorSnapshot; + localDescriptorsAreMutableCollection = false; + } + if (localDescriptorsAreMutableCollection) { + localTypefaceSnapshot = null; + localTypefaceSource = localDescriptorSource.Select(d => d.Typeface); + localTypefacesAreMutableCollection = true; + } else { + localTypefaceSnapshot = localDescriptorSnapshot.Select(d => d.Typeface).ToArray(); + localTypefaceSource = localTypefaceSnapshot; + localTypefacesAreMutableCollection = false; + } + MathTypeface = GetDescriptorsSnapshot().Select(d => d.Typeface).First(t => t.HasMathTable()); + MathConsts = MathTypeface.MathConsts ?? throw new Atom.InvalidCodePathException(nameof(MathTypeface) + " doesn't have " + nameof(MathConsts)); + } + static void ValidateDescriptors(IEnumerable descriptors, string parameterName) { + if (descriptors == null) throw new System.ArgumentNullException(parameterName); + if (descriptors.Any(d => d == null)) + throw new System.ArgumentException("Descriptor collections cannot contain null elements.", parameterName); + } + /// Creates a font collection with explicit semantic typeface metadata. + public static Fonts FromDescriptors(IEnumerable descriptors, float pointSize) => + descriptors == null + ? throw new System.ArgumentNullException(nameof(descriptors)) + : new Fonts(descriptors, pointSize); public static readonly Typefaces GlobalTypefaces = GetGlobalTypefaces(); public float PointSize { get; } public IEnumerable Typefaces => GetTypefacesSnapshot(); diff --git a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs index a33018b7..f3bf2242 100644 --- a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs +++ b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs @@ -60,17 +60,85 @@ Glyph LookupLocalStyle(IReadOnlyDictionary> l } /// Find ordinary text glyphs in a matching local family before applying mathematical Unicode styling. internal System.Collections.Generic.IEnumerable FindGlyphs(Fonts fonts, string str, FontStyle style) { - var localTypefaces = fonts.GetLocalTypefacesSnapshot(); - var typefaces = fonts.GetTypefacesSnapshot(localTypefaces); - var localStyles = BuildLocalStyleLookup(localTypefaces); + var snapshot = fonts.CaptureSnapshot(); + var localStyles = BuildLocalStyleLookup(snapshot.LocalTypefaces); var styled = Display.UnicodeFontChanger.ChangeFont(str, style); var sourceCodepoints = Typography.OpenFont.StringUtils.GetCodepoints(str.ToCharArray()).ToArray(); var styledCodepoints = Typography.OpenFont.StringUtils.GetCodepoints(styled.ToCharArray()).ToArray(); for (var i = 0; i < sourceCodepoints.Length; i++) { var local = IsOrdinary(style) ? LookupLocalStyle(localStyles, sourceCodepoints[i], style) : Glyph.Empty; - yield return local.IsEmpty ? Lookup(typefaces, styledCodepoints[i]) : local; + yield return local.IsEmpty ? Lookup(snapshot.Typefaces, styledCodepoints[i]) : local; + } + } + internal System.Collections.Generic.IEnumerable FindGlyphs(Fonts fonts, string str, TextStyle semanticStyle) { + var snapshot = fonts.CaptureSnapshot(); + var descriptors = snapshot.Descriptors; + var localTypefaces = snapshot.LocalTypefaces; + var typefaces = snapshot.Typefaces; + var localStyles = BuildLocalStyleLookup(localTypefaces); + // Slanted is never approximated with mathematical italic. If no slanted face exists, + // retry the same family/weight as upright before using the legacy Unicode fallback. + var fallbackSemanticStyle = semanticStyle.Posture == FontPosture.Slanted + ? semanticStyle.WithPosture(FontPosture.Upright) : semanticStyle; + var legacyStyle = fallbackSemanticStyle.ToFontStyle(); + var styled = Display.UnicodeFontChanger.ChangeFont(str, legacyStyle); + var sourceCodepoints = Typography.OpenFont.StringUtils.GetCodepoints(str.ToCharArray()).ToArray(); + var styledCodepoints = Typography.OpenFont.StringUtils.GetCodepoints(styled.ToCharArray()).ToArray(); + for (var i = 0; i < sourceCodepoints.Length; i++) { + var found = false; + var matchingDescriptors = snapshot.LocalDescriptors.Where(d => Matches(d, semanticStyle)); + if (semanticStyle.Posture == FontPosture.Slanted) + matchingDescriptors = matchingDescriptors.Concat( + snapshot.LocalDescriptors.Where(d => Matches(d, fallbackSemanticStyle))); + matchingDescriptors = matchingDescriptors.Concat( + snapshot.LocalDescriptors.Where(d => d.Family == styleFamily(semanticStyle) && + d.Weight == semanticStyle.Weight && + (semanticStyle.Posture != FontPosture.Slanted || d.Posture != FontPosture.Italic))); + matchingDescriptors = matchingDescriptors.Concat(snapshot.LocalDescriptors + .Where(d => semanticStyle.Posture != FontPosture.Slanted || d.Posture != FontPosture.Italic)); + foreach (var descriptor in matchingDescriptors) { + var face = semanticStyle.Capitals == FontCapitals.SmallCapitals + ? descriptor.SmallCapitalsTypeface ?? descriptor.Typeface : descriptor.Typeface; + if (semanticStyle.Capitals == FontCapitals.SmallCapitals && + descriptor.SmallCapitalsGlyphMap.TryGetValue(sourceCodepoints[i], out var mapped)) { + yield return new Glyph(face, face.GetGlyph(mapped)); + found = true; + break; + } + var glyphIndex = face.GetGlyphIndex(sourceCodepoints[i]); + if (glyphIndex != 0) { + yield return new Glyph(face, face.GetGlyph(glyphIndex)); + found = true; + break; + } + } + if (!found) { + foreach (var descriptor in descriptors.Skip(snapshot.LocalDescriptors.Length) + .Where(d => Matches(d, semanticStyle) || Matches(d, fallbackSemanticStyle))) { + var face = semanticStyle.Capitals == FontCapitals.SmallCapitals + ? descriptor.SmallCapitalsTypeface ?? descriptor.Typeface : descriptor.Typeface; + var glyphIndex = face.GetGlyphIndex(sourceCodepoints[i]); + if (glyphIndex != 0) { + yield return new Glyph(face, face.GetGlyph(glyphIndex)); + found = true; + break; + } + } + } + if (!found) { + var local = IsOrdinary(legacyStyle) ? LookupLocalStyle(localStyles, sourceCodepoints[i], legacyStyle) : Glyph.Empty; + yield return local.IsEmpty ? Lookup(typefaces, styledCodepoints[i]) : local; + } } + static FontFamily styleFamily(TextStyle style) => style.Family == FontFamily.Default + ? FontFamily.Roman : style.Family; } + static bool Matches(TypefaceDescriptor descriptor, TextStyle style) => + (descriptor.Family == style.Family || + descriptor.Family == FontFamily.Default && style.Family == FontFamily.Roman || + descriptor.Family == FontFamily.Roman && style.Family == FontFamily.Default) && + descriptor.Weight == style.Weight && descriptor.Posture == style.Posture && + (style.Capitals == FontCapitals.Normal || descriptor.SmallCapitalsTypeface != null || descriptor.SmallCapitalsGlyphMap.Count != 0); public int GetCodepoint(string str, int index) => index + 1 < str.Length && char.IsHighSurrogate(str[index]) diff --git a/CSharpMath.Rendering/BackEnd/TypefaceDescriptor.cs b/CSharpMath.Rendering/BackEnd/TypefaceDescriptor.cs new file mode 100644 index 00000000..39699d23 --- /dev/null +++ b/CSharpMath.Rendering/BackEnd/TypefaceDescriptor.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using CSharpMath.Atom; +using Typography.OpenFont; +using Typography.OpenFont.Extensions; + +namespace CSharpMath.Rendering.BackEnd { + /// Explicit semantic metadata used when selecting a text typeface. + public sealed class TypefaceDescriptor : IEquatable { + public TypefaceDescriptor(Typeface typeface, FontFamily family, FontWeight weight, + FontPosture posture, Typeface smallCapitalsTypeface = null, + IReadOnlyDictionary smallCapitalsGlyphMap = null, + IEnumerable supportedFeatures = null) { + Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); + if (!Enum.IsDefined(typeof(FontFamily), family)) throw new ArgumentOutOfRangeException(nameof(family)); + if (!Enum.IsDefined(typeof(FontWeight), weight)) throw new ArgumentOutOfRangeException(nameof(weight)); + if (!Enum.IsDefined(typeof(FontPosture), posture)) throw new ArgumentOutOfRangeException(nameof(posture)); + Family = family; + Weight = weight; + Posture = posture; + SmallCapitalsTypeface = smallCapitalsTypeface; + var map = new Dictionary(); + foreach (var pair in smallCapitalsGlyphMap ?? new Dictionary()) { + if (pair.Key < 0 || pair.Key > 0x10ffff || pair.Key is >= 0xd800 and <= 0xdfff) + throw new ArgumentOutOfRangeException(nameof(smallCapitalsGlyphMap)); + if (pair.Value == 0 || pair.Value >= (smallCapitalsTypeface ?? typeface).GlyphCount) + throw new ArgumentOutOfRangeException(nameof(smallCapitalsGlyphMap)); + map.Add(pair.Key, pair.Value); + } + SmallCapitalsGlyphMap = new ReadOnlyDictionary(map); + var features = new HashSet(StringComparer.Ordinal); + foreach (var feature in supportedFeatures ?? Enumerable.Empty()) { + if (feature == null || feature.Length != 4 || feature.Any(c => c > 0x7f)) + throw new ArgumentException("Features must be four ASCII characters.", nameof(supportedFeatures)); + features.Add(feature); + } + SupportedFeatures = new ReadOnlyCollection(features.ToArray()); + } + public Typeface Typeface { get; } + public FontFamily Family { get; } + public FontWeight Weight { get; } + public FontPosture Posture { get; } + public Typeface SmallCapitalsTypeface { get; } + public IReadOnlyDictionary SmallCapitalsGlyphMap { get; } + public IReadOnlyCollection SupportedFeatures { get; } + public bool Equals(TypefaceDescriptor other) => other != null && + ReferenceEquals(Typeface, other.Typeface) && Family == other.Family && Weight == other.Weight && + Posture == other.Posture && ReferenceEquals(SmallCapitalsTypeface, other.SmallCapitalsTypeface) && + SmallCapitalsGlyphMap.Count == other.SmallCapitalsGlyphMap.Count && + SmallCapitalsGlyphMap.All(pair => other.SmallCapitalsGlyphMap.TryGetValue(pair.Key, out var value) && value == pair.Value) && + SupportedFeatures.Count == other.SupportedFeatures.Count && + SupportedFeatures.All(other.SupportedFeatures.Contains); + public override bool Equals(object obj) => Equals(obj as TypefaceDescriptor); + public override int GetHashCode() { + unchecked { + var hash = (Typeface, Family, Weight, Posture, SmallCapitalsTypeface).GetHashCode(); + foreach (var pair in SmallCapitalsGlyphMap.OrderBy(pair => pair.Key)) hash = hash * 31 + pair.GetHashCode(); + foreach (var feature in SupportedFeatures.OrderBy(feature => feature, StringComparer.Ordinal)) hash = hash * 31 + feature.GetHashCode(); + return hash; + } + } + + internal static TypefaceDescriptor Adapt(Typeface typeface) { + var flags = typeface.TranslateOS2FontStyle(); + var weight = (flags & TranslatedOS2FontStyle.BOLD) != 0 + ? FontWeight.Bold : FontWeight.Regular; + var posture = (flags & TranslatedOS2FontStyle.OBLIQUE) != 0 + ? FontPosture.Slanted + : (flags & TranslatedOS2FontStyle.ITALIC) != 0 + ? FontPosture.Italic : FontPosture.Upright; + return new TypefaceDescriptor(typeface, FontFamily.Default, weight, posture); + } + } +} diff --git a/CSharpMath.Rendering/PublicAPI/DebugApi/PublicAPI.Unshipped.txt b/CSharpMath.Rendering/PublicAPI/DebugApi/PublicAPI.Unshipped.txt index 28b4dae4..ed99801a 100644 --- a/CSharpMath.Rendering/PublicAPI/DebugApi/PublicAPI.Unshipped.txt +++ b/CSharpMath.Rendering/PublicAPI/DebugApi/PublicAPI.Unshipped.txt @@ -9,3 +9,16 @@ CSharpMath.Rendering.Text.TextAtomListBuilder.Style(CSharpMath.Rendering.Text.Te override CSharpMath.Rendering.Text.TextAtom.RelativeSize.SingleChar(CSharpMath.Atom.FontStyle style) -> int? override CSharpMath.Rendering.Text.TextAtom.RelativeSize.Equals(CSharpMath.Rendering.Text.TextAtom! atom) -> bool override CSharpMath.Rendering.Text.TextAtom.RelativeSize.GetHashCode() -> int +CSharpMath.Rendering.BackEnd.TypefaceDescriptor +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.TypefaceDescriptor(Typography.OpenFont.Typeface! typeface, CSharpMath.Atom.FontFamily family, CSharpMath.Atom.FontWeight weight, CSharpMath.Atom.FontPosture posture, Typography.OpenFont.Typeface! smallCapitalsTypeface = null, System.Collections.Generic.IReadOnlyDictionary! smallCapitalsGlyphMap = null, System.Collections.Generic.IEnumerable! supportedFeatures = null) -> void +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Typeface.get -> Typography.OpenFont.Typeface! +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Family.get -> CSharpMath.Atom.FontFamily +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Weight.get -> CSharpMath.Atom.FontWeight +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Posture.get -> CSharpMath.Atom.FontPosture +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.SmallCapitalsTypeface.get -> Typography.OpenFont.Typeface! +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.SmallCapitalsGlyphMap.get -> System.Collections.Generic.IReadOnlyDictionary! +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.SupportedFeatures.get -> System.Collections.Generic.IReadOnlyCollection! +override CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Equals(object! obj) -> bool +override CSharpMath.Rendering.BackEnd.TypefaceDescriptor.GetHashCode() -> int +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Equals(CSharpMath.Rendering.BackEnd.TypefaceDescriptor! other) -> bool +static CSharpMath.Rendering.BackEnd.Fonts.FromDescriptors(System.Collections.Generic.IEnumerable! descriptors, float pointSize) -> CSharpMath.Rendering.BackEnd.Fonts! diff --git a/CSharpMath.Rendering/PublicAPI/ReleaseApi/PublicAPI.Unshipped.txt b/CSharpMath.Rendering/PublicAPI/ReleaseApi/PublicAPI.Unshipped.txt index a2d6a528..8fc656fb 100644 --- a/CSharpMath.Rendering/PublicAPI/ReleaseApi/PublicAPI.Unshipped.txt +++ b/CSharpMath.Rendering/PublicAPI/ReleaseApi/PublicAPI.Unshipped.txt @@ -9,3 +9,16 @@ override CSharpMath.Rendering.Text.TextAtom.RelativeSize.GetHashCode() -> int CSharpMath.Rendering.Text.TextAtom.Style.Style(CSharpMath.Rendering.Text.TextAtom! content, CSharpMath.Atom.TextStyleChange styleChange) -> void CSharpMath.Rendering.Text.TextAtom.Style.StyleChange.get -> CSharpMath.Atom.TextStyleChange CSharpMath.Rendering.Text.TextAtomListBuilder.Style(CSharpMath.Rendering.Text.TextAtom! atom, CSharpMath.Atom.TextStyleChange styleChange) -> void +CSharpMath.Rendering.BackEnd.TypefaceDescriptor +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.TypefaceDescriptor(Typography.OpenFont.Typeface! typeface, CSharpMath.Atom.FontFamily family, CSharpMath.Atom.FontWeight weight, CSharpMath.Atom.FontPosture posture, Typography.OpenFont.Typeface! smallCapitalsTypeface = null, System.Collections.Generic.IReadOnlyDictionary! smallCapitalsGlyphMap = null, System.Collections.Generic.IEnumerable! supportedFeatures = null) -> void +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Typeface.get -> Typography.OpenFont.Typeface! +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Family.get -> CSharpMath.Atom.FontFamily +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Weight.get -> CSharpMath.Atom.FontWeight +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Posture.get -> CSharpMath.Atom.FontPosture +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.SmallCapitalsTypeface.get -> Typography.OpenFont.Typeface! +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.SmallCapitalsGlyphMap.get -> System.Collections.Generic.IReadOnlyDictionary! +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.SupportedFeatures.get -> System.Collections.Generic.IReadOnlyCollection! +override CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Equals(object! obj) -> bool +override CSharpMath.Rendering.BackEnd.TypefaceDescriptor.GetHashCode() -> int +CSharpMath.Rendering.BackEnd.TypefaceDescriptor.Equals(CSharpMath.Rendering.BackEnd.TypefaceDescriptor! other) -> bool +static CSharpMath.Rendering.BackEnd.Fonts.FromDescriptors(System.Collections.Generic.IEnumerable! descriptors, float pointSize) -> CSharpMath.Rendering.BackEnd.Fonts! diff --git a/CSharpMath.Rendering/Text/TextLaTeXParser.cs b/CSharpMath.Rendering/Text/TextLaTeXParser.cs index f767b9d0..df088396 100644 --- a/CSharpMath.Rendering/Text/TextLaTeXParser.cs +++ b/CSharpMath.Rendering/Text/TextLaTeXParser.cs @@ -510,9 +510,12 @@ public static StringBuilder TextAtomToLaTeX(TextAtom atom, StringBuilder? b = nu } if (change.Family is { } family) wrappers.Add(family == Atom.FontFamily.Default ? "textrm" : family switch { - Atom.FontFamily.Roman => "textrm", Atom.FontFamily.SansSerif => "textsf", - Atom.FontFamily.Monospace => "texttt", Atom.FontFamily.Calligraphic => "textcal", - Atom.FontFamily.Fraktur => "textfrak", Atom.FontFamily.Blackboard => "textbb", + Atom.FontFamily.Roman => "textrm", + Atom.FontFamily.SansSerif => "textsf", + Atom.FontFamily.Monospace => "texttt", + Atom.FontFamily.Calligraphic => "textcal", + Atom.FontFamily.Fraktur => "textfrak", + Atom.FontFamily.Blackboard => "textbb", _ => throw new InvalidCodePathException("Unknown semantic font family.") }); if (change.Weight is { } weight) diff --git a/CSharpMath.Rendering/Text/TextTypesetter.cs b/CSharpMath.Rendering/Text/TextTypesetter.cs index 864fb336..9110b7cc 100644 --- a/CSharpMath.Rendering/Text/TextTypesetter.cs +++ b/CSharpMath.Rendering/Text/TextTypesetter.cs @@ -108,9 +108,13 @@ void FinalizeInlineDisplay(float ascender, float rawDescender, line.Add(display, ascender, -rawDescender, lineGap); } case TextAtom.Text t: + // Glyph fallback may use legacy mathematical Unicode, but the attributed run must + // retain the original source UTF-16 so editor clusters and hit testing remain valid. var renderingStyle = style.ToFontStyleForText(); - var content = UnicodeFontChanger.ChangeFont(t.Content, renderingStyle); - var glyphs = GlyphFinder.Instance.FindGlyphs(fonts, t.Content, renderingStyle).ToList(); + var content = t.Content; + var glyphs = (fonts.UsesDescriptors + ? GlyphFinder.Instance.FindGlyphs(fonts, t.Content, style) + : GlyphFinder.Instance.FindGlyphs(fonts, t.Content, renderingStyle)).ToList(); //Calling Select(g => g.Typeface).Distinct() speeds up query up to 10 times, //Calling Max(Func<,>) instead of Select(Func<,>).Max() speeds up query 2 times var typefaces = glyphs.Select(g => g.Typeface).Distinct().ToList(); From 596a9935610016d3a22d52ce391048f280fa62f3 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Mon, 31 Aug 2026 18:03:22 +0800 Subject: [PATCH 4/4] fix(rendering): synchronize glyph index lookups --- .../TestGlyphFinderConcurrency.cs | 56 +++++++++++++++++++ CSharpMath.Rendering/BackEnd/GlyphFinder.cs | 14 +++-- 2 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 CSharpMath.Rendering.Tests/TestGlyphFinderConcurrency.cs diff --git a/CSharpMath.Rendering.Tests/TestGlyphFinderConcurrency.cs b/CSharpMath.Rendering.Tests/TestGlyphFinderConcurrency.cs new file mode 100644 index 00000000..c188f664 --- /dev/null +++ b/CSharpMath.Rendering.Tests/TestGlyphFinderConcurrency.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CSharpMath.Rendering.BackEnd; +using Typography.OpenFont; +using Xunit; + +namespace CSharpMath.Rendering.Tests { + public class TestGlyphFinderConcurrency { + static Typeface ReadFreshTypeface() { + var resourceName = typeof(Fonts).Assembly.GetManifestResourceNames() + .Single(name => name.EndsWith("latinmodern-math.otf", StringComparison.OrdinalIgnoreCase)); + using var stream = typeof(Fonts).Assembly.GetManifestResourceStream(resourceName); + return new OpenFontReader().Read(stream ?? throw new InvalidOperationException("Embedded math font is missing.")) + ?? throw new InvalidOperationException("Embedded math font is invalid."); + } + + [Fact] + public async Task ConcurrentLookupsOnOneTypefaceAreStable() { + var typeface = ReadFreshTypeface(); + var fonts = new Fonts(new[] { typeface }, 20); + const int workerCount = 8; + const int lookupsPerWorker = 256; + var codepoints = Enumerable.Range('A', workerCount).ToArray(); + var results = new ushort[workerCount * lookupsPerWorker]; + using var cancellation = CancellationTokenSource.CreateLinkedTokenSource( + TestContext.Current.CancellationToken); + cancellation.CancelAfter(TimeSpan.FromSeconds(5)); + using var start = new Barrier(workerCount + 1); + var workers = Enumerable.Range(0, workerCount).Select(worker => + Task.Factory.StartNew(() => { + start.SignalAndWait(cancellation.Token); + var text = char.ConvertFromUtf32(codepoints[worker]); + for (var i = 0; i < lookupsPerWorker; i++) { + cancellation.Token.ThrowIfCancellationRequested(); + var glyph = GlyphFinder.Instance.FindGlyphForCharacterAtIndex(fonts, 0, text); + results[worker * lookupsPerWorker + i] = glyph.Info.GlyphIndex; + } + }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default) + ).ToArray(); + start.SignalAndWait(cancellation.Token); + await Task.WhenAll(workers).WaitAsync(cancellation.Token); + for (var worker = 0; worker < workerCount; worker++) { + var text = char.ConvertFromUtf32(codepoints[worker]); + var stable = GlyphFinder.Instance.FindGlyphForCharacterAtIndex(fonts, 0, text).Info.GlyphIndex; + Assert.NotEqual((ushort)0, stable); + for (var i = 0; i < lookupsPerWorker; i++) { + var result = results[worker * lookupsPerWorker + i]; + Assert.NotEqual((ushort)0, result); + Assert.Equal(stable, result); + } + } + } + } +} diff --git a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs index f3bf2242..2c6a4759 100644 --- a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs +++ b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using CSharpMath.Atom; using Typography.OpenFont; using Typography.OpenFont.Extensions; @@ -8,6 +9,7 @@ namespace CSharpMath.Rendering.BackEnd { public class GlyphFinder : Display.FrontEnd.IGlyphFinder { private GlyphFinder() { } + static readonly ConditionalWeakTable GlyphIndexLocks = new ConditionalWeakTable(); //http://unicode.org/charts/PDF/U25A0.pdf //U+25A1 WHITE SQUARE may be used to represent a missing ideograph //The glyph of this character is in the Latin Modern Math font @@ -18,11 +20,15 @@ public Glyph Lookup(Fonts fonts, int codepoint) { } static Glyph Lookup(IEnumerable typefaces, int codepoint) { foreach (var font in typefaces) { - var g = font.GetGlyphIndex(codepoint); + var g = GetGlyphIndex(font, codepoint); if (g != 0) return new Glyph(font, font.GetGlyph(g)); } return Lookup(typefaces, GlyphNotFound); } + static ushort GetGlyphIndex(Typeface typeface, int codepoint) { + lock (GlyphIndexLocks.GetValue(typeface, _ => new object())) + return typeface.GetGlyphIndex(codepoint); + } static bool IsOrdinary(FontStyle style) => style is FontStyle.Roman or FontStyle.Bold or FontStyle.Italic or FontStyle.BoldItalic; static FontStyle? GetOrdinaryStyle(Typeface typeface) { @@ -53,7 +59,7 @@ internal static IReadOnlyDictionary> BuildLoc Glyph LookupLocalStyle(IReadOnlyDictionary> localStyles, int codepoint, FontStyle style) { if (!localStyles.TryGetValue(style, out var faces)) return Glyph.Empty; foreach (var face in faces) { - var glyph = face.GetGlyphIndex(codepoint); + var glyph = GetGlyphIndex(face, codepoint); if (glyph != 0) return new Glyph(face, face.GetGlyph(glyph)); } return Glyph.Empty; @@ -105,7 +111,7 @@ internal System.Collections.Generic.IEnumerable FindGlyphs(Fonts fonts, s found = true; break; } - var glyphIndex = face.GetGlyphIndex(sourceCodepoints[i]); + var glyphIndex = GetGlyphIndex(face, sourceCodepoints[i]); if (glyphIndex != 0) { yield return new Glyph(face, face.GetGlyph(glyphIndex)); found = true; @@ -117,7 +123,7 @@ internal System.Collections.Generic.IEnumerable FindGlyphs(Fonts fonts, s .Where(d => Matches(d, semanticStyle) || Matches(d, fallbackSemanticStyle))) { var face = semanticStyle.Capitals == FontCapitals.SmallCapitals ? descriptor.SmallCapitalsTypeface ?? descriptor.Typeface : descriptor.Typeface; - var glyphIndex = face.GetGlyphIndex(sourceCodepoints[i]); + var glyphIndex = GetGlyphIndex(face, sourceCodepoints[i]); if (glyphIndex != 0) { yield return new Glyph(face, face.GetGlyph(glyphIndex)); found = true;