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
Binary file modified CSharpMath.Rendering.Tests/MathDisplay/Abs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified CSharpMath.Rendering.Tests/MathDisplay/Cases.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified CSharpMath.Rendering.Tests/MathDisplay/SimpleShortProof.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified CSharpMath.Rendering.Tests/MathInline/Abs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified CSharpMath.Rendering.Tests/MathInline/Cases.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified CSharpMath.Rendering.Tests/MathInline/SimpleShortProof.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 30 additions & 2 deletions CSharpMath.Rendering.Tests/TestMeasure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,32 @@ class D : Display.IDisplay<Fonts, Glyph> {
public float Descent => 3;
public float Width => 10;

public PointF Position { get => PointF.Empty; set => throw new NotImplementedException(); }
public virtual PointF Position { get => PointF.Empty; set => throw new NotImplementedException(); }
public Atom.Range Range => throw new NotImplementedException();
public Color? TextColor { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public Color? BackColor { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public bool HasScript { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public void Draw(IGraphicsContext<Fonts, Glyph> context) => throw new NotImplementedException();
public void SetTextColorRecursive(Color? textColor) => throw new NotImplementedException();
}
sealed class PositionedD : D {
PointF _position;
public override PointF Position { get => _position; set => _position = value; }
}
class DKeyboard : Editor.MathKeyboard<Fonts, Glyph> {
public DKeyboard() : base(TypesettingContext.Instance, new Fonts(Enumerable.Empty<Typography.OpenFont.Typeface>(), 0.0f)) =>
Display = new Display.Displays.ListDisplay<Fonts, Glyph>(new[] { new D() });
}
class DRenderingMath : SkiaSharp.MathPainter {
public DRenderingMath() =>
Display = new Display.Displays.ListDisplay<Fonts, Glyph>(new[] { new D() });
public void SetTestDisplay(Display.IDisplay<Fonts, Glyph> display) => Display = display;
protected override void UpdateDisplayCore(float unused) { }
}
class DRenderingText : SkiaSharp.TextPainter {
public DRenderingText() =>
Display = new Display.Displays.ListDisplay<Fonts, Glyph>(new[] { new D() });
public void SetTestDisplay(Display.IDisplay<Fonts, Glyph> display) => Display = display;
protected override void UpdateDisplayCore(float canvasWidth) { }
}
class DRenderingKeyboard : FrontEnd.MathKeyboard {
Expand All @@ -57,5 +63,27 @@ public void RenderingMeasure_YIsNegAscent() {
Assert.Equal(new RectangleF(0, -12, 10, 15), new DRenderingText().Measure(float.NaN));
Assert.Equal(new RectangleF(0, -12, 10, 15), new DRenderingKeyboard().Measure);
}

[Fact]
public void RenderingMeasure_RecursivelyUnionsPositionedRows() {
var left = new PositionedD { Position = new PointF(-4, 0) };
var right = new PositionedD { Position = new PointF(12, 0) };
var painter = new DRenderingText();
painter.SetTestDisplay(new Display.Displays.ListDisplay<Fonts, Glyph>(new Display.IDisplay<Fonts, Glyph>[] {
new Display.Displays.ListDisplay<Fonts, Glyph>(new Display.IDisplay<Fonts, Glyph>[] { left }),
new Display.Displays.ListDisplay<Fonts, Glyph>(new Display.IDisplay<Fonts, Glyph>[] { right }) {
Position = new PointF(0, -20)
}
}));
Assert.Equal(new RectangleF(-4, -12, 26, 35), painter.Measure(float.NaN));
var mathPainter = new DRenderingMath();
mathPainter.SetTestDisplay(new Display.Displays.ListDisplay<Fonts, Glyph>(new Display.IDisplay<Fonts, Glyph>[] {
new Display.Displays.ListDisplay<Fonts, Glyph>(new Display.IDisplay<Fonts, Glyph>[] { left }),
new Display.Displays.ListDisplay<Fonts, Glyph>(new Display.IDisplay<Fonts, Glyph>[] { right }) {
Position = new PointF(0, -20)
}
}));
Assert.Equal(new RectangleF(-4, -12, 26, 35), mathPainter.Measure());
}
}
}
}
166 changes: 166 additions & 0 deletions CSharpMath.Rendering.Tests/TestMultilineBounds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System;
using System.Drawing;
using System.Linq;
using CSharpMath.Display;
using CSharpMath.Rendering;
using CSharpMath.Rendering.BackEnd;
using CSharpMath.Rendering.FrontEnd;
using CSharpMath.SkiaSharp;
using SkiaSharp;
using Xunit;

namespace CSharpMath.Rendering.Tests {
public class TestMultilineBounds {
const string Maxwell = @"\begin{aligned}\nabla \cdot \vec{\bf E} & = \frac {\rho} {\varepsilon_0} \\ \nabla \cdot \vec{\bf B} & = 0 \\ \nabla \times \vec{\bf E} &= - \frac{\partial\vec{\bf B}}{\partial t} \\ \nabla \times \vec{\bf B} & = \mu_0\vec{\bf J} + \mu_0\varepsilon_0 \frac{\partial\vec{\bf E}}{\partial t}\end{aligned}";
const string EqnArrayMath = @"\begin{eqnarray}a+b+c+d+e+f&=&a+b+c+d+e+f\\0 &=& a+b+c+d+e+f-a-b-c-d-e-f\end{eqnarray}";
const string EqnArrayText = @"$$\begin{eqnarray}a+b+c+d+e+f&=&a+b+c+d+e+f\\0 &=& a+b+c+d+e+f-a-b-c-d-e-f\end{eqnarray}$$";

[Theory]
[InlineData(Maxwell)]
[InlineData(EqnArrayMath)]
[InlineData(@"\begin{array}{cc}a&b\\c&d\end{array}")]
[InlineData(@"\begin{matrix}1&2\\3&4\end{matrix}")]
public void MultilineSourcesHaveFiniteAggregateMeasure(string source) {
var painter = new SkiaSharp.MathPainter { FontSize = 32, LaTeX = source };
var bounds = painter.Measure(600);
Assert.Null(painter.ErrorMessage);
Assert.True(bounds.Width > 0 && bounds.Height > 0);
Assert.True(float.IsFinite(bounds.Left) && float.IsFinite(bounds.Top));
}

[Fact]
public void MeasureDoesNotChangeLaterDrawLayoutWidth() {
var afterMeasure = new SkiaSharp.TextPainter { FontSize = 32, LaTeX = "line one line two" };
afterMeasure.Measure(100);
using var first = new SKBitmap(1000, 1000);
using var firstCanvas = new SKCanvas(first);
afterMeasure.Draw(firstCanvas, TextAlignment.TopLeft);

var direct = new SkiaSharp.TextPainter { FontSize = 32, LaTeX = "line one line two" };
using var second = new SKBitmap(1000, 1000);
using var secondCanvas = new SKCanvas(second);
direct.Draw(secondCanvas, TextAlignment.TopLeft);
Assert.Equal(direct._relativeXCoordDisplay.Position, afterMeasure._relativeXCoordDisplay.Position);
}

[Fact]
public void DrawIsStatelessAcrossRepeatedCalls() {
var painter = new SkiaSharp.TextPainter { FontSize = 32, LaTeX = "line one line two" };
using var bitmap = new SKBitmap(1000, 1000);
using var canvas = new SKCanvas(bitmap);
painter.Draw(canvas, TextAlignment.TopLeft);
var first = painter._relativeXCoordDisplay.Position;
painter.Draw(canvas, TextAlignment.TopLeft);
Assert.Equal(first, painter._relativeXCoordDisplay.Position);
}

[Theory]
[InlineData(TextAlignment.TopLeft)]
[InlineData(TextAlignment.Top)]
public void DrawAsStreamDimensionsAndEdgesContainInk(TextAlignment alignment) {
var painter = new SkiaSharp.MathPainter { FontSize = 32, LaTeX = Maxwell };
using var stream = painter.DrawAsStream(600, alignmentForTests: alignment);
using var bitmap = SKBitmap.Decode(stream);
Assert.True(bitmap.Width > 0 && bitmap.Height > 0);
var pixels = Enumerable.Range(0, bitmap.Width).SelectMany(x =>
Enumerable.Range(0, bitmap.Height).Select(y => (x, y, a: bitmap.GetPixel(x, y).Alpha)))
.Where(p => p.a > 0).ToArray();
Assert.NotEmpty(pixels);
Assert.InRange(pixels.Min(p => p.x), 1, bitmap.Width - 2);
Assert.InRange(pixels.Max(p => p.x), 1, bitmap.Width - 2);
Assert.InRange(pixels.Min(p => p.y), 1, bitmap.Height - 2);
Assert.InRange(pixels.Max(p => p.y), 1, bitmap.Height - 2);
}

[Fact]
public void EqnArrayStreamContainsTheCompleteComposite() {
var painter = new SkiaSharp.TextPainter { FontSize = 32, LaTeX = EqnArrayText };
using var stream = painter.DrawAsStream(600);
using var bitmap = SKBitmap.Decode(stream);
var pixels = Enumerable.Range(0, bitmap.Width).SelectMany(x =>
Enumerable.Range(0, bitmap.Height).Select(y => (x, y, a: bitmap.GetPixel(x, y).Alpha)))
.Where(p => p.a > 0).ToArray();
Assert.NotEmpty(pixels);
Assert.InRange((pixels.Min(p => p.x) + pixels.Max(p => p.x)) / 2f,
bitmap.Width / 2f - 2, bitmap.Width / 2f + 2);
Assert.InRange(pixels.Min(p => p.x), 1, bitmap.Width - 2);
Assert.InRange(pixels.Max(p => p.x), 1, bitmap.Width - 2);
}

[Theory]
[InlineData(600f)]
[InlineData(float.NaN)]
[InlineData(float.PositiveInfinity)]
public void TextPainterCentersTheCompleteEqnArrayAtFiniteAndUnboundedWidths(float width) {
var painter = new SkiaSharp.TextPainter { FontSize = 32, LaTeX = EqnArrayText };
var measure = painter.Measure(width);
Assert.True(float.IsFinite(measure.Left) && float.IsFinite(measure.Top));
Assert.True(float.IsFinite(measure.Width) && float.IsFinite(measure.Height));
var display = Assert.Single(painter._absoluteXCoordDisplay.Displays);
var bounds = DisplayInkBounds.Get(display).Plus(display.Position);
if (float.IsFinite(width))
Assert.Equal(width / 2, bounds.Left + bounds.Width / 2, precision: 3);
else
Assert.Equal(0, bounds.Left, precision: 3);
}

[Theory]
[InlineData(TextAlignment.TopLeft)]
[InlineData(TextAlignment.Center)]
public void MathPainterMeasureThenDrawUsesTheSameAggregateBounds(TextAlignment alignment) {
var painter = new SkiaSharp.MathPainter { FontSize = 32, LaTeX = Maxwell };
var measured = painter.Measure();
var width = Math.Max(1, (int)Math.Ceiling(measured.Width) + 4);
var height = Math.Max(1, (int)Math.Ceiling(measured.Height) + 4);
using var bitmap = new SKBitmap(width, height);
using var canvas = new SKCanvas(bitmap);
painter.Draw(canvas, alignment);
var pixels = Enumerable.Range(0, bitmap.Width).SelectMany(x =>
Enumerable.Range(0, bitmap.Height).Select(y => (x, y, a: bitmap.GetPixel(x, y).Alpha)))
.Where(p => p.a > 0).ToArray();
Assert.NotEmpty(pixels);
Assert.InRange(pixels.Min(p => p.x), 1, bitmap.Width - 2);
Assert.InRange(pixels.Max(p => p.x), 1, bitmap.Width - 2);
Assert.True(measured.Width > 0 && measured.Height > 0);
if (alignment == TextAlignment.Center)
Assert.InRange((pixels.Min(p => p.x) + pixels.Max(p => p.x)) / 2f,
bitmap.Width / 2f - 3, bitmap.Width / 2f + 3);
}

[Fact]
public void OffsetAccentIsIncludedInAggregateInkBounds() {
var painter = new SkiaSharp.MathPainter { FontSize = 32, LaTeX = @"\widehat{AB}" };
painter.Measure();
var root = Assert.IsType<Display.Displays.ListDisplay<Fonts, Glyph>>(painter.Display);
var accent = Assert.Single(root.Displays.OfType<Display.Displays.AccentDisplay<Fonts, Glyph>>());
var bounds = DisplayInkBounds.Get(root);
var accentRight = accent.Position.X + accent.Accent.Position.X + accent.Accent.DisplayBounds().Width;
Assert.InRange(bounds.Right, accentRight - 0.01f, float.PositiveInfinity);
}

[Fact]
public void NonMultilineTextHighlightRetainsLegacyBounds() {
var painter = new SkiaSharp.TextPainter {
FontSize = 32, LaTeX = "single line", HighlightColor = SKColors.Red
};
painter.Measure(400);
Assert.False(DisplayInkBounds.RequiresAggregateBounds(painter.Display!));
using var bitmap = new SKBitmap(400, 100);
using var canvas = new SKCanvas(bitmap);
painter.Draw(canvas, TextAlignment.TopLeft);
}

[Fact]
public void MultilineHighlightUsesAggregateBounds() {
var painter = new SkiaSharp.MathPainter {
FontSize = 32, LaTeX = Maxwell, HighlightColor = SKColors.Red
};
var measure = painter.Measure();
Assert.True(DisplayInkBounds.RequiresAggregateBounds(painter.Display!));
using var bitmap = new SKBitmap(Math.Max(1, (int)Math.Ceiling(measure.Width) + 4),
Math.Max(1, (int)Math.Ceiling(measure.Height) + 4));
using var canvas = new SKCanvas(bitmap);
painter.Draw(canvas, TextAlignment.TopLeft);
}
}
}
Binary file modified CSharpMath.Rendering.Tests/TextCenter/IntegrationByParts.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified CSharpMath.Rendering.Tests/TextLeft/IntegrationByParts.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified CSharpMath.Rendering.Tests/TextRight/IntegrationByParts.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 11 additions & 2 deletions CSharpMath.Rendering/FrontEnd/MathPainter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,16 @@ protected override void UpdateDisplayCore(float unused) {
public override void Draw(TCanvas canvas, TextAlignment alignment = TextAlignment.Center, Thickness padding = default, float offsetX = 0, float offsetY = 0) {
var c = WrapCanvas(canvas);
UpdateDisplay(float.NaN);
DrawCore(c, Display, Display == null ? new PointF?() : IPainterExtensions.GetDisplayPosition(Display.Width, Display.Ascent, Display.Descent, FontSize, c.Width, c.Height, alignment, padding, offsetX, offsetY));
if (Display is not { } display) {
DrawCore(c, null);
return;
}
var aggregate = DisplayInkBounds.RequiresAggregateBounds(display);
var bounds = aggregate ? DisplayInkBounds.Get(display) : display.DisplayBounds();
var position = IPainterExtensions.GetDisplayPosition(
aggregate ? bounds.Width : display.Width, display.Ascent, display.Descent, FontSize,
c.Width, c.Height, alignment, padding, offsetX, offsetY);
DrawCore(c, display, aggregate ? new PointF(position.X - bounds.X, position.Y) : position);
}
public void Draw(TCanvas canvas, float x, float y) {
var c = WrapCanvas(canvas);
Expand Down Expand Up @@ -56,4 +65,4 @@ public void DrawDisplay(IDisplay<Fonts, Glyph>? display, TCanvas canvas,
}
public new MathPainter<TCanvas, TColor> ShallowClone() => (MathPainter<TCanvas, TColor>)MemberwiseClone();
}
}
}
Loading
Loading