csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
316 lines
11 KiB
316 lines
11 KiB
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Generic;
|
|
using Avalonia.Media;
|
|
using Avalonia.Media.Fonts.Tables;
|
|
using Avalonia.Media.Fonts.Tables.Glyf;
|
|
using Avalonia.Platform;
|
|
using Xunit;
|
|
|
|
namespace Avalonia.Base.UnitTests.Media.Fonts.Tables
|
|
{
|
|
public class GlyfTableTests
|
|
{
|
|
private const string InterFontUri =
|
|
"resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests";
|
|
|
|
private static GlyphTypeface LoadInter()
|
|
{
|
|
var assetLoader = new StandardAssetLoader();
|
|
using var stream = assetLoader.Open(new Uri(InterFontUri));
|
|
return new GlyphTypeface(new CustomPlatformTypeface(stream));
|
|
}
|
|
|
|
private static GlyfTable LoadGlyf(GlyphTypeface typeface)
|
|
{
|
|
Assert.True(HeadTable.TryLoad(typeface, out var head));
|
|
var maxp = MaxpTable.Load(typeface);
|
|
Assert.True(GlyfTable.TryLoad(typeface, head!, maxp, out var glyf));
|
|
Assert.NotNull(glyf);
|
|
return glyf!;
|
|
}
|
|
|
|
private static ushort GlyphFor(GlyphTypeface typeface, char c)
|
|
{
|
|
var map = typeface.CharacterToGlyphMap;
|
|
Assert.True(map.ContainsGlyph(c));
|
|
return map[c];
|
|
}
|
|
|
|
[Fact]
|
|
public void TryLoad_Succeeds_For_Inter()
|
|
{
|
|
var glyf = LoadGlyf(LoadInter());
|
|
|
|
Assert.True(glyf.GlyphCount > 0);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1)]
|
|
[InlineData(int.MaxValue)]
|
|
public void TryGetGlyphData_Returns_False_For_Out_Of_Range(int glyphIndex)
|
|
{
|
|
var glyf = LoadGlyf(LoadInter());
|
|
|
|
Assert.False(glyf.TryGetGlyphData(glyphIndex, out var data));
|
|
Assert.True(data.IsEmpty);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGetGlyphData_Returns_Empty_For_Space_Glyph()
|
|
{
|
|
var typeface = LoadInter();
|
|
var glyf = LoadGlyf(typeface);
|
|
|
|
var spaceGlyph = GlyphFor(typeface, ' ');
|
|
|
|
// An empty glyph is a valid glyph with no outline data.
|
|
Assert.True(glyf.TryGetGlyphData(spaceGlyph, out var data));
|
|
Assert.True(data.IsEmpty);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGetGlyphData_Returns_Data_For_Letter()
|
|
{
|
|
var typeface = LoadInter();
|
|
var glyf = LoadGlyf(typeface);
|
|
|
|
var letterGlyph = GlyphFor(typeface, 'A');
|
|
|
|
Assert.True(glyf.TryGetGlyphData(letterGlyph, out var data));
|
|
Assert.False(data.IsEmpty);
|
|
|
|
// At minimum a glyph carries its 10-byte header (numberOfContours + bbox).
|
|
Assert.True(data.Length >= 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBuildGlyphGeometry_Builds_Closed_Figures_For_Letter()
|
|
{
|
|
var typeface = LoadInter();
|
|
var glyf = LoadGlyf(typeface);
|
|
|
|
var letterGlyph = GlyphFor(typeface, 'A');
|
|
|
|
var context = new RecordingGeometryContext();
|
|
|
|
Assert.True(glyf.TryBuildGlyphGeometry(letterGlyph, Matrix.Identity, context));
|
|
|
|
Assert.True(context.BeginFigureCount >= 1, "Expected at least one contour.");
|
|
Assert.Equal(context.BeginFigureCount, context.EndFigureCount);
|
|
Assert.True(context.AllFiguresClosed, "Glyph contours must be closed.");
|
|
Assert.NotEmpty(context.Points);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBuildGlyphGeometry_Returns_False_For_Empty_Glyph()
|
|
{
|
|
var typeface = LoadInter();
|
|
var glyf = LoadGlyf(typeface);
|
|
|
|
var spaceGlyph = GlyphFor(typeface, ' ');
|
|
|
|
var context = new RecordingGeometryContext();
|
|
|
|
// Nothing to draw for an empty glyph.
|
|
Assert.False(glyf.TryBuildGlyphGeometry(spaceGlyph, Matrix.Identity, context));
|
|
Assert.Equal(0, context.BeginFigureCount);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1)]
|
|
[InlineData(int.MaxValue)]
|
|
public void TryBuildGlyphGeometry_Returns_False_For_Out_Of_Range(int glyphIndex)
|
|
{
|
|
var glyf = LoadGlyf(LoadInter());
|
|
|
|
var context = new RecordingGeometryContext();
|
|
|
|
Assert.False(glyf.TryBuildGlyphGeometry(glyphIndex, Matrix.Identity, context));
|
|
Assert.Equal(0, context.BeginFigureCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBuildGlyphGeometry_Applies_Transform()
|
|
{
|
|
var typeface = LoadInter();
|
|
var glyf = LoadGlyf(typeface);
|
|
|
|
var letterGlyph = GlyphFor(typeface, 'A');
|
|
|
|
var identity = new RecordingGeometryContext();
|
|
Assert.True(glyf.TryBuildGlyphGeometry(letterGlyph, Matrix.Identity, identity));
|
|
|
|
var scaled = new RecordingGeometryContext();
|
|
Assert.True(glyf.TryBuildGlyphGeometry(letterGlyph, Matrix.CreateScale(2, 2), scaled));
|
|
|
|
// Same glyph, same decode order: scaling the transform by 2 must scale every
|
|
// emitted point by 2.
|
|
Assert.Equal(identity.Points.Count, scaled.Points.Count);
|
|
|
|
var sawNonZero = false;
|
|
|
|
for (var i = 0; i < identity.Points.Count; i++)
|
|
{
|
|
var a = identity.Points[i];
|
|
var b = scaled.Points[i];
|
|
|
|
Assert.Equal(a.X * 2, b.X, 3);
|
|
Assert.Equal(a.Y * 2, b.Y, 3);
|
|
|
|
if (Math.Abs(a.X) > 0.001 || Math.Abs(a.Y) > 0.001)
|
|
{
|
|
sawNonZero = true;
|
|
}
|
|
}
|
|
|
|
Assert.True(sawNonZero, "Expected the glyph to emit non-zero coordinates.");
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBuildGlyphGeometry_Builds_Composite_Glyph()
|
|
{
|
|
var typeface = LoadInter();
|
|
var glyf = LoadGlyf(typeface);
|
|
|
|
var compositeGlyph = FindCompositeGlyph(glyf);
|
|
|
|
// Inter builds its accented characters from components; there should be at
|
|
// least one composite glyph to exercise the recursive build path.
|
|
Assert.True(compositeGlyph >= 0, "Expected Inter to contain at least one composite glyph.");
|
|
|
|
var context = new RecordingGeometryContext();
|
|
|
|
Assert.True(glyf.TryBuildGlyphGeometry(compositeGlyph, Matrix.Identity, context));
|
|
|
|
// A composite expands into its components' contours.
|
|
Assert.True(context.BeginFigureCount >= 1);
|
|
Assert.Equal(context.BeginFigureCount, context.EndFigureCount);
|
|
Assert.True(context.AllFiguresClosed);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1)]
|
|
[InlineData(int.MaxValue)]
|
|
public void TryGetGlyphBounds_Returns_False_For_Out_Of_Range(int glyphIndex)
|
|
{
|
|
var glyf = LoadGlyf(LoadInter());
|
|
|
|
Assert.False(glyf.TryGetGlyphBounds(glyphIndex, out _, out _, out _, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGetGlyphBounds_Returns_Zero_For_Empty_Glyph()
|
|
{
|
|
var typeface = LoadInter();
|
|
var glyf = LoadGlyf(typeface);
|
|
|
|
var spaceGlyph = GlyphFor(typeface, ' ');
|
|
|
|
// Empty glyph is valid but carries no box.
|
|
Assert.True(glyf.TryGetGlyphBounds(spaceGlyph, out var xMin, out var yMin, out var xMax, out var yMax));
|
|
Assert.Equal(0, xMin);
|
|
Assert.Equal(0, yMin);
|
|
Assert.Equal(0, xMax);
|
|
Assert.Equal(0, yMax);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGetGlyphBounds_Returns_NonEmpty_Box_For_Letter()
|
|
{
|
|
var typeface = LoadInter();
|
|
var glyf = LoadGlyf(typeface);
|
|
|
|
var letterGlyph = GlyphFor(typeface, 'A');
|
|
|
|
Assert.True(glyf.TryGetGlyphBounds(letterGlyph, out var xMin, out var yMin, out var xMax, out var yMax));
|
|
Assert.True(xMax > xMin, "Letter glyph should have a positive-width box.");
|
|
Assert.True(yMax > yMin, "Letter glyph should have a positive-height box.");
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGetGlyphBounds_Matches_GlyphDescriptor_Header()
|
|
{
|
|
var typeface = LoadInter();
|
|
var glyf = LoadGlyf(typeface);
|
|
|
|
var letterGlyph = GlyphFor(typeface, 'A');
|
|
|
|
Assert.True(glyf.TryGetGlyphData(letterGlyph, out var data));
|
|
var descriptor = new GlyphDescriptor(data);
|
|
|
|
Assert.True(glyf.TryGetGlyphBounds(letterGlyph, out var xMin, out var yMin, out var xMax, out var yMax));
|
|
|
|
// The header-only read must agree with the descriptor's parsed bounds.
|
|
Assert.Equal((double)xMin, descriptor.ConservativeBounds.X);
|
|
Assert.Equal((double)yMin, descriptor.ConservativeBounds.Y);
|
|
Assert.Equal((double)(xMax - xMin), descriptor.ConservativeBounds.Width);
|
|
Assert.Equal((double)(yMax - yMin), descriptor.ConservativeBounds.Height);
|
|
}
|
|
|
|
private static int FindCompositeGlyph(GlyfTable glyf)
|
|
{
|
|
for (var i = 0; i < glyf.GlyphCount; i++)
|
|
{
|
|
if (glyf.TryGetGlyphData(i, out var data) && data.Length >= 2)
|
|
{
|
|
var numberOfContours = BinaryPrimitives.ReadInt16BigEndian(data.Span.Slice(0, 2));
|
|
|
|
// Negative contour count marks a composite glyph.
|
|
if (numberOfContours < 0)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private sealed class RecordingGeometryContext : IGeometryContext
|
|
{
|
|
public int BeginFigureCount { get; private set; }
|
|
public int EndFigureCount { get; private set; }
|
|
public bool AllFiguresClosed { get; private set; } = true;
|
|
public List<Point> Points { get; } = new();
|
|
|
|
public void BeginFigure(Point startPoint, bool isFilled = true)
|
|
{
|
|
BeginFigureCount++;
|
|
Points.Add(startPoint);
|
|
}
|
|
|
|
public void LineTo(Point point, bool isStroked = true) => Points.Add(point);
|
|
|
|
public void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked = true)
|
|
{
|
|
Points.Add(controlPoint);
|
|
Points.Add(endPoint);
|
|
}
|
|
|
|
public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked = true)
|
|
{
|
|
Points.Add(controlPoint1);
|
|
Points.Add(controlPoint2);
|
|
Points.Add(endPoint);
|
|
}
|
|
|
|
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc,
|
|
SweepDirection sweepDirection, bool isStroked = true) => Points.Add(point);
|
|
|
|
public void EndFigure(bool isClosed)
|
|
{
|
|
EndFigureCount++;
|
|
|
|
if (!isClosed)
|
|
{
|
|
AllFiguresClosed = false;
|
|
}
|
|
}
|
|
|
|
public void SetFillRule(FillRule fillRule) { }
|
|
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
}
|
|
|