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.Tests/TestStyledLocalTypefaces.cs b/CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs new file mode 100644 index 00000000..d8c501f4 --- /dev/null +++ b/CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs @@ -0,0 +1,155 @@ +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(); + } + 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) { + 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); + } + [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.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 40dffef7..9da43cb2 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; @@ -22,18 +23,111 @@ Typeface LoadFont(string fileName) { globalTypefaces.AddSupplement(LoadFont("cyrillic-modern-nmr10.otf")); return globalTypefaces; } + 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. + internal Typeface[] GetLocalTypefacesSnapshot() => localTypefacesAreMutableCollection + ? localTypefaceSource.ToArray() + : 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; - Typefaces = localTypefaces.Concat(GlobalTypefaces); - MathTypeface = Typefaces.First(t => t.HasMathTable()); + 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; + } 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)); + } + 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 { 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(); } -} \ No newline at end of file +} diff --git a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs index dc9509e3..2c6a4759 100644 --- a/CSharpMath.Rendering/BackEnd/GlyphFinder.cs +++ b/CSharpMath.Rendering/BackEnd/GlyphFinder.cs @@ -1,20 +1,150 @@ +using System; +using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; +using CSharpMath.Atom; +using Typography.OpenFont; +using Typography.OpenFont.Extensions; 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 public const char GlyphNotFound = '□'; public static GlyphFinder Instance { get; } = new GlyphFinder(); public Glyph Lookup(Fonts fonts, int codepoint) { - foreach (var font in fonts) { - var g = font.GetGlyphIndex(codepoint); + return Lookup(fonts.GetTypefacesSnapshot(), codepoint); + } + static Glyph Lookup(IEnumerable typefaces, int codepoint) { + foreach (var font in typefaces) { + var g = GetGlyphIndex(font, codepoint); if (g != 0) return new Glyph(font, font.GetGlyph(g)); } - return Lookup(fonts, GlyphNotFound); + 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) { + 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(IReadOnlyDictionary> localStyles, int codepoint, FontStyle style) { + if (!localStyles.TryGetValue(style, out var faces)) return Glyph.Empty; + foreach (var face in faces) { + var glyph = GetGlyphIndex(face, 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 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(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 = GetGlyphIndex(face, 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 = GetGlyphIndex(face, 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]) @@ -27,10 +157,12 @@ 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; } -} \ No newline at end of file +} 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/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.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/TextTypesetter.cs b/CSharpMath.Rendering/Text/TextTypesetter.cs index 4d7e4394..9110b7cc 100644 --- a/CSharpMath.Rendering/Text/TextTypesetter.cs +++ b/CSharpMath.Rendering/Text/TextTypesetter.cs @@ -108,8 +108,13 @@ void FinalizeInlineDisplay(float ascender, float rawDescender, line.Add(display, ascender, -rawDescender, lineGap); } case TextAtom.Text t: - var content = UnicodeFontChanger.ChangeFont(t.Content, style.ToFontStyleForText()); - var glyphs = GlyphFinder.Instance.FindGlyphs(fonts, content); + // 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 = 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(); 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 +}