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
55 changes: 55 additions & 0 deletions CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,61 @@ public void ControlWordDoesNotConsumeFollowingStar() {
Assert.Equal(@"\sin *", LaTeXParser.MathListToLaTeX(list).ToString());
}

[Fact]
public void MathRelAndJoinRelAreStructuralAndRoundTrip() {
var list = ParseLaTeX(@"a\mathrel{|}\joinrel=b");
Assert.Collection(list,
atom => Assert.IsType<Variable>(atom),
atom => {
var rel = Assert.IsType<MathRel>(atom);
Assert.Collection(rel.InnerList, inner => Assert.IsType<Ordinary>(inner));
},
atom => Assert.IsType<JoinRel>(atom),
atom => Assert.IsType<Relation>(atom),
atom => Assert.IsType<Variable>(atom));
Assert.Equal(@"a\mathrel{|}\joinrel =b", LaTeXParser.MathListToLaTeX(list).ToString());
var reparsed = ParseLaTeX(LaTeXParser.MathListToLaTeX(list).ToString());
Assert.True(list.NullCheckingStructuralEquality(reparsed));
}

[Theory]
[InlineData(@"\mathrel{", "Missing closing brace")]
public void MathRelReportsMalformedArguments(string input, string expected) {
var (_, error) = new LaTeXParser(input).Build();
Assert.Equal(expected, error);
}

[Fact]
public void MathRelPreservesNestedContentAndScripts() {
var list = ParseLaTeX(@"\mathrel{\left( x^2 \right)}_i");
var rel = Assert.IsType<MathRel>(Assert.Single(list));
Assert.Single(rel.Subscript);
var inner = Assert.IsType<Inner>(Assert.Single(rel.InnerList));
Assert.Equal("(", inner.LeftBoundary.Nucleus);
Assert.Equal(")", inner.RightBoundary.Nucleus);
var canonical = LaTeXParser.MathListToLaTeX(list).ToString();
Assert.True(list.NullCheckingStructuralEquality(ParseLaTeX(canonical)));
}

[Fact]
public void JoinRelIsNotADisplayedNodeDuringCloneNormalization() {
var list = ParseLaTeX(@"a+\joinrel=b").Clone(true);
Assert.Collection(list,
atom => Assert.IsType<Variable>(atom),
atom => Assert.IsType<UnaryOperator>(atom),
atom => Assert.IsType<JoinRel>(atom),
atom => Assert.IsType<Relation>(atom),
atom => Assert.IsType<Variable>(atom));
}

[Fact]
public void JoinRelPreservesScriptsStructurally() {
var list = ParseLaTeX(@"\joinrel^x");
var join = Assert.IsType<JoinRel>(Assert.Single(list));
Assert.Single(join.Superscript);
Assert.True(list.NullCheckingStructuralEquality(ParseLaTeX(LaTeXParser.MathListToLaTeX(list).ToString())));
}

/// new[] { Base list }, new[] { Script of first atom }, new[] { Script of first atom inside script of first atom }
[Theory]
[InlineData("x^2", "x^2", new[] { typeof(Variable) }, new[] { typeof(Number) })]
Expand Down
102 changes: 101 additions & 1 deletion CSharpMath.Core.Tests/Display/TypesetterTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using CSharpMath.Atom;
Expand All @@ -10,6 +11,105 @@

namespace CSharpMath.Core.DisplayTests {
public class TypesetterTests {
[Theory]
[InlineData(LineStyle.Display)]
[InlineData(LineStyle.Text)]
[InlineData(LineStyle.Script)]
[InlineData(LineStyle.ScriptScript)]
public void JoinRelContributesExactlyNegativeThreeMu(LineStyle style) {
var plain = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX("xx"), _font, _context, style);
var joined = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(@"x\joinrel x"), _font, _context, style);
var styleFont = new TFont(_context.MathTable.GetStyleSize(style, _font));
Approximately.Equal(-3 * _context.MathTable.MuUnit(styleFont), joined.Width - plain.Width);
}

[Fact]
public void MathRelGetsRelationSpacingExactlyOnceAndKeepsInternalLayout() {
var ordinary = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX("a=b"), _font, _context, LineStyle.Display);
var wrapped = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(@"a\mathrel{x}b"), _font, _context, LineStyle.Display);
Approximately.Equal(ordinary.Width, wrapped.Width);
var inner = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX("x+y"), _font, _context, LineStyle.Display);
var wrappedInner = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(@"\mathrel{x+y}"), _font, _context, LineStyle.Display);
Approximately.Equal(inner.Width, wrappedInner.Width);
}

