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.
164 lines
6.9 KiB
164 lines
6.9 KiB
using System;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Media;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Skia;
|
|
using SkiaSharp;
|
|
using Xunit;
|
|
|
|
namespace Avalonia.Skia.RenderTests
|
|
{
|
|
public class GlyphOutlineRenderTests : TestBase
|
|
{
|
|
// Direct asset URIs (not via FontManager) so we can address Inter-Regular and
|
|
// InterVariable separately even though they share the family name "Inter".
|
|
private const string InterRegularAsset =
|
|
"resm:Avalonia.Skia.RenderTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Skia.RenderTests";
|
|
|
|
private const string InterVariableAsset =
|
|
"resm:Avalonia.Skia.RenderTests.Assets.InterVariable.ttf?assembly=Avalonia.Skia.RenderTests";
|
|
|
|
private const string MiSansAsset =
|
|
"resm:Avalonia.Skia.RenderTests.Assets.MiSans-Normal.ttf?assembly=Avalonia.Skia.RenderTests";
|
|
|
|
private const string AdobeBlankAsset =
|
|
"resm:Avalonia.Skia.RenderTests.Assets.AdobeBlank2VF.ttf?assembly=Avalonia.Skia.RenderTests";
|
|
|
|
// Purpose-built fixture (no production font encodes point matching): its 'P' glyph is a
|
|
// composite assembled with ARGS_ARE_XY_VALUES clear, i.e. by point matching.
|
|
private const string PointMatchAsset =
|
|
"resm:Avalonia.Skia.RenderTests.Assets.PointMatch.ttf?assembly=Avalonia.Skia.RenderTests";
|
|
|
|
public GlyphOutlineRenderTests()
|
|
: base(@"Media\GlyphOutline")
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Render_Inter_Latin_Glyph()
|
|
{
|
|
await RenderToFile(BuildTarget(LoadGlyphTypeface(InterRegularAsset), 'A'));
|
|
CompareImages();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Render_Inter_Composite_Glyph()
|
|
{
|
|
// Á = base 'A' + combining acute accent. Inter stores it as a composite glyph,
|
|
// which exercises GlyfTable's recursive component path.
|
|
await RenderToFile(BuildTarget(LoadGlyphTypeface(InterRegularAsset), 'Á'));
|
|
CompareImages();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Render_PointMatched_Composite_Glyph()
|
|
{
|
|
// 'P' is a composite whose second component (a small square) is placed by point
|
|
// matching — aligning its bottom-left point onto the top-right point of the base
|
|
// rectangle — rather than by an x/y offset. This is the only way to exercise
|
|
// GlyfTable's point-matching build path through the full Skia pipeline, since no
|
|
// production font uses point matching. A broken implementation would drop the square
|
|
// at the origin instead of the rectangle's corner, which the image diff would catch.
|
|
await RenderToFile(BuildTarget(LoadGlyphTypeface(PointMatchAsset), 'P'));
|
|
CompareImages();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Render_MiSans_CJK_Glyph()
|
|
{
|
|
// 中 — a CJK ideograph with many contours and counters.
|
|
await RenderToFile(BuildTarget(LoadGlyphTypeface(MiSansAsset), '中'));
|
|
CompareImages();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Render_InterVariable_At_Default()
|
|
{
|
|
// Variable font with fvar / gvar tables present. PR2 doesn't apply variation,
|
|
// so this renders the default-instance outline — the baseline against which a
|
|
// future variable-font render test will compare deformed outputs.
|
|
await RenderToFile(BuildTarget(LoadGlyphTypeface(InterVariableAsset), 'R'));
|
|
CompareImages();
|
|
}
|
|
|
|
[Fact]
|
|
public void Blank_Variable_Font_Returns_Null_Outline()
|
|
{
|
|
// AdobeBlank2VF is a variable font whose glyphs are intentionally empty.
|
|
// GetGlyphOutline must return null without throwing on a font with fvar but
|
|
// no usable contour data.
|
|
var gt = LoadGlyphTypeface(AdobeBlankAsset);
|
|
var glyphIndex = gt.CharacterToGlyphMap.ContainsGlyph('A')
|
|
? gt.CharacterToGlyphMap['A']
|
|
: (ushort)0;
|
|
|
|
Assert.Null(gt.GetGlyphOutline(glyphIndex));
|
|
}
|
|
|
|
private static GlyphTypeface LoadGlyphTypeface(string assetUri)
|
|
{
|
|
var loader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
|
|
using var stream = loader.Open(new Uri(assetUri));
|
|
using var memory = new MemoryStream();
|
|
stream.CopyTo(memory);
|
|
// Copy the bytes into Skia-owned memory so the typeface's table reads keep
|
|
// working after the source stream is disposed. SKTypeface.FromStream's
|
|
// lifetime story varies by font; SKData.CreateCopy is bulletproof.
|
|
var skData = SKData.CreateCopy(memory.ToArray());
|
|
var skTypeface = SKTypeface.FromData(skData)
|
|
?? throw new InvalidOperationException("SkiaSharp failed to load the font.");
|
|
return new GlyphTypeface(new SkiaTypeface(skTypeface, FontSimulations.None));
|
|
}
|
|
|
|
private static Border BuildTarget(GlyphTypeface glyphTypeface, char ch)
|
|
=> new Border
|
|
{
|
|
Width = 240,
|
|
Height = 240,
|
|
Background = Brushes.White,
|
|
Child = new GlyphOutlineControl(glyphTypeface, ch),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Renders a single glyph outline produced by
|
|
/// <see cref="GlyphTypeface.GetGlyphOutline(ushort)"/>, filled in black.
|
|
/// The hosting <see cref="Border"/> supplies the white background and the layout
|
|
/// size; the control itself fills the area assigned by layout.
|
|
/// </summary>
|
|
public class GlyphOutlineControl : Control
|
|
{
|
|
private readonly IGeometryImpl? _outline;
|
|
|
|
public GlyphOutlineControl(GlyphTypeface glyphTypeface, char ch)
|
|
{
|
|
const double emSize = 200;
|
|
const double margin = 20;
|
|
|
|
if (!glyphTypeface.CharacterToGlyphMap.ContainsGlyph(ch))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var glyphIndex = glyphTypeface.CharacterToGlyphMap[ch];
|
|
var scale = emSize / glyphTypeface.Metrics.DesignEmHeight;
|
|
|
|
// glyf is y-up; flip the y axis and translate so the baseline lands inside
|
|
// the bitmap with a margin of `margin` on every side.
|
|
var transform = Matrix.CreateScale(scale, -scale)
|
|
* Matrix.CreateTranslation(margin, emSize + margin);
|
|
|
|
_outline = glyphTypeface.GetGlyphOutline(glyphIndex)?.WithTransform(transform);
|
|
}
|
|
|
|
public override void Render(DrawingContext context)
|
|
{
|
|
if (_outline != null)
|
|
{
|
|
context.DrawGeometry(Brushes.Black, null, _outline);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|