Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions CSharpMath.Rendering.Tests/TestGlyphFinderConcurrency.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
155 changes: 155 additions & 0 deletions CSharpMath.Rendering.Tests/TestStyledLocalTypefaces.cs
Original file line number Diff line number Diff line change
@@ -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<Typeface> {
readonly IReadOnlyList<Typeface> _faces;
public OneShotTypefaceEnumerable(IReadOnlyList<Typeface> faces) => _faces = faces;
public int EnumerationCount { get; private set; }
public IEnumerator<Typeface> 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<Typeface> {
readonly List<Typeface> _faces;
public CountingTypefaceCollection(params Typeface[] faces) => _faces = new List<Typeface>(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<Typeface> 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<Fonts, RenderingGlyph>)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<TextRunDisplay<Fonts, RenderingGlyph>>().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<Fonts, RenderingGlyph>)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<Typeface> { 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<Fonts, RenderingGlyph>)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<Fonts, RenderingGlyph>)display.Displays.Single()).Run.Glyphs.Single();
Assert.Same(bold, glyph.Typeface);
Assert.Equal(captureCountBeforeFind + 1, source.CaptureCount);
}
}
}
Loading
Loading