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
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);
}
}
}
34 changes: 29 additions & 5 deletions CSharpMath.Rendering/BackEnd/Fonts.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using CSharpMath.Atom;
using Typography.OpenFont;
using Typography.OpenFont.Extensions;

Expand All @@ -22,18 +23,41 @@ Typeface LoadFont(string fileName) {
globalTypefaces.AddSupplement(LoadFont("cyrillic-modern-nmr10.otf"));
return globalTypefaces;
}
readonly IEnumerable<Typeface> localTypefaceSource;
readonly Typeface[] localTypefaceSnapshot;
readonly bool localTypefacesAreMutableCollection;
internal IEnumerable<Typeface> 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<Typeface> localTypefaces, float pointSize) {
PointSize = pointSize;
Typefaces = localTypefaces.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<Typeface> || localTypefaces is IReadOnlyCollection<Typeface>) {
localTypefaceSource = localTypefaces ?? Enumerable.Empty<Typeface>();
localTypefaceSnapshot = null;
localTypefacesAreMutableCollection = true;
} else {
localTypefaceSnapshot = (localTypefaces ?? Enumerable.Empty<Typeface>()).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; }
public IEnumerable<Typeface> Typefaces { get; }
public IEnumerable<Typeface> Typefaces => GetTypefacesSnapshot();
public Typeface MathTypeface { get; }
public Typography.OpenFont.MathGlyphs.MathConstants MathConsts { get; }
public IEnumerator<Typeface> GetEnumerator() => Typefaces.GetEnumerator();
public IEnumerator<Typeface> GetEnumerator() => GetTypefacesSnapshot().AsEnumerable().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Typefaces.GetEnumerator();
}
}
}
70 changes: 64 additions & 6 deletions CSharpMath.Rendering/BackEnd/GlyphFinder.cs
Original file line number Diff line number Diff line change
@@ -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<Fonts, Glyph> {
Expand All @@ -9,11 +14,62 @@ 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<Typeface> 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;
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<FontStyle, IReadOnlyList<Typeface>> BuildLocalStyleLookup(
IReadOnlyList<Typeface> localTypefaces) {
var result = new Dictionary<FontStyle, IReadOnlyList<Typeface>>();
var byStyle = new Dictionary<FontStyle, List<Typeface>>();
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<Typeface>();
faces.Add(face);
}
}
foreach (var pair in byStyle)
result[pair.Key] = pair.Value;
return result;
}
Glyph LookupLocalStyle(IReadOnlyDictionary<FontStyle, IReadOnlyList<Typeface>> 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));
}
return Glyph.Empty;
}
/// <summary>Find ordinary text glyphs in a matching local family before applying mathematical Unicode styling.</summary>
internal System.Collections.Generic.IEnumerable<Glyph> 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(localStyles, sourceCodepoints[i], style) : Glyph.Empty;
yield return local.IsEmpty ? Lookup(typefaces, styledCodepoints[i]) : local;
}
}
public int GetCodepoint(string str, int index) =>
index + 1 < str.Length
Expand All @@ -27,10 +83,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<Glyph> FindGlyphs(Fonts fonts, string str) =>
Typography.OpenFont.StringUtils.GetCodepoints(str.ToCharArray())
.Select(c => Lookup(fonts, c));
public System.Collections.Generic.IEnumerable<Glyph> 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;
}
}
}
7 changes: 6 additions & 1 deletion CSharpMath.Rendering/FrontEnd/ICSharpMathAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public interface ICSharpMathAPI<TContent, TColor> where TContent : class {
#region Display-recreating properties
/// <summary>Unit of measure: points</summary>
float FontSize { get; set; }
/// <summary>
/// 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.
/// </summary>
System.Collections.Generic.IEnumerable<Typeface> LocalTypefaces { get; set; }
Atom.LineStyle LineStyle { get; set; }
TContent? Content { get; set; }
Expand Down Expand Up @@ -53,4 +58,4 @@ public static PointF GetDisplayPosition(
return new PointF(x + offsetX, y + offsetY - height);
}
}
}
}
3 changes: 2 additions & 1 deletion CSharpMath.Rendering/FrontEnd/Painter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public Painter() {
/// <summary>Unit of measure: points</summary>
public float FontSize { get => Fonts.PointSize; set { Fonts = new Fonts(Fonts, value); SetRedisplay(); } }
IEnumerable<Typeface> __localTypefaces = Array.Empty<Typeface>();
/// <inheritdoc cref="ICSharpMathAPI{TContent, TColor}.LocalTypefaces" />
public IEnumerable<Typeface> 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(); } }
Expand Down Expand Up @@ -132,4 +133,4 @@ GlyphBoxColor is var (glyph, textRun) ? Nullable((WrapColor(glyph), WrapColor(te
public Painter<TCanvas, TContent, TColor> ShallowClone() => (Painter<TCanvas, TContent, TColor>)MemberwiseClone();
#endregion Methods
}
}
}
4 changes: 2 additions & 2 deletions CSharpMath.Rendering/Text/TextTypesetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -216,4 +216,4 @@ void FinalizeInlineDisplay(float ascender, float rawDescender,
return (new Display(relativePositionList), new Display(absolutePositionList));
}
}
}
}
3 changes: 2 additions & 1 deletion CSharpMath.Xaml/Views.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ protected override void RenderOverride(XCanvas canvas, Windows.Foundation.Size a
/// <summary>Unit of measure: points; Defaults to <see cref="FontSize"/>.</summary>
public float? ErrorFontSize { get => (float?)GetValue(ErrorFontSizeProperty); set => SetValue(ErrorFontSizeProperty, value); }
public static readonly XProperty ErrorFontSizeProperty;
/// <inheritdoc cref="CSharpMath.Rendering.FrontEnd.ICSharpMathAPI{TContent, TColor}.LocalTypefaces" />
public IEnumerable<Typeface> LocalTypefaces { get => (IEnumerable<Typeface>)GetValue(LocalTypefacesProperty)!; set => SetValue(LocalTypefacesProperty, value); }
public static readonly XProperty LocalTypefacesProperty;
public XColor TextColor { get => (XColor)GetValue(TextColorProperty)!; set => SetValue(TextColorProperty, value); }
Expand Down Expand Up @@ -321,4 +322,4 @@ protected override void RenderOverride(XCanvas canvas, Windows.Foundation.Size a
}
public partial class MathView : BaseView<MathPainter, MathList> { }
public partial class TextView : BaseView<TextPainter, Rendering.Text.TextAtom> { }
}
}
Loading