Browse Source

Add boundary check to HarfBuzzTextShaper (#20716)

* feat: add boundary check.

* feat: add empty string handling and unit tests for HarfBuzzTextShaper
pull/20730/head
Dong Bin 1 month ago
committed by GitHub
parent
commit
c9d6efbf67
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs
  2. 80
      tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/HeadTableTests.cs
  3. 120
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/HarfbuzzTextShaperTests.cs

5
src/HarfBuzz/Avalonia.HarfBuzz/HarfBuzzTextShaper.cs

@ -24,6 +24,9 @@ namespace Avalonia.Harfbuzz
{
var textSpan = text.Span;
if (text.Length == 0)
return new ShapedBuffer(text, 0, options.GlyphTypeface, options.FontRenderingEmSize, options.BidiLevel);
var glyphTypeface = options.GlyphTypeface;
if (glyphTypeface.TextShaperTypeface is not HarfBuzzTypeface harfBuzzTypeface)
@ -121,6 +124,8 @@ namespace Avalonia.Harfbuzz
{
var length = buffer.Length;
if (length == 0) return;
var glyphInfos = buffer.GetGlyphInfoSpan();
var second = glyphInfos[length - 1];

80
tests/Avalonia.Base.UnitTests/Media/Fonts/Tables/HeadTableTests.cs

@ -191,66 +191,66 @@ namespace Avalonia.Base.UnitTests.Media.Fonts.Tables
Assert.True(HeadTable.TryLoad(typeface, out var headTable));
Assert.Equal(FontDirectionHint.LeftToRightWithNeutrals, headTable.FontDirectionHint);
}
}
internal class CustomPlatformTypeface : IPlatformTypeface
{
private readonly UnmanagedFontMemory _fontMemory;
private class CustomPlatformTypeface : IPlatformTypeface
public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom")
{
private readonly UnmanagedFontMemory _fontMemory;
_fontMemory = UnmanagedFontMemory.LoadFromStream(stream);
FamilyName = fontFamily;
}
public CustomPlatformTypeface(Stream stream, string fontFamily = "Custom")
{
_fontMemory = UnmanagedFontMemory.LoadFromStream(stream);
FamilyName = fontFamily;
}
public FontWeight Weight => FontWeight.Normal;
public FontWeight Weight => FontWeight.Normal;
public FontStyle Style => FontStyle.Normal;
public FontStyle Style => FontStyle.Normal;
public FontStretch Stretch => FontStretch.Normal;
public FontStretch Stretch => FontStretch.Normal;
public string FamilyName { get; }
public string FamilyName { get; }
public FontSimulations FontSimulations => FontSimulations.None;
public FontSimulations FontSimulations => FontSimulations.None;
public void Dispose()
{
((IDisposable)_fontMemory).Dispose();
}
public void Dispose()
{
((IDisposable)_fontMemory).Dispose();
}
public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream)
{
var memory = _fontMemory.Memory;
public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream)
{
var memory = _fontMemory.Memory;
var handle = memory.Pin();
stream = new PinnedUnmanagedMemoryStream(handle, memory.Length);
var handle = memory.Pin();
stream = new PinnedUnmanagedMemoryStream(handle, memory.Length);
return true;
}
return true;
}
private sealed class PinnedUnmanagedMemoryStream : UnmanagedMemoryStream
{
private MemoryHandle _handle;
private sealed class PinnedUnmanagedMemoryStream : UnmanagedMemoryStream
public unsafe PinnedUnmanagedMemoryStream(MemoryHandle handle, long length)
: base((byte*)handle.Pointer, length)
{
private MemoryHandle _handle;
_handle = handle;
}
public unsafe PinnedUnmanagedMemoryStream(MemoryHandle handle, long length)
: base((byte*)handle.Pointer, length)
protected override void Dispose(bool disposing)
{
try
{
_handle = handle;
base.Dispose(disposing);
}
protected override void Dispose(bool disposing)
finally
{
try
{
base.Dispose(disposing);
}
finally
{
_handle.Dispose();
}
_handle.Dispose();
}
}
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table) => _fontMemory.TryGetTable(tag, out table);
}
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table) => _fontMemory.TryGetTable(tag, out table);
}
}

120
tests/Avalonia.Base.UnitTests/Media/TextFormatting/HarfbuzzTextShaperTests.cs

@ -0,0 +1,120 @@
using System;
using System.Linq;
using Avalonia.Base.UnitTests.Media.Fonts.Tables;
using Avalonia.Harfbuzz;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests.Media.TextFormatting;
public class HarfBuzzTextShaperTests
{
private static readonly string s_InterFontUri =
"resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests";
private readonly HarfBuzzTextShaper _shaper;
private TestServices Services => TestServices.MockThreadingInterface.With(
textShaperImpl: _shaper);
public HarfBuzzTextShaperTests()
{
_shaper = new HarfBuzzTextShaper();
}
[Fact]
public void ShapeText_WithValidInput_ReturnsShapedBuffer()
{
using (UnitTestApplication.Start(Services))
{
var text = "Hello World".AsMemory();
var options = CreateTextShaperOptions();
var result = _shaper.ShapeText(text, options);
Assert.NotNull(result);
Assert.Equal(text.Length, result.Length);
}
}
[Fact]
public void ShapeText_WithEmptyString_ReturnsEmptyShapedBuffer()
{
using (UnitTestApplication.Start(Services))
{
var text = "".AsMemory();
var options = CreateTextShaperOptions();
var result = _shaper.ShapeText(text, options);
Assert.NotNull(result);
Assert.Equal(0, result.Length);
}
}
[Fact]
public void ShapeText_WithTabCharacter_ReplacesWithSpace()
{
using (UnitTestApplication.Start(Services))
{
var text = "Hello\tWorld".AsMemory();
var options = CreateTextShaperOptions();
var result = _shaper.ShapeText(text, options);
Assert.NotNull(result);
Assert.True(result.Length == 11);
}
}
[Fact]
public void ShapeText_WithCRLF_MergesBreakPair()
{
using (UnitTestApplication.Start(Services))
{
var text = "Line1\r\nLine2".AsMemory();
var options = CreateTextShaperOptions();
var result = _shaper.ShapeText(text, options);
Assert.NotNull(result);
Assert.NotEqual(0.0, result[5].GlyphAdvance);
}
}
[Fact]
public void ShapeText_EndWithCRLF_MergesBreakPair()
{
using (UnitTestApplication.Start(Services))
{
var text = "Line1\r\n".AsMemory();
var options = CreateTextShaperOptions();
var result = _shaper.ShapeText(text, options);
Assert.NotNull(result);
Assert.Equal(0.0, result[5].GlyphAdvance);
}
}
private TextShaperOptions CreateTextShaperOptions(
sbyte bidiLevel = 0,
double letterSpacing = 0,
double fontSize = 16)
{
var assetLoader = new StandardAssetLoader();
using var stream = assetLoader.Open(new Uri(s_InterFontUri));
var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream));
return new TextShaperOptions(
typeface,
fontSize,
bidiLevel,
letterSpacing: letterSpacing);
}
}
Loading…
Cancel
Save