Skip to content
Open
121 changes: 121 additions & 0 deletions src/Avalonia.Base/Media/Fonts/Tables/Glyf/GlyfTable.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using Avalonia.Platform;
using Avalonia.Logging;

Expand Down Expand Up @@ -99,6 +100,126 @@ public bool TryGetGlyphData(int glyphIndex, out ReadOnlyMemory<byte> data)
return true;
}

/// <summary>
/// Reads a glyph's bounding box from its 'glyf' header without parsing contours.
/// </summary>
/// <remarks>
/// The values are the control-point bounding box stored in the glyph header
/// (the min/max of all on- and off-curve points), in font design units. This is a
/// slight superset of the rendered ink bounds for glyphs with off-curve points.
/// Composite glyphs carry their overall bounding box in the header too, so no
/// recursion is needed. Returns <see langword="true"/> with all-zero bounds for
/// empty glyphs (e.g. whitespace); returns <see langword="false"/> when the glyph
/// index is out of range or the glyph data is too short to contain a header.
/// </remarks>
/// <param name="glyphIndex">The zero-based glyph index.</param>
/// <param name="xMin">The minimum x coordinate of the bounding box.</param>
/// <param name="yMin">The minimum y coordinate of the bounding box.</param>
/// <param name="xMax">The maximum x coordinate of the bounding box.</param>
/// <param name="yMax">The maximum y coordinate of the bounding box.</param>
/// <returns><see langword="true"/> if bounds were resolved (including empty glyphs); otherwise <see langword="false"/>.</returns>
public bool TryGetGlyphBounds(int glyphIndex, out short xMin, out short yMin, out short xMax, out short yMax)
{
xMin = 0;
yMin = 0;
xMax = 0;
yMax = 0;

if (!TryGetGlyphData(glyphIndex, out var data))
{
// Out of range.
return false;
}

if (data.IsEmpty)
{
// Empty glyph (e.g. whitespace): valid, zero bounds.
return true;
}

var span = data.Span;

// Glyph header: int16 numberOfContours, then int16 xMin, yMin, xMax, yMax.
if (span.Length < 10)
{
return false;
}

xMin = BinaryPrimitives.ReadInt16BigEndian(span.Slice(2, 2));
yMin = BinaryPrimitives.ReadInt16BigEndian(span.Slice(4, 2));
xMax = BinaryPrimitives.ReadInt16BigEndian(span.Slice(6, 2));
yMax = BinaryPrimitives.ReadInt16BigEndian(span.Slice(8, 2));

return true;
}

/// <summary>
/// Reads bounding boxes for a batch of glyphs into <paramref name="bounds"/>.
/// </summary>
/// <remarks>
/// The hot path for ink-bounds computation. The <c>glyf</c> and <c>loca</c> spans are
/// fetched once for the whole batch (not per glyph), and offsets and headers are read
/// directly — no per-glyph <see cref="ReadOnlyMemory{T}.Span"/> conversion, no
/// intermediate slices, no nested call chain. Out-of-range, empty, or malformed
/// glyphs are written as the default (zero) box.
/// </remarks>
/// <param name="glyphIndices">The glyph indices to read.</param>
/// <param name="bounds">Output; must be at least as long as <paramref name="glyphIndices"/>.</param>
public void GetGlyphBounds(ReadOnlySpan<ushort> glyphIndices, Span<GlyphBounds> bounds)
{
var glyf = _glyfData.Span;
var loca = _locaTable.RawData;
var shortFormat = _locaTable.IsShortFormat;
var glyphCount = _locaTable.GlyphCount;
var entrySize = shortFormat ? 2 : 4;

for (var i = 0; i < glyphIndices.Length; i++)
{
bounds[i] = default;

int gid = glyphIndices[i];

if ((uint)gid >= (uint)glyphCount)
{
continue;
}

var locaOffset = gid * entrySize;

// Need both loca[gid] and loca[gid + 1].
if (locaOffset + (2 * entrySize) > loca.Length)
{
continue;
}

int start, end;

if (shortFormat)
{
// Short format: uint16 values stored divided by 2
start = BinaryPrimitives.ReadUInt16BigEndian(loca.Slice(locaOffset)) * 2;
Comment thread
Gillibald marked this conversation as resolved.
end = BinaryPrimitives.ReadUInt16BigEndian(loca.Slice(locaOffset + 2)) * 2;
}
else
{
start = (int)BinaryPrimitives.ReadUInt32BigEndian(loca.Slice(locaOffset));
end = (int)BinaryPrimitives.ReadUInt32BigEndian(loca.Slice(locaOffset + 4));
}

// Empty (start == end) or malformed glyph → leave the zero box.
if (end - start < 10 || start < 0 || (uint)end > (uint)glyf.Length)
{
continue;
}

bounds[i] = new GlyphBounds(
BinaryPrimitives.ReadInt16BigEndian(glyf.Slice(start + 2)),
BinaryPrimitives.ReadInt16BigEndian(glyf.Slice(start + 4)),
BinaryPrimitives.ReadInt16BigEndian(glyf.Slice(start + 6)),
BinaryPrimitives.ReadInt16BigEndian(glyf.Slice(start + 8)));
}
}

