From fa6f58eaba11b24ebcaa8a53f71f86ab37a55260 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Mon, 31 Aug 2026 02:44:41 +0800 Subject: [PATCH 1/2] 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/2] 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 +}