[Fact]
public void MathRelAtFormulaBoundaryAndWithScriptsRemainsMeasured() {
var standalone = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(@"\mathrel{x}"), _font, _context, LineStyle.Display);
var x = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX("x"), _font, _context, LineStyle.Display);
Approximately.Equal(x.Width, standalone.Width);
var scripted = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(@"\mathrel{x}^2"), _font, _context, LineStyle.Display);
Assert.Contains(scripted.Displays, d => d.HasScript);
Assert.True(scripted.Width >= standalone.Width);
}

[Fact]
public void JoinRelScriptHasAVisibleScriptAnchor() {
var display = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(@"\joinrel^x"), _font, _context, LineStyle.Display);
Assert.Contains(display.Displays, d => d.HasScript);
Assert.True(display.Width > 0);
}

[Theory]
[InlineData(@"\joinrel x", 6.6666667f)]
[InlineData(@"x\joinrel", 6.6666667f)]
[InlineData(@"\joinrel", -3.3333333f)]
public void JoinRelKeepsLogicalAdvanceAtBoundaries(string latex, float expectedLogicalWidth) {
var display = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(latex), _font, _context, LineStyle.Display);
Approximately.Equal(expectedLogicalWidth, display.LogicalWidth);
Assert.True(display.Width >= 0);
if (latex == @"\joinrel x")
Approximately.Equal(-10f / 3f, display.InkLeft);
}

[Fact]
public void NestedMathRelPropagatesJoinRelLogicalAdvance() {
var display = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(@"\mathrel{\joinrel x}"), _font, _context, LineStyle.Display);
Approximately.Equal(6.6666667, display.LogicalWidth);
Assert.True(display.Width >= display.LogicalWidth);
}

[Fact]
public void JoinRelInkIsPreservedThroughOverlineWrapper() {
var display = ParseLaTeXToDisplay(@"\overline{\joinrel x}");
Assert.Contains(display.Displays, d => d is OverOrUnderlineDisplay<TFont, TGlyph>);
Assert.True(display.InkLeft < 0);
}

[Fact]
public void JoinRelInkIsPreservedThroughLargeOperatorLimits() {
var display = ParseLaTeXToDisplay(@"\sum\limits_{\joinrel x}");
Assert.Contains(display.Displays, d => d is LargeOpLimitsDisplay<TFont, TGlyph>);
Assert.True(display.InkLeft < 0);
}

[Fact]
public void JoinRelInkIsPreservedThroughTableContainer() {
var display = ParseLaTeXToDisplay(@"\begin{matrix}\joinrel x & y\\ z & w\end{matrix}");
Assert.True(display.HasJoinRel());
Assert.True(display.InkLeft < 0);
}

[Fact]
public void ListDisplaySnapshotsMutableChildrenForGeometryAndProvenance() {
var joined = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(@"\joinrel x"), _font, _context, LineStyle.Display);
var ordinary = Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX("x"), _font, _context, LineStyle.Display);
var children = new List<IDisplay<TFont, TGlyph>> { joined };
var snapshot = new ListDisplay<TFont, TGlyph>(children);
var width = snapshot.Width;

children[0] = ordinary;
children.Add(ordinary);

Assert.Single(snapshot.Displays);
Assert.Same(joined, snapshot.Displays[0]);
Approximately.Equal(width, snapshot.Width);
Assert.True(snapshot.HasJoinRel());
Assert.False(snapshot.Displays is System.Array);
Assert.False(snapshot.Displays is List<IDisplay<TFont, TGlyph>>);
var readOnlyView = Assert.IsAssignableFrom<IList<IDisplay<TFont, TGlyph>>>(snapshot.Displays);
Assert.Throws<System.NotSupportedException>(() => readOnlyView[0] = ordinary);
}
internal static ListDisplay<TFont, TGlyph> ParseLaTeXToDisplay(string latex) =>
Typesetter.CreateLine(AtomTests.LaTeXParserTest.ParseLaTeX(latex), _font, _context, LineStyle.Display);

Expand Down Expand Up @@ -673,4 +773,4 @@ public void SpacingBetweenNumbers() {
});
}
}
}
}
71 changes: 71 additions & 0 deletions CSharpMath.Rendering.Tests/TestAngouriMathForms.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using System.Linq;
using SkiaSharp;
using Xunit;