/// <summary>
/// Builds the glyph outline into the provided geometry context. Returns false for empty glyphs.
/// Coordinates are in font design units. Composite glyphs are supported.
Expand Down
13 changes: 13 additions & 0 deletions src/Avalonia.Base/Media/Fonts/Tables/LocaTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ private LocaTable(ReadOnlyMemory<byte> data, int glyphCount, bool isShortFormat)
/// </summary>
public int GlyphCount => _glyphCount;

/// <summary>
/// Gets the raw table bytes. Exposed so batch readers can fetch the span once and
/// read offsets directly, avoiding a per-glyph <see cref="ReadOnlyMemory{T}.Span"/>
/// conversion.
/// </summary>
internal ReadOnlySpan<byte> RawData => _data.Span;

/// <summary>
/// Gets a value indicating whether offsets are stored in the short (<c>uint16</c>×2)
/// format; otherwise the long (<c>uint32</c>) format is used.
/// </summary>
internal bool IsShortFormat => _isShortFormat;

/// <summary>
/// Loads the loca table from the specified typeface.
/// </summary>
Expand Down
29 changes: 29 additions & 0 deletions src/Avalonia.Base/Media/GlyphBounds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;

namespace Avalonia.Media
{
/// <summary>
/// A glyph's control-point bounding box in font design units, as stored in the
/// <c>glyf</c> header. Used by the batch bounds path
/// (<see cref="GlyphTypeface.TryGetGlyphBounds"/>) where only the ink extent is
/// needed and advances are already known by the caller.
/// </summary>
internal readonly record struct GlyphBounds(short XMin, short YMin, short XMax, short YMax)
{
/// <summary>
/// Width of the bounding box (<see cref="XMax"/> − <see cref="XMin"/>), clamped to a
/// non-negative value. A malformed header with <see cref="XMax"/> &lt; <see cref="XMin"/>
/// yields <c>0</c> rather than wrapping when narrowed to an unsigned extent. The maximum
/// possible extent for <see cref="short"/> coordinates is 65535, so the result always
/// fits in a <see cref="ushort"/>.
/// </summary>
public int Width => Math.Max(0, XMax - XMin);

/// <summary>
/// Height of the bounding box (<see cref="YMax"/> − <see cref="YMin"/>), clamped to a
/// non-negative value. A malformed header with <see cref="YMax"/> &lt; <see cref="YMin"/>
/// yields <c>0</c> rather than wrapping when narrowed to an unsigned extent.
/// </summary>
public int Height => Math.Max(0, YMax - YMin);
}
}
40 changes: 35 additions & 5 deletions src/Avalonia.Base/Media/GlyphMetrics.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
namespace Avalonia.Media;
namespace Avalonia.Media;

public readonly record struct GlyphMetrics
{
/// <summary>
/// Distance from the x-origin to the left extremum of the glyph.
/// Distance from the x-origin to the leftmost outline point.
/// </summary>
public int XBearing { get; init; }

/// <summary>
/// Distance from the top extremum of the glyph to the y-origin.
/// Distance from the topmost outline point to the y-origin.
/// </summary>
public int YBearing { get; init; }

/// <summary>
/// Distance from the left extremum of the glyph to the right extremum.
/// Width of the glyph's outline bounding box.
/// </summary>
public ushort Width { get; init; }

/// <summary>
/// Distance from the top extremum of the glyph to the bottom extremum.
/// Height of the glyph's outline bounding box.
/// </summary>
public ushort Height { get; init; }

/// <summary>
/// Horizontal advance width (distance to the next glyph's origin).
/// </summary>
public ushort AdvanceWidth { get; init; }

/// <summary>
/// Vertical advance height (distance to the next glyph's origin in vertical layout).
/// </summary>
public ushort AdvanceHeight { get; init; }

/// <summary>
/// Horizontal offset from the glyph's origin to the leftmost outline point (used for bitmap glyphs).
/// </summary>
public ushort XOffset { get; init; }

/// <summary>
/// Vertical offset from the glyph's origin to the topmost outline point (used for bitmap glyphs).
/// </summary>
public ushort YOffset { get; init; }

/// <summary>
/// X coordinate of the vertical origin (used for vertical layout).
/// </summary>
public ushort VerticalOriginX { get; init; }

/// <summary>
/// Y coordinate of the vertical origin (used for vertical layout).
/// </summary>
public ushort VerticalOriginY { get; init; }
}
Loading