namespace CSharpMath.Rendering.Tests {
Expand All @@ -8,6 +11,7 @@ namespace CSharpMath.Rendering.Tests {
/// <see cref="TestCommandDisplay.CommandsAreDisplayable"/>; what is new is the layout, so this
/// measures rather than comparing against a baseline image.
/// </summary>
[Collection(nameof(TestRenderingFixture))]
public class TestAngouriMathForms {
static System.Drawing.RectangleF Measure(string latex) {
var painter = new SkiaSharp.MathPainter { LaTeX = latex };
Expand All @@ -33,5 +37,72 @@ public void TheyLayOut(string latex) {
[Fact]
public void ModuloTakesUpRoom() =>
Assert.True(Measure(@"x\bmod y").Width > Measure(@"xy").Width * 2);

[Fact]
public void MathRelAndJoinRelMeasureWithoutClipping() {
var wrapped = Measure(@"a\mathrel{|}\joinrel=b");
var relation = Measure("a=b");
Assert.True(wrapped.Width > 0 && wrapped.Height > 0);
Assert.True(relation.Width > 0 && relation.Height > 0);
Assert.Null(new SkiaSharp.MathPainter { LaTeX = @"\mathrel{\left( x\right)}" }.ErrorMessage);
}

[Fact]
public void UnaffectedMathKeepsLegacyMeasureOriginAndAdvance() {
var relation = Measure("a=b");
var wrapped = Measure(@"\mathrel{x+y}");
Assert.Equal(0, relation.X);
Assert.Equal(0, wrapped.X);
Assert.True(relation.Width > 0 && wrapped.Width > 0);
}

[Theory]
[InlineData(@"\joinrel x")]
[InlineData(@"x\joinrel")]
[InlineData(@"\mathrel{\left(\joinrel x\right)}")]
[InlineData(@"\frac{\joinrel x}{x}")]
[InlineData(@"\sqrt{\joinrel x}")]
[InlineData(@"\bar{\joinrel x}")]
public void SkiaTightCanvasContainsInkAtBothEdges(string latex) {
var painter = new SkiaSharp.MathPainter { LaTeX = latex };
var measure = painter.Measure(1000);
// Allocate only the measured ink span, with a one-pixel safety edge. This catches
// both clipping and accidental double compensation of a negative ink origin.
var width = Math.Max(1, (int)Math.Ceiling(measure.Width) + 2);
var height = Math.Max(1, (int)Math.Ceiling(measure.Height) + 2);
using var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
painter.Draw(canvas, 1, height - 1 - painter.Display!.Descent);
var occupied = Enumerable.Range(0, width).Where(x =>
Enumerable.Range(0, height).Any(y => bitmap.GetPixel(x, y).Alpha > 0)).ToArray();
Assert.NotEmpty(occupied);
Assert.True(occupied.First() >= 1);
Assert.True(occupied.Last() <= width - 2);
Assert.True(occupied.Last() > occupied.First());
}

[Theory]
[InlineData(@"\joinrel x")]
[InlineData(@"x\joinrel")]
public void DirectBoundaryJoinRelMatchesPlainX(string latex) {
static (int width, int height, int left, int right, int top, int bottom) Render(string source) {
var painter = new SkiaSharp.MathPainter { LaTeX = source };
var measure = painter.Measure(1000);
var width = Math.Max(1, (int)Math.Ceiling(measure.Width) + 2);
var height = Math.Max(1, (int)Math.Ceiling(measure.Height) + 2);
using var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
painter.Draw(canvas, 1, height - 1 - painter.Display!.Descent);
var pixels = Enumerable.Range(0, width)
.SelectMany(x => Enumerable.Range(0, height).Select(y => (x, y)))
.Where(p => bitmap.GetPixel(p.x, p.y).Alpha > 0).ToArray();
Assert.NotEmpty(pixels);
return (width, height, pixels.Min(p => p.x), pixels.Max(p => p.x),
pixels.Min(p => p.y), pixels.Max(p => p.y));
}
Assert.Equal(Render("x"), Render(latex));
}
}
}
92 changes: 91 additions & 1 deletion CSharpMath.Rendering.Tests/TestRendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SkiaSharp;
using Xunit;

namespace CSharpMath.Rendering.Tests {
Expand Down Expand Up @@ -74,6 +75,95 @@ public void MathDisplay(string file, string latex) =>
[Theory, ClassData(typeof(TestRenderingMathData))]
public void MathInline(string file, string latex) =>
Run(file, latex, new TMathPainter { LineStyle = Atom.LineStyle.Text });

[Theory]
[InlineData(@"\joinrel x")]
[InlineData(@"x\joinrel")]
[InlineData(@"\mathrel{\left(\joinrel x\right)}")]
[InlineData(@"\bar{\joinrel x}")]
public void JoinRelInkFitsInMeasuredImage(string latex) {
var painter = new TMathPainter { LaTeX = latex };
Assert.Null(painter.ErrorMessage);
var measure = painter.Measure(1000);
Assert.True(measure.Width > 0 && measure.Height > 0);

using var stream = new MemoryStream();
DrawToStream(painter, stream, 1000, TextAlignment.TopLeft);
stream.Position = 0;
using var bitmap = SKBitmap.Decode(stream);
Assert.NotNull(bitmap);
var pixels = Enumerable.Range(0, bitmap.Width)
.SelectMany(x => Enumerable.Range(0, bitmap.Height).Select(y => (x, y)))
.Where(p => bitmap.GetPixel(p.x, p.y).Alpha > 0)
.ToArray();
Assert.NotEmpty(pixels);
Assert.InRange(pixels.Min(p => p.x), 0, bitmap.Width - 1);
Assert.InRange(pixels.Max(p => p.x), 0, bitmap.Width - 1);
Assert.InRange(pixels.Min(p => p.y), 0, bitmap.Height - 1);
Assert.InRange(pixels.Max(p => p.y), 0, bitmap.Height - 1);
// DrawAsStream/DrawAsPng use the measured dimensions cast to int (floor).
Assert.Equal(Math.Max(1, (int)measure.Width), bitmap.Width);
Assert.Equal(Math.Max(1, (int)measure.Height), bitmap.Height);
}

[Theory]
[InlineData(@"\joinrel x")]
[InlineData(@"x\joinrel")]
public void BoundaryJoinRelHasSameRasterAsPlainX(string latex) {
(int width, int height, int left, int right, int top, int bottom) Render(string source) {
var painter = new TMathPainter { LaTeX = source };
using var stream = new MemoryStream();
DrawToStreamForContract(painter, stream);
stream.Position = 0;
using var bitmap = SKBitmap.Decode(stream);
Assert.NotNull(bitmap);
var pixels = Enumerable.Range(0, bitmap!.Width)
.SelectMany(x => Enumerable.Range(0, bitmap.Height).Select(y => (x, y)))
.Where(p => bitmap.GetPixel(p.x, p.y).Alpha > 0).ToArray();
Assert.NotEmpty(pixels);
return (bitmap.Width, bitmap.Height, pixels.Min(p => p.x), pixels.Max(p => p.x),
pixels.Min(p => p.y), pixels.Max(p => p.y));
}
var plain = Render("x");
var joined = Render(latex);
Assert.Equal(plain, joined);
}

[Theory]
[InlineData(TextAlignment.TopLeft)]
[InlineData(TextAlignment.Top)]
[InlineData(TextAlignment.TopRight)]
public void TextPainterJoinRelInkSurvivesTextTypesetterRoots(TextAlignment alignment) {
const float canvasWidth = 240;
var painter = new TTextPainter { LaTeX = "prefix $\\joinrel x$ suffix" };
Assert.Null(painter.ErrorMessage);
var measure = painter.Measure(canvasWidth);
Assert.True(measure.Width > 0 && measure.Height > 0);
Assert.True(measure.Width >= painter.Display!.Width);

using var stream = new MemoryStream();
DrawToStream(painter, stream, canvasWidth, alignment);
stream.Position = 0;
using var bitmap = SKBitmap.Decode(stream);
Assert.NotNull(bitmap);
var pixels = Enumerable.Range(0, bitmap!.Width)
.SelectMany(x => Enumerable.Range(0, bitmap.Height).Select(y => (x, y)))
.Where(p => bitmap.GetPixel(p.x, p.y).Alpha > 0)
.ToArray();
Assert.NotEmpty(pixels);
Assert.InRange(pixels.Min(p => p.x), 0, bitmap.Width - 1);
Assert.InRange(pixels.Max(p => p.x), 0, bitmap.Width - 1);
}

[Fact]
public void TextPainterWithoutJoinRelKeepsLegacyMeasuredWidth() {
var painter = new TTextPainter { LaTeX = "prefix $x$ suffix" };
var measure = painter.Measure(240);
Assert.Equal(painter.Display!.Width, measure.Width);
}

private void DrawToStreamForContract(TMathPainter painter, Stream stream) =>
DrawToStream(painter, stream, 1000, TextAlignment.TopLeft);
[Theory, ClassData(typeof(TestRenderingTextData))]
public void TextLeft(string file, string latex) =>
Run(file, latex, new TTextPainter());
Expand Down Expand Up @@ -202,4 +292,4 @@ public virtual void MathPainterSettings(string file, TMathPainter painter) =>
public void TextPainterSettings(string file, TTextPainter painter) =>
Run(file, @"Inline \color{red}{Maths}: $\int_{a_1^2}^{a_2^2}\color{green}\sqrt\frac x2dx$Display \color{red}{Maths}: $$\int_{a_1^2}^{a_2^2}\color{green}\sqrt\frac x2dx$$", painter);
}
}
}
Loading
Loading