committed by
GitHub
46 changed files with 7854 additions and 9 deletions
@ -1,17 +1,194 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Input.TextInput; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Win32.WinRT.Composition; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts; |
|||
using Avalonia.Media.TextFormatting; |
|||
using Avalonia.Media.TextFormatting.Unicode; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
using Avalonia.Skia; |
|||
using Avalonia.Skia.Helpers; |
|||
using SkiaSharp; |
|||
|
|||
namespace Sandbox |
|||
{ |
|||
public partial class MainWindow : Window |
|||
{ |
|||
private int[] clockCodepoints = new int[] |
|||
{ |
|||
0x1F550, // 🕐 Clock Face One O’Clock
|
|||
0x1F551, // 🕑 Clock Face Two O’Clock
|
|||
0x1F552, // 🕒 Clock Face Three O’Clock
|
|||
0x1F553, // 🕓 Clock Face Four O’Clock
|
|||
0x1F554, // 🕔 Clock Face Five O’Clock
|
|||
0x1F555, // 🕕 Clock Face Six O’Clock
|
|||
0x1F556, // 🕖 Clock Face Seven O’Clock
|
|||
0x1F557, // 🕗 Clock Face Eight O’Clock
|
|||
0x1F558, // 🕘 Clock Face Nine O’Clock
|
|||
0x1F559, // 🕙 Clock Face Ten O’Clock
|
|||
0x1F55A, // 🕚 Clock Face Eleven O’Clock
|
|||
0x1F55B, // 🕛 Clock Face Twelve O’Clock
|
|||
|
|||
0x1F55C, // 🕜 Clock Face One-Thirty
|
|||
0x1F55D, // 🕝 Clock Face Two-Thirty
|
|||
0x1F55E, // 🕞 Clock Face Three-Thirty
|
|||
0x1F55F, // 🕟 Clock Face Four-Thirty
|
|||
0x1F560, // 🕠 Clock Face Five-Thirty
|
|||
0x1F561, // 🕡 Clock Face Six-Thirty
|
|||
0x1F562, // 🕢 Clock Face Seven-Thirty
|
|||
0x1F563, // 🕣 Clock Face Eight-Thirty
|
|||
0x1F564, // 🕤 Clock Face Nine-Thirty
|
|||
0x1F565, // 🕥 Clock Face Ten-Thirty
|
|||
0x1F566, // 🕦 Clock Face Eleven-Thirty
|
|||
0x1F567 // 🕧 Clock Face Twelve-Thirty
|
|||
}; |
|||
|
|||
public MainWindow() |
|||
{ |
|||
InitializeComponent(); |
|||
|
|||
var fontCollection = new EmbeddedFontCollection(new Uri("fonts:colr"), |
|||
new Uri("resm:Sandbox?assembly=Sandbox", UriKind.Absolute)); |
|||
|
|||
FontManager.Current.AddFontCollection(fontCollection); |
|||
|
|||
var notoColorEmojiTypeface = new Typeface("fonts:colr#Noto Color Emoji"); |
|||
var notoColorEmojiGlyphTypeface = notoColorEmojiTypeface.GlyphTypeface; |
|||
|
|||
var wrap = new WrapPanel |
|||
{ |
|||
Margin = new Thickness(8), |
|||
Orientation = Orientation.Horizontal, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
}; |
|||
|
|||
foreach (var (c, g) in notoColorEmojiGlyphTypeface.CharacterToGlyphMap.AsReadOnlyDictionary()) |
|||
{ |
|||
// Create a glyph control for each glyph
|
|||
var glyphControl = new GlyphControl |
|||
{ |
|||
GlyphTypeface = notoColorEmojiGlyphTypeface, |
|||
GlyphId = g, |
|||
Width = 66, |
|||
Height = 66, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
}; |
|||
|
|||
var border = new Border |
|||
{ |
|||
BorderBrush = Brushes.LightGray, |
|||
BorderThickness = new Thickness(1), |
|||
MinHeight = 80, |
|||
MinWidth = 80, |
|||
Padding = new Thickness(4), |
|||
Child = new Grid { Children = { glyphControl } }, |
|||
Margin = new Thickness(4) |
|||
}; |
|||
|
|||
wrap.Children.Add(border); |
|||
|
|||
Content = new ScrollViewer { Content = wrap }; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Custom control that renders a single glyph using GetGlyphDrawing for color glyphs
|
|||
/// and GetGlyphOutline for outline glyphs.
|
|||
/// </summary>
|
|||
public class GlyphControl : Control |
|||
{ |
|||
public static readonly StyledProperty<GlyphTypeface?> GlyphTypefaceProperty = |
|||
AvaloniaProperty.Register<GlyphControl, GlyphTypeface?>(nameof(GlyphTypeface)); |
|||
|
|||
public static readonly StyledProperty<ushort> GlyphIdProperty = |
|||
AvaloniaProperty.Register<GlyphControl, ushort>(nameof(GlyphId)); |
|||
|
|||
public static readonly StyledProperty<IBrush?> ForegroundProperty = |
|||
AvaloniaProperty.Register<GlyphControl, IBrush?>(nameof(Foreground), Brushes.Black); |
|||
|
|||
static GlyphControl() |
|||
{ |
|||
AffectsRender<GlyphControl>(GlyphTypefaceProperty, GlyphIdProperty, ForegroundProperty); |
|||
} |
|||
|
|||
public GlyphTypeface? GlyphTypeface |
|||
{ |
|||
get => GetValue(GlyphTypefaceProperty); |
|||
set => SetValue(GlyphTypefaceProperty, value); |
|||
} |
|||
|
|||
public ushort GlyphId |
|||
{ |
|||
get => GetValue(GlyphIdProperty); |
|||
set => SetValue(GlyphIdProperty, value); |
|||
} |
|||
|
|||
public IBrush? Foreground |
|||
{ |
|||
get => GetValue(ForegroundProperty); |
|||
set => SetValue(ForegroundProperty, value); |
|||
} |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
base.Render(context); |
|||
|
|||
var glyphTypeface = GlyphTypeface; |
|||
if (glyphTypeface == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var glyphId = GlyphId; |
|||
|
|||
// Calculate scale
|
|||
var targetSize = Math.Min(Width, Height); |
|||
var designEmHeight = glyphTypeface.Metrics.DesignEmHeight; |
|||
var scale = targetSize / designEmHeight; |
|||
|
|||
// Try to get color glyph drawing first
|
|||
var glyphDrawing = glyphTypeface.GetGlyphDrawing(glyphId); |
|||
|
|||
if (glyphDrawing != null) |
|||
{ |
|||
var bounds = glyphDrawing.Bounds; |
|||
|
|||
var offsetX = (Width / scale - bounds.Width) / 2 - bounds.Left; |
|||
var offsetY = (Height / scale - bounds.Height) / 2 - bounds.Top; |
|||
|
|||
using (context.PushTransform(Matrix.CreateTranslation(offsetX, offsetY) * |
|||
Matrix.CreateScale(scale, scale))) |
|||
{ |
|||
glyphDrawing.Draw(context, new Point()); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Outline glyph
|
|||
var glyphOutline = glyphTypeface.GetGlyphOutline(glyphId, Matrix.CreateScale(1, -1)); |
|||
|
|||
if (glyphOutline != null) |
|||
{ |
|||
// Get tight bounds of scaled geometry
|
|||
var bounds = glyphOutline.Bounds; |
|||
|
|||
// Calculate transform based on bounds
|
|||
var offsetX = (Width / scale - bounds.Width) / 2 - bounds.Left; |
|||
var offsetY = (Height / scale - bounds.Height) / 2 - bounds.Top; |
|||
|
|||
// Apply transform and render
|
|||
using (context.PushTransform(Matrix.CreateTranslation(offsetX, offsetY) * |
|||
Matrix.CreateScale(scale, scale))) |
|||
{ |
|||
context.DrawGeometry(Foreground, null, glyphOutline); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,37 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media.Fonts; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Represents font variation settings, including normalized axis coordinates, optional variation instance index,
|
|||
/// color palette selection, and pixel size for bitmap strikes.
|
|||
/// </summary>
|
|||
/// <remarks>Use this type to specify font rendering parameters for variable fonts, color fonts, and
|
|||
/// bitmap strikes. The settings correspond to OpenType font features such as axis variations (fvar/avar), named
|
|||
/// instances, color palettes (COLR/CPAL), and bitmap sizes. All properties are immutable and must be set during
|
|||
/// initialization.</remarks>
|
|||
public sealed record class FontVariationSettings |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the normalized variation coordinates for each axis, derived from fvar/avar tables.
|
|||
/// </summary>
|
|||
public required IReadOnlyDictionary<OpenTypeTag, float> NormalizedCoordinates { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of a predefined variation instance (optional).
|
|||
/// If specified, NormalizedCoordinates represent that instance.
|
|||
/// </summary>
|
|||
public int? InstanceIndex { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the color palette index for COLR/CPAL.
|
|||
/// </summary>
|
|||
public int PaletteIndex { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the pixel size for bitmap strikes.
|
|||
/// </summary>
|
|||
public int PixelSize { get; init; } |
|||
} |
|||
} |
|||
@ -0,0 +1,129 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Cmap |
|||
{ |
|||
/// <summary>
|
|||
/// Provides a read-only dictionary view over a <see cref="CharacterToGlyphMap"/>.
|
|||
/// </summary>
|
|||
internal sealed class CharacterToGlyphMapDictionary : IReadOnlyDictionary<int, ushort> |
|||
{ |
|||
private readonly CharacterToGlyphMap _map; |
|||
private List<CodepointRange>? _cachedRanges; |
|||
|
|||
public CharacterToGlyphMapDictionary(CharacterToGlyphMap map) |
|||
{ |
|||
_map = map; |
|||
} |
|||
|
|||
public ushort this[int key] |
|||
{ |
|||
get |
|||
{ |
|||
if (!_map.ContainsGlyph(key)) |
|||
{ |
|||
throw new KeyNotFoundException($"The code point {key} was not found in the character map."); |
|||
} |
|||
return _map.GetGlyph(key); |
|||
} |
|||
} |
|||
|
|||
public IEnumerable<int> Keys |
|||
{ |
|||
get |
|||
{ |
|||
foreach (var range in GetRanges()) |
|||
{ |
|||
for (int codePoint = range.Start; codePoint <= range.End; codePoint++) |
|||
{ |
|||
if (_map.ContainsGlyph(codePoint)) |
|||
{ |
|||
yield return codePoint; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IEnumerable<ushort> Values |
|||
{ |
|||
get |
|||
{ |
|||
foreach (var range in GetRanges()) |
|||
{ |
|||
for (int codePoint = range.Start; codePoint <= range.End; codePoint++) |
|||
{ |
|||
if (_map.TryGetGlyph(codePoint, out var glyphId)) |
|||
{ |
|||
yield return glyphId; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
int count = 0; |
|||
foreach (var range in GetRanges()) |
|||
{ |
|||
for (int codePoint = range.Start; codePoint <= range.End; codePoint++) |
|||
{ |
|||
if (_map.ContainsGlyph(codePoint)) |
|||
{ |
|||
count++; |
|||
} |
|||
} |
|||
} |
|||
return count; |
|||
} |
|||
} |
|||
|
|||
public bool ContainsKey(int key) |
|||
{ |
|||
return _map.ContainsGlyph(key); |
|||
} |
|||
|
|||
public bool TryGetValue(int key, [MaybeNullWhen(false)] out ushort value) |
|||
{ |
|||
return _map.TryGetGlyph(key, out value); |
|||
} |
|||
|
|||
public IEnumerator<KeyValuePair<int, ushort>> GetEnumerator() |
|||
{ |
|||
foreach (var range in GetRanges()) |
|||
{ |
|||
for (int codePoint = range.Start; codePoint <= range.End; codePoint++) |
|||
{ |
|||
if (_map.TryGetGlyph(codePoint, out var glyphId)) |
|||
{ |
|||
yield return new KeyValuePair<int, ushort>(codePoint, glyphId); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
|
|||
private List<CodepointRange> GetRanges() |
|||
{ |
|||
if (_cachedRanges == null) |
|||
{ |
|||
_cachedRanges = new List<CodepointRange>(); |
|||
var enumerator = _map.GetMappedRanges(); |
|||
while (enumerator.MoveNext()) |
|||
{ |
|||
_cachedRanges.Add(enumerator.Current); |
|||
} |
|||
} |
|||
return _cachedRanges; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,191 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media.Immutable; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Implements painting for COLR v1 glyphs using Avalonia's DrawingContext.
|
|||
/// </summary>
|
|||
internal sealed class ColorGlyphV1Painter : IColorPainter |
|||
{ |
|||
private readonly DrawingContext _drawingContext; |
|||
private readonly ColrContext _context; |
|||
private readonly Stack<IDisposable> _stateStack = new Stack<IDisposable>(); |
|||
|
|||
// Track the pending glyph that needs to be painted with the next fill
|
|||
// In COLR v1, there's a 1:1 mapping between glyph and fill operations
|
|||
private Geometry? _pendingGlyph; |
|||
|
|||
// Track the accumulated transform that should be applied to geometry and brushes
|
|||
private Matrix _accumulatedTransform = Matrix.Identity; |
|||
private readonly Stack<Matrix> _transformStack = new Stack<Matrix>(); |
|||
|
|||
public ColorGlyphV1Painter(DrawingContext drawingContext, ColrContext context) |
|||
{ |
|||
_drawingContext = drawingContext; |
|||
_context = context; |
|||
} |
|||
|
|||
public void PushTransform(Matrix transform) |
|||
{ |
|||
_transformStack.Push(_accumulatedTransform); |
|||
_accumulatedTransform = transform * _accumulatedTransform; |
|||
} |
|||
|
|||
public void PopTransform() |
|||
{ |
|||
if (_transformStack.Count > 0) |
|||
{ |
|||
_accumulatedTransform = _transformStack.Pop(); |
|||
} |
|||
} |
|||
|
|||
public void PushLayer(CompositeMode mode) |
|||
{ |
|||
// COLR v1 composite modes are not fully supported in the base drawing context
|
|||
// For now, we use opacity layers to provide basic composition support
|
|||
// TODO: Implement proper blend mode support when available
|
|||
_stateStack.Push(_drawingContext.PushOpacity(1.0)); |
|||
} |
|||
|
|||
public void PopLayer() |
|||
{ |
|||
if (_stateStack.Count > 0) |
|||
{ |
|||
_stateStack.Pop().Dispose(); |
|||
} |
|||
} |
|||
|
|||
public void PushClip(Rect clipBox) |
|||
{ |
|||
// Transform the clip box with accumulated transforms
|
|||
var transformedClip = clipBox.TransformToAABB(_accumulatedTransform); |
|||
|
|||
_stateStack.Push(_drawingContext.PushClip(transformedClip)); |
|||
} |
|||
|
|||
public void PopClip() |
|||
{ |
|||
if (_stateStack.Count > 0) |
|||
{ |
|||
_stateStack.Pop().Dispose(); |
|||
} |
|||
} |
|||
|
|||
public void FillSolid(Color color) |
|||
{ |
|||
// Render the pending glyph with this solid color
|
|||
if (_pendingGlyph != null) |
|||
{ |
|||
var brush = new ImmutableSolidColorBrush(color); |
|||
|
|||
_drawingContext.DrawGeometry(brush, null, _pendingGlyph); |
|||
|
|||
_pendingGlyph = null; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a brush transform that applies any accumulated transforms.
|
|||
/// </summary>
|
|||
private ImmutableTransform? CreateBrushTransform() |
|||
{ |
|||
return _accumulatedTransform != Matrix.Identity ? new ImmutableTransform(_accumulatedTransform) : null; |
|||
} |
|||
|
|||
public void FillLinearGradient(Point p0, Point p1, GradientStop[] stops, GradientSpreadMethod extend) |
|||
{ |
|||
if (_pendingGlyph != null) |
|||
{ |
|||
var gradientStops = new ImmutableGradientStop[stops.Length]; |
|||
|
|||
for (var i = 0; i < stops.Length; i++) |
|||
{ |
|||
gradientStops[i] = new ImmutableGradientStop(stops[i].Offset, stops[i].Color); |
|||
} |
|||
|
|||
var brush = new ImmutableLinearGradientBrush( |
|||
gradientStops: gradientStops, |
|||
opacity: 1.0, |
|||
transform: CreateBrushTransform(), |
|||
transformOrigin: new RelativePoint(0, 0, RelativeUnit.Absolute), |
|||
spreadMethod: extend, |
|||
startPoint: new RelativePoint(p0, RelativeUnit.Absolute), |
|||
endPoint: new RelativePoint(p1, RelativeUnit.Absolute)); |
|||
|
|||
_drawingContext.DrawGeometry(brush, null, _pendingGlyph); |
|||
_pendingGlyph = null; |
|||
} |
|||
} |
|||
|
|||
public void FillRadialGradient(Point c0, double r0, Point c1, double r1, GradientStop[] stops, GradientSpreadMethod extend) |
|||
{ |
|||
if (_pendingGlyph != null) |
|||
{ |
|||
// Avalonia's RadialGradientBrush doesn't support two-point gradients with different radii
|
|||
// We approximate by using the larger circle as the gradient
|
|||
var center = r1 > r0 ? c1 : c0; |
|||
var radius = Math.Max(r0, r1); |
|||
|
|||
var gradientStops = new ImmutableGradientStop[stops.Length]; |
|||
|
|||
for (var i = 0; i < stops.Length; i++) |
|||
{ |
|||
gradientStops[i] = new ImmutableGradientStop(stops[i].Offset, stops[i].Color); |
|||
} |
|||
|
|||
var brush = new ImmutableRadialGradientBrush( |
|||
gradientStops: gradientStops, |
|||
opacity: 1.0, |
|||
transform: CreateBrushTransform(), |
|||
transformOrigin: new RelativePoint(0, 0, RelativeUnit.Absolute), |
|||
spreadMethod: extend, |
|||
center: new RelativePoint(center, RelativeUnit.Absolute), |
|||
gradientOrigin: new RelativePoint(center, RelativeUnit.Absolute), |
|||
radiusX: new RelativeScalar(radius, RelativeUnit.Absolute), |
|||
radiusY: new RelativeScalar(radius, RelativeUnit.Absolute)); |
|||
|
|||
_drawingContext.DrawGeometry(brush, null, _pendingGlyph); |
|||
_pendingGlyph = null; |
|||
} |
|||
} |
|||
|
|||
public void FillConicGradient(Point center, double startAngle, double endAngle, GradientStop[] stops, GradientSpreadMethod extend) |
|||
{ |
|||
if (_pendingGlyph != null) |
|||
{ |
|||
var gradientStops = new ImmutableGradientStop[stops.Length]; |
|||
|
|||
for (var i = 0; i < stops.Length; i++) |
|||
{ |
|||
gradientStops[i] = new ImmutableGradientStop(stops[i].Offset, stops[i].Color); |
|||
} |
|||
|
|||
var brush = new ImmutableConicGradientBrush( |
|||
gradientStops: gradientStops, |
|||
opacity: 1.0, |
|||
transform: CreateBrushTransform(), |
|||
transformOrigin: new RelativePoint(0, 0, RelativeUnit.Absolute), |
|||
spreadMethod: extend, |
|||
center: new RelativePoint(center, RelativeUnit.Absolute), |
|||
angle: startAngle); |
|||
|
|||
_drawingContext.DrawGeometry(brush, null, _pendingGlyph); |
|||
|
|||
_pendingGlyph = null; |
|||
} |
|||
} |
|||
|
|||
public void Glyph(ushort glyphId) |
|||
{ |
|||
// Store the glyph geometry to be rendered when we encounter the fill
|
|||
var geometry = _context.GlyphTypeface.GetGlyphOutline(glyphId, _accumulatedTransform); |
|||
|
|||
if (geometry != null) |
|||
{ |
|||
_pendingGlyph = geometry; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,191 @@ |
|||
using System; |
|||
using System.Drawing; |
|||
using System.Linq; |
|||
using Avalonia.Media.Immutable; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Context for parsing Paint tables and resolving ResolvedPaint, providing access to required tables and data.
|
|||
/// </summary>
|
|||
internal readonly struct ColrContext |
|||
{ |
|||
public ColrContext( |
|||
GlyphTypeface glyphTypeface, |
|||
ColrTable colrTable, |
|||
CpalTable cpalTable, |
|||
int paletteIndex) |
|||
{ |
|||
GlyphTypeface = glyphTypeface; |
|||
ColrData = colrTable.ColrData; |
|||
ColrTable = colrTable; |
|||
CpalTable = cpalTable; |
|||
PaletteIndex = paletteIndex; |
|||
} |
|||
|
|||
public GlyphTypeface GlyphTypeface { get; } |
|||
public ReadOnlyMemory<byte> ColrData { get; } |
|||
public ColrTable ColrTable { get; } |
|||
public CpalTable CpalTable { get; } |
|||
public int PaletteIndex { get; } |
|||
|
|||
/// <summary>
|
|||
/// Applies alpha delta to a color.
|
|||
/// </summary>
|
|||
public Color ApplyAlphaDelta(Color color, uint varIndexBase, int deltaIndex) |
|||
{ |
|||
if (!ColrTable.TryGetVariationDeltaSet(varIndexBase, out var deltaSet)) |
|||
{ |
|||
return color; |
|||
} |
|||
|
|||
if (deltaIndex >= deltaSet.Count) |
|||
{ |
|||
return color; |
|||
} |
|||
|
|||
// Alpha deltas are F2DOT14 format
|
|||
var alphaDelta = deltaSet.GetF2Dot14Delta(deltaIndex); |
|||
|
|||
var newAlpha = Math.Clamp(color.A / 255.0 + alphaDelta, 0.0, 1.0); |
|||
|
|||
return Color.FromArgb((byte)(newAlpha * 255), color.R, color.G, color.B); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies affine transformation deltas to a matrix.
|
|||
/// </summary>
|
|||
public Matrix ApplyAffineDeltas(Matrix matrix, uint varIndexBase) |
|||
{ |
|||
if (!ColrTable.TryGetVariationDeltaSet(varIndexBase, out var deltaSet)) |
|||
{ |
|||
return matrix; |
|||
} |
|||
|
|||
// Affine2x3 matrix component deltas are Fixed format (16.16)
|
|||
// Note: Depending on the spec, these might need to be treated differently
|
|||
// For now, using the raw deltas as they might already be in the correct format
|
|||
var m11 = matrix.M11 + (deltaSet.Count > 0 ? deltaSet.GetFixedDelta(0) : 0.0); |
|||
var m12 = matrix.M12 + (deltaSet.Count > 1 ? deltaSet.GetFixedDelta(1) : 0.0); |
|||
var m21 = matrix.M21 + (deltaSet.Count > 2 ? deltaSet.GetFixedDelta(2) : 0.0); |
|||
var m22 = matrix.M22 + (deltaSet.Count > 3 ? deltaSet.GetFixedDelta(3) : 0.0); |
|||
var m31 = matrix.M31 + (deltaSet.Count > 4 ? deltaSet.GetFixedDelta(4) : 0.0); |
|||
var m32 = matrix.M32 + (deltaSet.Count > 5 ? deltaSet.GetFixedDelta(5) : 0.0); |
|||
|
|||
return new Matrix(m11, m12, m21, m22, m31, m32); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Resolves color stops with variation deltas applied and normalized to 0-1 range.
|
|||
/// Based on fontations ColorStops::resolve and gradient normalization logic.
|
|||
/// </summary>
|
|||
public GradientStop[] ResolveColorStops( |
|||
GradientStopVar[] stops, |
|||
uint? varIndexBase) |
|||
{ |
|||
if (stops.Length == 0) |
|||
return Array.Empty<GradientStop>(); |
|||
|
|||
// No variation deltas to apply, just normalize
|
|||
if (!varIndexBase.HasValue) |
|||
{ |
|||
return NormalizeColorStops(stops); |
|||
} |
|||
|
|||
// Check if we should apply variations
|
|||
DeltaSet deltaSet = default; |
|||
var shouldApplyVariations = ColrTable.TryGetVariationDeltaSet(varIndexBase.Value, out deltaSet); |
|||
|
|||
GradientStop[]? resolvedStops = null; |
|||
|
|||
if (shouldApplyVariations) |
|||
{ |
|||
resolvedStops = new GradientStop[stops.Length]; |
|||
|
|||
for (int i = 0; i < stops.Length; i++) |
|||
{ |
|||
var stop = stops[i]; |
|||
var offset = stop.Offset; |
|||
var color = stop.Color; |
|||
|
|||
if (deltaSet.Count >= 2) |
|||
{ |
|||
// Stop offset and alpha deltas are F2DOT14 format
|
|||
offset += deltaSet.GetF2Dot14Delta(0); |
|||
|
|||
// Apply alpha delta
|
|||
var alphaDelta = deltaSet.GetF2Dot14Delta(1); |
|||
var newAlpha = Math.Clamp(color.A / 255.0 + alphaDelta, 0.0, 1.0); |
|||
color = Color.FromArgb((byte)(newAlpha * 255), color.R, color.G, color.B); |
|||
} |
|||
|
|||
resolvedStops[i] = new GradientStop(offset, color); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
resolvedStops = stops; |
|||
} |
|||
|
|||
return NormalizeColorStops(resolvedStops); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Normalizes color stops to 0-1 range and handles edge cases.
|
|||
/// Modifies the array in-place to avoid allocations when possible.
|
|||
/// </summary>
|
|||
public GradientStop[] NormalizeColorStops(GradientStop[] stops) |
|||
{ |
|||
if (stops.Length == 0) |
|||
return stops; |
|||
|
|||
// Sort by offset (ImmutableGradientStop is immutable, so we need to sort the array)
|
|||
Array.Sort(stops, (a, b) => a.Offset.CompareTo(b.Offset)); |
|||
|
|||
// Get first and last stops for normalization
|
|||
var firstStop = stops[0]; |
|||
var lastStop = stops[stops.Length - 1]; |
|||
var colorStopRange = lastStop.Offset - firstStop.Offset; |
|||
|
|||
// If all stops are at the same position
|
|||
if (Math.Abs(colorStopRange) < 1e-6) |
|||
{ |
|||
// For Pad mode with zero range, add an extra stop
|
|||
if (colorStopRange == 0.0) |
|||
{ |
|||
var newStops = new GradientStop[stops.Length + 1]; |
|||
Array.Copy(stops, newStops, stops.Length); |
|||
newStops[stops.Length] = new GradientStop(lastStop.Offset + 1.0, lastStop.Color); |
|||
stops = newStops; |
|||
colorStopRange = 1.0; |
|||
firstStop = stops[0]; |
|||
} |
|||
else |
|||
{ |
|||
return stops; |
|||
} |
|||
} |
|||
|
|||
// Check if normalization is needed
|
|||
var needsNormalization = Math.Abs(colorStopRange - 1.0) > 1e-6 || Math.Abs(firstStop.Offset) > 1e-6; |
|||
|
|||
if (!needsNormalization) |
|||
return stops; |
|||
|
|||
// Normalize stops to 0-1 range
|
|||
var scale = 1.0 / colorStopRange; |
|||
var startOffset = firstStop.Offset; |
|||
|
|||
// Create new array with normalized values
|
|||
var normalizedStops = new GradientStop[stops.Length]; |
|||
for (int i = 0; i < stops.Length; i++) |
|||
{ |
|||
var stop = stops[i]; |
|||
var normalizedOffset = (stop.Offset - startOffset) * scale; |
|||
normalizedStops[i] = new GradientStop(normalizedOffset, stop.Color); |
|||
} |
|||
|
|||
return normalizedStops; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,765 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using System.Diagnostics; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Reader for the 'COLR' (Color) table. Provides access to layered color glyph data.
|
|||
/// Supports COLR v0 and v1 formats.
|
|||
/// </summary>
|
|||
internal sealed class ColrTable |
|||
{ |
|||
internal const string TableName = "COLR"; |
|||
|
|||
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); |
|||
|
|||
private readonly ReadOnlyMemory<byte> _colrData; |
|||
private readonly ushort _version; |
|||
private readonly ushort _numBaseGlyphRecords; |
|||
private readonly uint _baseGlyphRecordsOffset; |
|||
private readonly uint _layerRecordsOffset; |
|||
private readonly ushort _numLayerRecords; |
|||
|
|||
// COLR v1 fields
|
|||
private readonly uint _baseGlyphV1ListOffset; |
|||
private readonly uint _layerV1ListOffset; |
|||
private readonly uint _clipListOffset; |
|||
private readonly uint _varIndexMapOffset; |
|||
private readonly uint _itemVariationStoreOffset; |
|||
|
|||
// Cached DeltaSetIndexMap (loaded during construction)
|
|||
private readonly DeltaSetIndexMap? _deltaSetIndexMap; |
|||
|
|||
// Cached ItemVariationStore (loaded during construction)
|
|||
private readonly ItemVariationStore? _itemVariationStore; |
|||
|
|||
private ColrTable( |
|||
ReadOnlyMemory<byte> colrData, |
|||
ushort version, |
|||
ushort numBaseGlyphRecords, |
|||
uint baseGlyphRecordsOffset, |
|||
uint layerRecordsOffset, |
|||
ushort numLayerRecords, |
|||
uint baseGlyphV1ListOffset = 0, |
|||
uint layerV1ListOffset = 0, |
|||
uint clipListOffset = 0, |
|||
uint varIndexMapOffset = 0, |
|||
uint itemVariationStoreOffset = 0, |
|||
DeltaSetIndexMap? deltaSetIndexMap = null, |
|||
ItemVariationStore? itemVariationStore = null) |
|||
{ |
|||
_colrData = colrData; |
|||
_version = version; |
|||
_numBaseGlyphRecords = numBaseGlyphRecords; |
|||
_baseGlyphRecordsOffset = baseGlyphRecordsOffset; |
|||
_layerRecordsOffset = layerRecordsOffset; |
|||
_numLayerRecords = numLayerRecords; |
|||
_baseGlyphV1ListOffset = baseGlyphV1ListOffset; |
|||
_layerV1ListOffset = layerV1ListOffset; |
|||
_clipListOffset = clipListOffset; |
|||
_varIndexMapOffset = varIndexMapOffset; |
|||
_itemVariationStoreOffset = itemVariationStoreOffset; |
|||
_deltaSetIndexMap = deltaSetIndexMap; |
|||
_itemVariationStore = itemVariationStore; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the version of the COLR table (0 or 1).
|
|||
/// </summary>
|
|||
public ushort Version => _version; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of base glyph records (v0).
|
|||
/// </summary>
|
|||
public int BaseGlyphCount => _numBaseGlyphRecords; |
|||
|
|||
/// <summary>
|
|||
/// Gets whether this table has COLR v1 data.
|
|||
/// </summary>
|
|||
public bool HasV1Data => _version >= 1 && _baseGlyphV1ListOffset > 0; |
|||
|
|||
/// <summary>
|
|||
/// Gets the LayerV1List offset from the COLR table header.
|
|||
/// Returns 0 if the LayerV1List is not present (COLR v0 or no LayerV1List in v1).
|
|||
/// </summary>
|
|||
public uint LayerV1ListOffset => _layerV1ListOffset; |
|||
|
|||
public ReadOnlyMemory<byte> ColrData => _colrData; |
|||
|
|||
/// <summary>
|
|||
/// Attempts to load the COLR (Color) table from the specified glyph typeface.
|
|||
/// </summary>
|
|||
/// <remarks>This method supports both COLR version 0 and version 1 tables, as defined in the
|
|||
/// OpenType specification. If the COLR table is not present or is invalid, the method returns false and sets
|
|||
/// colrTable to null.</remarks>
|
|||
/// <param name="glyphTypeface">The glyph typeface from which to load the COLR table. Cannot be null.</param>
|
|||
/// <param name="colrTable">When this method returns, contains the loaded COLR table if successful; otherwise, null. This parameter is
|
|||
/// passed uninitialized.</param>
|
|||
/// <returns>true if the COLR table was successfully loaded; otherwise, false.</returns>
|
|||
public static bool TryLoad(GlyphTypeface glyphTypeface, [NotNullWhen(true)] out ColrTable? colrTable) |
|||
{ |
|||
colrTable = null; |
|||
|
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var colrData)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (colrData.Length < 14) |
|||
{ |
|||
return false; // Minimum size for COLR v0 header
|
|||
} |
|||
|
|||
var span = colrData.Span; |
|||
|
|||
// Parse COLR table header (v0)
|
|||
// uint16 version
|
|||
// uint16 numBaseGlyphRecords
|
|||
// Offset32 baseGlyphRecordsOffset
|
|||
// Offset32 layerRecordsOffset
|
|||
// uint16 numLayerRecords
|
|||
|
|||
var version = BinaryPrimitives.ReadUInt16BigEndian(span); |
|||
var numBaseGlyphRecords = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(2)); |
|||
var baseGlyphRecordsOffset = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(4)); |
|||
var layerRecordsOffset = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(8)); |
|||
var numLayerRecords = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(12)); |
|||
|
|||
// Validate v0 offsets
|
|||
if (baseGlyphRecordsOffset >= colrData.Length || layerRecordsOffset >= colrData.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// Parse COLR v1 extensions if present
|
|||
uint baseGlyphV1ListOffset = 0; |
|||
uint layerV1ListOffset = 0; |
|||
uint clipListOffset = 0; |
|||
uint varIndexMapOffset = 0; |
|||
uint itemVariationStoreOffset = 0; |
|||
|
|||
if (version >= 1) |
|||
{ |
|||
// COLR v1 adds additional fields after the v0 header
|
|||
// Offset32 baseGlyphV1ListOffset (14 bytes)
|
|||
// Offset32 layerV1ListOffset (18 bytes)
|
|||
// Offset32 clipListOffset (22 bytes) - optional
|
|||
// Offset32 varIndexMapOffset (26 bytes) - optional
|
|||
// Offset32 itemVariationStoreOffset (30 bytes) - optional
|
|||
|
|||
if (colrData.Length >= 22) |
|||
{ |
|||
baseGlyphV1ListOffset = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(14)); |
|||
layerV1ListOffset = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(18)); |
|||
} |
|||
|
|||
if (colrData.Length >= 26) |
|||
{ |
|||
clipListOffset = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(22)); |
|||
} |
|||
|
|||
if (colrData.Length >= 30) |
|||
{ |
|||
varIndexMapOffset = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(26)); |
|||
} |
|||
|
|||
if (colrData.Length >= 34) |
|||
{ |
|||
itemVariationStoreOffset = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(30)); |
|||
} |
|||
|
|||
// Note: 0 offset means the optional table is not present
|
|||
} |
|||
|
|||
// Load DeltaSetIndexMap if present
|
|||
DeltaSetIndexMap? deltaSetIndexMap = null; |
|||
if (version >= 1 && varIndexMapOffset > 0) |
|||
{ |
|||
deltaSetIndexMap = DeltaSetIndexMap.Load(colrData, varIndexMapOffset); |
|||
} |
|||
|
|||
// Load ItemVariationStore if present
|
|||
ItemVariationStore? itemVariationStore = null; |
|||
if (version >= 1 && itemVariationStoreOffset > 0) |
|||
{ |
|||
itemVariationStore = ItemVariationStore.Load(colrData, itemVariationStoreOffset); |
|||
} |
|||
|
|||
colrTable = new ColrTable( |
|||
colrData, |
|||
version, |
|||
numBaseGlyphRecords, |
|||
baseGlyphRecordsOffset, |
|||
layerRecordsOffset, |
|||
numLayerRecords, |
|||
baseGlyphV1ListOffset, |
|||
layerV1ListOffset, |
|||
clipListOffset, |
|||
varIndexMapOffset, |
|||
itemVariationStoreOffset, |
|||
deltaSetIndexMap, |
|||
itemVariationStore); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to find the base glyph record for the specified glyph ID (v0 format).
|
|||
/// Uses binary search for efficient lookup.
|
|||
/// </summary>
|
|||
public bool TryGetBaseGlyphRecord(ushort glyphId, out BaseGlyphRecord record) |
|||
{ |
|||
record = default; |
|||
|
|||
if (_numBaseGlyphRecords == 0) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = _colrData.Span; |
|||
var baseRecordsSpan = span.Slice((int)_baseGlyphRecordsOffset); |
|||
|
|||
// Binary search for the glyph ID
|
|||
int low = 0; |
|||
int high = _numBaseGlyphRecords - 1; |
|||
|
|||
while (low <= high) |
|||
{ |
|||
int mid = low + (high - low) / 2; |
|||
int offset = mid * 6; // Each BaseGlyphRecord is 6 bytes
|
|||
|
|||
if (offset + 6 > baseRecordsSpan.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var recordSpan = baseRecordsSpan.Slice(offset, 6); |
|||
var recordGlyphId = BinaryPrimitives.ReadUInt16BigEndian(recordSpan); |
|||
|
|||
if (recordGlyphId == glyphId) |
|||
{ |
|||
// Found it
|
|||
var firstLayerIndex = BinaryPrimitives.ReadUInt16BigEndian(recordSpan.Slice(2)); |
|||
var numLayers = BinaryPrimitives.ReadUInt16BigEndian(recordSpan.Slice(4)); |
|||
|
|||
record = new BaseGlyphRecord(glyphId, firstLayerIndex, numLayers); |
|||
return true; |
|||
} |
|||
else if (recordGlyphId < glyphId) |
|||
{ |
|||
low = mid + 1; |
|||
} |
|||
else |
|||
{ |
|||
high = mid - 1; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the layer record at the specified index (v0 format).
|
|||
/// /// Gets the number of base glyph records (v0).
|
|||
/// /// Gets whether this table has COLR v1 data.
|
|||
/// /// Gets the LayerV1List offset from the COLR table header.
|
|||
/// Returns 0 if the LayerV1List is not present (COLR v0 or no LayerV1List in v1).
|
|||
/// /// </summary>
|
|||
public bool TryGetLayerRecord(int layerIndex, out LayerRecord record) |
|||
{ |
|||
record = default; |
|||
|
|||
if (layerIndex < 0 || layerIndex >= _numLayerRecords) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = _colrData.Span; |
|||
var layerRecordsSpan = span.Slice((int)_layerRecordsOffset); |
|||
|
|||
int offset = layerIndex * 4; // Each LayerRecord is 4 bytes
|
|||
|
|||
if (offset + 4 > layerRecordsSpan.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var recordSpan = layerRecordsSpan.Slice(offset, 4); |
|||
var glyphId = BinaryPrimitives.ReadUInt16BigEndian(recordSpan); |
|||
var paletteIndex = BinaryPrimitives.ReadUInt16BigEndian(recordSpan.Slice(2)); |
|||
|
|||
record = new LayerRecord(glyphId, paletteIndex); |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets all layers for the specified glyph ID.
|
|||
/// Returns an empty array if the glyph has no color layers.
|
|||
/// </summary>
|
|||
public LayerRecord[] GetLayers(ushort glyphId) |
|||
{ |
|||
if (!TryGetBaseGlyphRecord(glyphId, out var baseRecord)) |
|||
{ |
|||
return Array.Empty<LayerRecord>(); |
|||
} |
|||
|
|||
var layers = new LayerRecord[baseRecord.NumLayers]; |
|||
|
|||
for (int i = 0; i < baseRecord.NumLayers; i++) |
|||
{ |
|||
if (TryGetLayerRecord(baseRecord.FirstLayerIndex + i, out var layer)) |
|||
{ |
|||
layers[i] = layer; |
|||
} |
|||
} |
|||
|
|||
return layers; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get the v1 base glyph record for the specified glyph ID.
|
|||
/// </summary>
|
|||
public bool TryGetBaseGlyphV1Record(ushort glyphId, out BaseGlyphV1Record record) |
|||
{ |
|||
record = default; |
|||
|
|||
if (!HasV1Data) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = _colrData.Span; |
|||
var baseGlyphV1ListSpan = span.Slice((int)_baseGlyphV1ListOffset); |
|||
|
|||
// BaseGlyphV1List format:
|
|||
// uint32 numBaseGlyphV1Records
|
|||
// BaseGlyphV1Record[numBaseGlyphV1Records] (sorted by glyphID)
|
|||
|
|||
if (baseGlyphV1ListSpan.Length < 4) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var numRecords = BinaryPrimitives.ReadUInt32BigEndian(baseGlyphV1ListSpan); |
|||
|
|||
Debug.Assert(4 + numRecords * 6 <= _colrData.Length - _baseGlyphV1ListOffset); |
|||
|
|||
int low = 0; |
|||
int high = (int)numRecords - 1; |
|||
int recordOffset = 4; |
|||
|
|||
while (low <= high) |
|||
{ |
|||
int mid = low + (high - low) / 2; |
|||
|
|||
// 6 bytes per record: 2 (glyphID) + 4 (Offset32 paintOffset)
|
|||
int offset = recordOffset + (mid * 6); |
|||
|
|||
if (offset + 6 > baseGlyphV1ListSpan.Length) |
|||
return false; |
|||
|
|||
var recordSpan = baseGlyphV1ListSpan.Slice(offset, 6); |
|||
var recordGlyphId = BinaryPrimitives.ReadUInt16BigEndian(recordSpan); |
|||
|
|||
if (recordGlyphId == glyphId) |
|||
{ |
|||
var paintOffset = BinaryPrimitives.ReadUInt32BigEndian(recordSpan.Slice(2)); |
|||
record = new BaseGlyphV1Record(glyphId, paintOffset); |
|||
return true; |
|||
} |
|||
else if (recordGlyphId < glyphId) |
|||
{ |
|||
low = mid + 1; |
|||
} |
|||
else |
|||
{ |
|||
high = mid - 1; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a paint offset from BaseGlyphV1Record (relative to BaseGlyphV1List) to an absolute offset within the COLR table.
|
|||
/// According to OpenType COLR v1 specification, offsets in BaseGlyphV1Records are relative to the start of the BaseGlyphV1List.
|
|||
/// </summary>
|
|||
/// <param name="relativePaintOffset">The paint offset from a BaseGlyphV1Record, relative to the BaseGlyphV1List.</param>
|
|||
/// <returns>The absolute offset within the COLR table.</returns>
|
|||
internal uint GetAbsolutePaintOffset(uint relativePaintOffset) |
|||
{ |
|||
// According to the OpenType spec, paint offsets in BaseGlyphV1Records are
|
|||
// relative to the BaseGlyphV1List, so we add the BaseGlyphV1List offset to get
|
|||
// the absolute position in the COLR table
|
|||
return _baseGlyphV1ListOffset + relativePaintOffset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to resolve and retrieve the paint definition for the specified glyph, if available.
|
|||
/// </summary>
|
|||
/// <remarks>This method returns <see langword="false"/> if the glyph does not have a version 1
|
|||
/// base glyph record or if the paint cannot be parsed or resolved. The output parameter <paramref
|
|||
/// name="paint"/> is set only when the method returns <see langword="true"/>.</remarks>
|
|||
/// <param name="context">The context containing color and font information used to resolve the paint.</param>
|
|||
/// <param name="glyphId">The identifier of the glyph for which to retrieve the resolved paint.</param>
|
|||
/// <param name="paint">When this method returns, contains the resolved paint for the specified glyph if the operation succeeds;
|
|||
/// otherwise, <see langword="null"/>. This parameter is passed uninitialized.</param>
|
|||
/// <returns><see langword="true"/> if the resolved paint was successfully retrieved; otherwise, <see langword="false"/>.</returns>
|
|||
public bool TryGetResolvedPaint(ColrContext context, ushort glyphId, [NotNullWhen(true)] out Paint? paint) |
|||
{ |
|||
paint = null; |
|||
|
|||
if (!TryGetBaseGlyphV1Record(glyphId, out var record)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var absolutePaintOffset = GetAbsolutePaintOffset(record.PaintOffset); |
|||
|
|||
var decycler = PaintDecycler.Rent(); |
|||
try |
|||
{ |
|||
if (!PaintParser.TryParse(_colrData.Span, absolutePaintOffset, in context, in decycler, out var parsedPaint)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
paint = PaintResolver.ResolvePaint(parsedPaint, in context); |
|||
|
|||
return true; |
|||
} |
|||
finally |
|||
{ |
|||
PaintDecycler.Return(decycler); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Checks if the specified glyph has color layers defined (v0 or v1).
|
|||
/// </summary>
|
|||
public bool HasColorLayers(ushort glyphId) |
|||
{ |
|||
// Check v0 first
|
|||
if (TryGetBaseGlyphRecord(glyphId, out _)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
// Check v1
|
|||
if (HasV1Data && TryGetBaseGlyphV1Record(glyphId, out _)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a complete delta set for the specified variation index.
|
|||
/// </summary>
|
|||
/// <param name="variationIndex">The index to look up in the DeltaSetIndexMap.</param>
|
|||
/// <param name="deltaSet">A DeltaSet ref struct providing format-aware access to deltas.</param>
|
|||
/// <returns>True if variation deltas were found; otherwise false.</returns>
|
|||
/// <remarks>
|
|||
/// This method uses the DeltaSetIndexMap to map the variation index to an (outer, inner) index pair,
|
|||
/// then retrieves the corresponding delta set from the ItemVariationStore.
|
|||
/// If no DeltaSetIndexMap is present, an implicit 1:1 mapping is used.
|
|||
/// The DeltaSet ref struct provides allocation-free access to both word deltas (16-bit) and byte deltas (8-bit).
|
|||
/// </remarks>
|
|||
public bool TryGetVariationDeltaSet(uint variationIndex, out DeltaSet deltaSet) |
|||
{ |
|||
deltaSet = DeltaSet.Empty; |
|||
|
|||
// Magic value 0xFFFFFFFF indicates no variation deltas should be applied
|
|||
const uint NO_VARIATION_DELTAS = 0xFFFFFFFF; |
|||
|
|||
if (variationIndex == NO_VARIATION_DELTAS) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// ItemVariationStore is required
|
|||
if (!HasV1Data || _itemVariationStore == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// If DeltaSetIndexMap is present, use it for mapping
|
|||
if (_deltaSetIndexMap != null) |
|||
{ |
|||
return _deltaSetIndexMap.TryGetVariationDeltaSet(_itemVariationStore, variationIndex, out deltaSet); |
|||
} |
|||
|
|||
// Otherwise, use implicit 1:1 mapping as per OpenType spec
|
|||
if (variationIndex > ushort.MaxValue) |
|||
{ |
|||
return false; // Index too large for implicit mapping
|
|||
} |
|||
|
|||
var implicitOuterIndex = (ushort)variationIndex; |
|||
var implicitInnerIndex = (ushort)variationIndex; |
|||
|
|||
return _itemVariationStore.TryGetDeltaSet(implicitOuterIndex, implicitInnerIndex, out deltaSet); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get the clip box for a specified glyph ID from the ClipList (COLR v1).
|
|||
/// </summary>
|
|||
/// <param name="glyphId">The glyph ID to get the clip box for.</param>
|
|||
/// <param name="clipBox">The clip box rectangle, or null if no clip box is defined.</param>
|
|||
/// <returns>True if a clip box was found; otherwise false.</returns>
|
|||
public bool TryGetClipBox(ushort glyphId, out Rect clipBox) |
|||
{ |
|||
clipBox = default; |
|||
|
|||
// ClipList is only available in COLR v1
|
|||
if (!HasV1Data || _clipListOffset == 0) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = _colrData.Span; |
|||
|
|||
if (_clipListOffset >= span.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var clipListSpan = span.Slice((int)_clipListOffset); |
|||
|
|||
// ClipList format:
|
|||
// uint8 format (must be 1)
|
|||
// uint32 numClips
|
|||
// ClipRecord[numClips] (sorted by startGlyphID)
|
|||
|
|||
if (clipListSpan.Length < 5) // format (1) + numClips (4)
|
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var format = clipListSpan[0]; |
|||
if (format != 1) |
|||
{ |
|||
return false; // Only format 1 is defined
|
|||
} |
|||
|
|||
var numClips = BinaryPrimitives.ReadUInt32BigEndian(clipListSpan.Slice(1)); |
|||
|
|||
if (numClips == 0) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// Binary search for the clip record
|
|||
// ClipRecord format:
|
|||
// uint16 startGlyphID
|
|||
// uint16 endGlyphID
|
|||
// Offset24 clipBoxOffset (relative to start of ClipList)
|
|||
|
|||
int recordSize = 7; // 2 + 2 + 3
|
|||
int low = 0; |
|||
int high = (int)numClips - 1; |
|||
int recordsOffset = 5; // After format + numClips
|
|||
|
|||
while (low <= high) |
|||
{ |
|||
int mid = low + (high - low) / 2; |
|||
int offset = recordsOffset + (mid * recordSize); |
|||
|
|||
if (offset + recordSize > clipListSpan.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var recordSpan = clipListSpan.Slice(offset, recordSize); |
|||
var startGlyphId = BinaryPrimitives.ReadUInt16BigEndian(recordSpan); |
|||
var endGlyphId = BinaryPrimitives.ReadUInt16BigEndian(recordSpan.Slice(2)); |
|||
|
|||
if (glyphId >= startGlyphId && glyphId <= endGlyphId) |
|||
{ |
|||
// Found the clip record - parse the clip box
|
|||
var clipBoxOffset = ReadOffset24(recordSpan.Slice(4)); |
|||
var absoluteClipBoxOffset = _clipListOffset + clipBoxOffset; |
|||
|
|||
return TryParseClipBox(span, absoluteClipBoxOffset, out clipBox); |
|||
} |
|||
else if (glyphId < startGlyphId) |
|||
{ |
|||
high = mid - 1; |
|||
} |
|||
else |
|||
{ |
|||
low = mid + 1; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to parse a ClipBox from the specified offset.
|
|||
/// </summary>
|
|||
private static bool TryParseClipBox(ReadOnlySpan<byte> data, uint offset, out Rect clipBox) |
|||
{ |
|||
clipBox = default; |
|||
|
|||
if (offset >= data.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = data.Slice((int)offset); |
|||
|
|||
// ClipBox format (format 1 or 2):
|
|||
// uint8 format
|
|||
// For format 1 (FWORD values):
|
|||
// FWORD xMin
|
|||
// FWORD yMin
|
|||
// FWORD xMax
|
|||
// FWORD yMax
|
|||
// For format 2 (FWORD + VarIndexBase for each):
|
|||
// FWORD xMin, uint32 varIndexBase
|
|||
// FWORD yMin, uint32 varIndexBase
|
|||
// FWORD xMax, uint32 varIndexBase
|
|||
// FWORD yMax, uint32 varIndexBase
|
|||
|
|||
if (span.Length < 1) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var format = span[0]; |
|||
|
|||
if (format == 1) |
|||
{ |
|||
// Format 1: Fixed ClipBox
|
|||
if (span.Length < 9) // format (1) + 4 FWORDs (8)
|
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var xMin = BinaryPrimitives.ReadInt16BigEndian(span.Slice(1)); |
|||
var yMin = BinaryPrimitives.ReadInt16BigEndian(span.Slice(3)); |
|||
var xMax = BinaryPrimitives.ReadInt16BigEndian(span.Slice(5)); |
|||
var yMax = BinaryPrimitives.ReadInt16BigEndian(span.Slice(7)); |
|||
|
|||
// Keep in font-space coordinates (Y-up)
|
|||
// The Y-flip transformation is applied at the root level in PaintResolver
|
|||
clipBox = new Rect(xMin, yMin, xMax - xMin, yMax - yMin); |
|||
return true; |
|||
} |
|||
else if (format == 2) |
|||
{ |
|||
// Format 2: Variable ClipBox (ignore VarIndexBase for now)
|
|||
if (span.Length < 25) // format (1) + 4 * (FWORD (2) + VarIndexBase (4))
|
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var xMin = BinaryPrimitives.ReadInt16BigEndian(span.Slice(1)); |
|||
// Skip VarIndexBase at span[3..7]
|
|||
var yMin = BinaryPrimitives.ReadInt16BigEndian(span.Slice(7)); |
|||
// Skip VarIndexBase at span[9..13]
|
|||
var xMax = BinaryPrimitives.ReadInt16BigEndian(span.Slice(13)); |
|||
// Skip VarIndexBase at span[15..19]
|
|||
var yMax = BinaryPrimitives.ReadInt16BigEndian(span.Slice(19)); |
|||
// Skip VarIndexBase at span[21..25]
|
|||
|
|||
// Keep in font-space coordinates (Y-up)
|
|||
// The Y-flip transformation is applied at the root level in PaintResolver
|
|||
clipBox = new Rect(xMin, yMin, xMax - xMin, yMax - yMin); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a 24-bit offset (3 bytes, big-endian).
|
|||
/// </summary>
|
|||
private static uint ReadOffset24(ReadOnlySpan<byte> span) |
|||
{ |
|||
return ((uint)span[0] << 16) | ((uint)span[1] << 8) | span[2]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a base glyph record in the COLR table (v0).
|
|||
/// Maps a glyph ID to its color layers.
|
|||
/// </summary>
|
|||
internal readonly struct BaseGlyphRecord |
|||
{ |
|||
public BaseGlyphRecord(ushort glyphId, ushort firstLayerIndex, ushort numLayers) |
|||
{ |
|||
GlyphId = glyphId; |
|||
FirstLayerIndex = firstLayerIndex; |
|||
NumLayers = numLayers; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the glyph ID of the base glyph.
|
|||
/// </summary>
|
|||
public ushort GlyphId { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the first layer record for this glyph.
|
|||
/// </summary>
|
|||
public ushort FirstLayerIndex { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of color layers for this glyph.
|
|||
/// </summary>
|
|||
public ushort NumLayers { get; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a v1 base glyph record in the COLR table.
|
|||
/// Maps a glyph ID to a paint offset.
|
|||
/// </summary>
|
|||
internal readonly struct BaseGlyphV1Record |
|||
{ |
|||
public BaseGlyphV1Record(ushort glyphId, uint paintOffset) |
|||
{ |
|||
GlyphId = glyphId; |
|||
PaintOffset = paintOffset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the glyph ID of the base glyph.
|
|||
/// </summary>
|
|||
public ushort GlyphId { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the offset to the paint table for this glyph.
|
|||
/// </summary>
|
|||
public uint PaintOffset { get; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a layer record in the COLR table (v0).
|
|||
/// Each layer references a glyph and a color palette index.
|
|||
/// </summary>
|
|||
internal readonly struct LayerRecord |
|||
{ |
|||
public LayerRecord(ushort glyphId, ushort paletteIndex) |
|||
{ |
|||
GlyphId = glyphId; |
|||
PaletteIndex = paletteIndex; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the glyph ID for this layer.
|
|||
/// This typically references a glyph in the 'glyf' or 'CFF' table.
|
|||
/// </summary>
|
|||
public ushort GlyphId { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the color palette index for this layer.
|
|||
/// References a color in the CPAL (Color Palette) table.
|
|||
/// </summary>
|
|||
public ushort PaletteIndex { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Composite modes for blend operations.
|
|||
/// </summary>
|
|||
internal enum CompositeMode |
|||
{ |
|||
Clear = 0, |
|||
Src = 1, |
|||
Dest = 2, |
|||
SrcOver = 3, |
|||
DestOver = 4, |
|||
SrcIn = 5, |
|||
DestIn = 6, |
|||
SrcOut = 7, |
|||
DestOut = 8, |
|||
SrcAtop = 9, |
|||
DestAtop = 10, |
|||
Xor = 11, |
|||
Plus = 12, |
|||
Screen = 13, |
|||
Overlay = 14, |
|||
Darken = 15, |
|||
Lighten = 16, |
|||
ColorDodge = 17, |
|||
ColorBurn = 18, |
|||
HardLight = 19, |
|||
SoftLight = 20, |
|||
Difference = 21, |
|||
Exclusion = 22, |
|||
Multiply = 23, |
|||
HslHue = 24, |
|||
HslSaturation = 25, |
|||
HslColor = 26, |
|||
HslLuminosity = 27 |
|||
} |
|||
} |
|||
@ -0,0 +1,230 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Reader for the 'CPAL' (Color Palette) table. Provides access to color palettes used by COLR glyphs.
|
|||
/// </summary>
|
|||
internal sealed class CpalTable |
|||
{ |
|||
internal const string TableName = "CPAL"; |
|||
|
|||
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); |
|||
|
|||
private readonly ReadOnlyMemory<byte> _cpalData; |
|||
private readonly ushort _version; |
|||
private readonly ushort _numPaletteEntries; |
|||
private readonly ushort _numPalettes; |
|||
private readonly ushort _numColorRecords; |
|||
private readonly uint _colorRecordsArrayOffset; |
|||
|
|||
private CpalTable( |
|||
ReadOnlyMemory<byte> cpalData, |
|||
ushort version, |
|||
ushort numPaletteEntries, |
|||
ushort numPalettes, |
|||
ushort numColorRecords, |
|||
uint colorRecordsArrayOffset) |
|||
{ |
|||
_cpalData = cpalData; |
|||
_version = version; |
|||
_numPaletteEntries = numPaletteEntries; |
|||
_numPalettes = numPalettes; |
|||
_numColorRecords = numColorRecords; |
|||
_colorRecordsArrayOffset = colorRecordsArrayOffset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the version of the CPAL table.
|
|||
/// </summary>
|
|||
public ushort Version => _version; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of palette entries in each palette.
|
|||
/// </summary>
|
|||
public int PaletteEntryCount => _numPaletteEntries; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of palettes.
|
|||
/// </summary>
|
|||
public int PaletteCount => _numPalettes; |
|||
|
|||
/// <summary>
|
|||
/// Attempts to load the CPAL (Color Palette) table from the specified glyph typeface.
|
|||
/// </summary>
|
|||
/// <remarks>This method supports CPAL table versions 0 and 1. If the glyph typeface does not
|
|||
/// contain a valid CPAL table, or if the table version is not supported, the method returns false and sets
|
|||
/// cpalTable to null.</remarks>
|
|||
/// <param name="glyphTypeface">The glyph typeface from which to load the CPAL table. Cannot be null.</param>
|
|||
/// <param name="cpalTable">When this method returns, contains the loaded CPAL table if successful; otherwise, null. This parameter is
|
|||
/// passed uninitialized.</param>
|
|||
/// <returns>true if the CPAL table was successfully loaded; otherwise, false.</returns>
|
|||
public static bool TryLoad(GlyphTypeface glyphTypeface, [NotNullWhen(true)] out CpalTable? cpalTable) |
|||
{ |
|||
cpalTable = null; |
|||
|
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var cpalData)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (cpalData.Length < 12) |
|||
{ |
|||
return false; // Minimum size for CPAL header
|
|||
} |
|||
|
|||
var span = cpalData.Span; |
|||
|
|||
// Parse CPAL table header
|
|||
// uint16 version
|
|||
// uint16 numPaletteEntries
|
|||
// uint16 numPalettes
|
|||
// uint16 numColorRecords
|
|||
// Offset32 colorRecordsArrayOffset
|
|||
|
|||
var version = BinaryPrimitives.ReadUInt16BigEndian(span); |
|||
var numPaletteEntries = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(2)); |
|||
var numPalettes = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(4)); |
|||
var numColorRecords = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(6)); |
|||
var colorRecordsArrayOffset = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(8)); |
|||
|
|||
// Currently support CPAL v0 and v1
|
|||
if (version > 1) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// Validate offset
|
|||
if (colorRecordsArrayOffset >= cpalData.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
cpalTable = new CpalTable( |
|||
cpalData, |
|||
version, |
|||
numPaletteEntries, |
|||
numPalettes, |
|||
numColorRecords, |
|||
colorRecordsArrayOffset); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the offset to the first color record for the specified palette index.
|
|||
/// </summary>
|
|||
private bool TryGetPaletteOffset(int paletteIndex, out int firstColorIndex) |
|||
{ |
|||
firstColorIndex = 0; |
|||
|
|||
if (paletteIndex < 0 || paletteIndex >= _numPalettes) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = _cpalData.Span; |
|||
|
|||
// The colorRecordIndices array starts at offset 12 (after the header)
|
|||
// Each entry is uint16
|
|||
int offsetTableOffset = 12 + (paletteIndex * 2); |
|||
|
|||
if (offsetTableOffset + 2 > span.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
firstColorIndex = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(offsetTableOffset, 2)); |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get the color at the specified palette index and color index.
|
|||
/// </summary>
|
|||
/// <param name="paletteIndex">The palette index (0-based).</param>
|
|||
/// <param name="colorIndex">The color index within the palette (0-based).</param>
|
|||
/// <param name="color">The resulting color.</param>
|
|||
/// <returns>True if the color was successfully retrieved; otherwise, false.</returns>
|
|||
public bool TryGetColor(int paletteIndex, int colorIndex, out Color color) |
|||
{ |
|||
color = default; |
|||
|
|||
if (!TryGetPaletteOffset(paletteIndex, out var firstColorIndex)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (colorIndex < 0 || colorIndex >= _numPaletteEntries) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var actualColorIndex = firstColorIndex + colorIndex; |
|||
|
|||
if (actualColorIndex >= _numColorRecords) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = _cpalData.Span; |
|||
|
|||
// Each color record is 4 bytes: BGRA
|
|||
int offset = (int)_colorRecordsArrayOffset + (actualColorIndex * 4); |
|||
|
|||
if (offset + 4 > span.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var colorSpan = span.Slice(offset, 4); |
|||
|
|||
// Colors are stored as BGRA (little-endian uint32 when viewed as 0xAARRGGBB)
|
|||
var b = colorSpan[0]; |
|||
var g = colorSpan[1]; |
|||
var r = colorSpan[2]; |
|||
var a = colorSpan[3]; |
|||
|
|||
color = Color.FromArgb(a, r, g, b); |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets all colors in the specified palette.
|
|||
/// Returns an empty array if the palette index is invalid.
|
|||
/// </summary>
|
|||
public Color[] GetPalette(int paletteIndex) |
|||
{ |
|||
if (!TryGetPaletteOffset(paletteIndex, out var firstColorIndex)) |
|||
{ |
|||
return Array.Empty<Color>(); |
|||
} |
|||
|
|||
var colors = new Color[_numPaletteEntries]; |
|||
|
|||
for (int i = 0; i < _numPaletteEntries; i++) |
|||
{ |
|||
if (TryGetColor(paletteIndex, i, out var color)) |
|||
{ |
|||
colors[i] = color; |
|||
} |
|||
else |
|||
{ |
|||
colors[i] = Colors.Black; // Fallback
|
|||
} |
|||
} |
|||
|
|||
return colors; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a color from the default palette (palette 0).
|
|||
/// </summary>
|
|||
public bool TryGetColor(int colorIndex, out Color color) |
|||
{ |
|||
return TryGetColor(0, colorIndex, out color); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,166 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Errors that can occur during paint graph traversal with cycle detection.
|
|||
/// </summary>
|
|||
internal enum DecyclerError |
|||
{ |
|||
/// <summary>
|
|||
/// A cycle was detected in the paint graph.
|
|||
/// </summary>
|
|||
CycleDetected, |
|||
|
|||
/// <summary>
|
|||
/// The maximum depth limit was exceeded.
|
|||
/// </summary>
|
|||
DepthLimitExceeded |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Exception thrown when a decycler error occurs.
|
|||
/// </summary>
|
|||
internal class DecyclerException : Exception |
|||
{ |
|||
public DecyclerError Error { get; } |
|||
|
|||
public DecyclerException(DecyclerError error, string message) : base(message) |
|||
{ |
|||
Error = error; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A guard that tracks entry into a paint node and ensures proper cleanup.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of the paint identifier.</typeparam>
|
|||
internal ref struct CycleGuard<T> where T : struct |
|||
{ |
|||
private readonly Decycler<T> _decycler; |
|||
private readonly T _id; |
|||
private bool _exited; |
|||
|
|||
internal CycleGuard(Decycler<T> decycler, T id) |
|||
{ |
|||
_decycler = decycler; |
|||
_id = id; |
|||
_exited = false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Exits the guard, removing the paint ID from the visited set.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
if (!_exited) |
|||
{ |
|||
_decycler.Exit(_id); |
|||
_exited = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tracks visited paint nodes to detect cycles in the paint graph.
|
|||
/// Uses a depth limit to prevent stack overflow even without a HashSet in no_std builds.
|
|||
///
|
|||
/// Usage example:
|
|||
/// <code>
|
|||
/// var decycler = new PaintDecycler();
|
|||
///
|
|||
/// void TraversePaint(Paint paint, ushort glyphId)
|
|||
/// {
|
|||
/// using var guard = decycler.Enter(glyphId);
|
|||
///
|
|||
/// // Process the paint node here
|
|||
/// // The guard will automatically clean up when the using block exits
|
|||
///
|
|||
/// // If this paint references other paints, traverse them recursively:
|
|||
/// if (paint is ColrGlyph colrGlyph)
|
|||
/// {
|
|||
/// var childPaint = GetPaint(colrGlyph.GlyphId);
|
|||
/// TraversePaint(childPaint, (ushort)colrGlyph.GlyphId);
|
|||
/// }
|
|||
/// }
|
|||
/// </code>
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of the paint identifier (typically ushort for GlyphId).</typeparam>
|
|||
internal class Decycler<T> where T : struct |
|||
{ |
|||
private readonly HashSet<T> _visited; |
|||
private readonly int _maxDepth; |
|||
private int _currentDepth; |
|||
|
|||
/// <summary>
|
|||
/// Creates a new Decycler with the specified maximum depth.
|
|||
/// </summary>
|
|||
/// <param name="maxDepth">Maximum traversal depth before returning an error.</param>
|
|||
public Decycler(int maxDepth) |
|||
{ |
|||
_visited = new HashSet<T>(); |
|||
_maxDepth = maxDepth; |
|||
_currentDepth = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to enter a paint node with the given ID.
|
|||
/// Returns a guard that will automatically exit when disposed.
|
|||
/// </summary>
|
|||
/// <param name="id">The paint identifier to enter.</param>
|
|||
/// <returns>A guard that will clean up on disposal.</returns>
|
|||
/// <exception cref="DecyclerException">Thrown if a cycle is detected or depth limit exceeded.</exception>
|
|||
public CycleGuard<T> Enter(T id) |
|||
{ |
|||
if (_currentDepth >= _maxDepth) |
|||
{ |
|||
throw new DecyclerException( |
|||
DecyclerError.DepthLimitExceeded, |
|||
$"Paint graph depth limit of {_maxDepth} exceeded"); |
|||
} |
|||
|
|||
if (_visited.Contains(id)) |
|||
{ |
|||
throw new DecyclerException( |
|||
DecyclerError.CycleDetected, |
|||
"Cycle detected in paint graph"); |
|||
} |
|||
|
|||
_visited.Add(id); |
|||
_currentDepth++; |
|||
|
|||
return new CycleGuard<T>(this, id); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Exits a paint node, removing it from the visited set.
|
|||
/// Called automatically by CycleGuard.Dispose().
|
|||
/// </summary>
|
|||
/// <param name="id">The paint identifier to exit.</param>
|
|||
internal void Exit(T id) |
|||
{ |
|||
_visited.Remove(id); |
|||
_currentDepth--; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the current traversal depth.
|
|||
/// </summary>
|
|||
public int CurrentDepth => _currentDepth; |
|||
|
|||
/// <summary>
|
|||
/// Returns the maximum allowed traversal depth.
|
|||
/// </summary>
|
|||
public int MaxDepth => _maxDepth; |
|||
|
|||
/// <summary>
|
|||
/// Resets the decycler to its initial state, clearing all visited nodes.
|
|||
/// </summary>
|
|||
public void Reset() |
|||
{ |
|||
_visited.Clear(); |
|||
_currentDepth = 0; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,247 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a set of variation deltas from an ItemVariationStore.
|
|||
/// This is a ref struct for allocation-free access to delta data.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// OpenType ItemVariationStore uses a mixed format for deltas:
|
|||
/// - Word deltas (16-bit, int16): More significant variations
|
|||
/// - Byte deltas (8-bit, int8): Smaller variations for space optimization
|
|||
///
|
|||
/// The wordDeltaCount determines how many deltas are 16-bit vs 8-bit.
|
|||
///
|
|||
/// Delta values are stored as integers but must be converted based on their target type:
|
|||
/// - FWORD deltas (coordinates, translations): No conversion needed (design units)
|
|||
/// - F2DOT14 deltas (scales, angles, alpha): Divide by 16384.0
|
|||
/// - Fixed deltas (Affine2x3 components): Divide by 65536.0
|
|||
/// </remarks>
|
|||
public ref struct DeltaSet |
|||
{ |
|||
/// <summary>
|
|||
/// Creates an empty DeltaSet.
|
|||
/// </summary>
|
|||
public static DeltaSet Empty => new DeltaSet(ReadOnlySpan<byte>.Empty, 0, 0); |
|||
|
|||
private readonly ReadOnlySpan<byte> _data; |
|||
private readonly ushort _wordDeltaCount; |
|||
private readonly ushort _totalDeltaCount; |
|||
|
|||
internal DeltaSet(ReadOnlySpan<byte> data, ushort wordDeltaCount, ushort totalDeltaCount) |
|||
{ |
|||
_data = data; |
|||
_wordDeltaCount = wordDeltaCount; |
|||
_totalDeltaCount = totalDeltaCount; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets whether this delta set is empty (no deltas available).
|
|||
/// </summary>
|
|||
public bool IsEmpty => _totalDeltaCount == 0; |
|||
|
|||
/// <summary>
|
|||
/// Gets the total number of deltas in this set (word + byte deltas).
|
|||
/// </summary>
|
|||
public int Count => _totalDeltaCount; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of word deltas (16-bit) in this set.
|
|||
/// </summary>
|
|||
public int WordDeltaCount => _wordDeltaCount; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of byte deltas (8-bit) in this set.
|
|||
/// </summary>
|
|||
public int ByteDeltaCount => _totalDeltaCount - _wordDeltaCount; |
|||
|
|||
/// <summary>
|
|||
/// Gets the word deltas (16-bit signed integers) as a ReadOnlySpan.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<short> WordDeltas |
|||
{ |
|||
get |
|||
{ |
|||
if (_wordDeltaCount == 0) |
|||
{ |
|||
return ReadOnlySpan<short>.Empty; |
|||
} |
|||
|
|||
var wordBytes = _data.Slice(0, _wordDeltaCount * 2); |
|||
return MemoryMarshal.Cast<byte, short>(wordBytes); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the byte deltas (8-bit signed integers) as a ReadOnlySpan.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<sbyte> ByteDeltas |
|||
{ |
|||
get |
|||
{ |
|||
var byteDeltaCount = _totalDeltaCount - _wordDeltaCount; |
|||
if (byteDeltaCount == 0) |
|||
{ |
|||
return ReadOnlySpan<sbyte>.Empty; |
|||
} |
|||
|
|||
var byteOffset = _wordDeltaCount * 2; |
|||
var byteBytes = _data.Slice(byteOffset, byteDeltaCount); |
|||
return MemoryMarshal.Cast<byte, sbyte>(byteBytes); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a delta value at the specified index, converting byte deltas to short for uniform access.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the delta (0 to Count-1).</param>
|
|||
/// <returns>The delta value as a 16-bit signed integer.</returns>
|
|||
/// <exception cref="IndexOutOfRangeException">If index is out of range.</exception>
|
|||
public short this[int index] |
|||
{ |
|||
get |
|||
{ |
|||
if (index < 0 || index >= _totalDeltaCount) |
|||
{ |
|||
throw new IndexOutOfRangeException($"Delta index {index} is out of range [0, {_totalDeltaCount})"); |
|||
} |
|||
|
|||
// Word deltas come first
|
|||
if (index < _wordDeltaCount) |
|||
{ |
|||
var wordBytes = _data.Slice(index * 2, 2); |
|||
return System.Buffers.Binary.BinaryPrimitives.ReadInt16BigEndian(wordBytes); |
|||
} |
|||
|
|||
// Byte deltas come after word deltas
|
|||
var byteIndex = index - _wordDeltaCount; |
|||
var byteOffset = (_wordDeltaCount * 2) + byteIndex; |
|||
return (sbyte)_data[byteOffset]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a delta value at the specified index.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the delta.</param>
|
|||
/// <param name="delta">The delta value if successful.</param>
|
|||
/// <returns>True if the index is valid; otherwise false.</returns>
|
|||
public bool TryGetDelta(int index, out short delta) |
|||
{ |
|||
delta = 0; |
|||
|
|||
if (index < 0 || index >= _totalDeltaCount) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
delta = this[index]; |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a delta as an FWORD value (design units - no conversion needed).
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the delta.</param>
|
|||
/// <returns>The delta value as a double in design units, or 0.0 if index is out of range.</returns>
|
|||
/// <remarks>
|
|||
/// FWORD deltas are used for:
|
|||
/// - Translation offsets (dx, dy)
|
|||
/// - Gradient coordinates (x0, y0, x1, y1, etc.)
|
|||
/// - Center points (centerX, centerY)
|
|||
/// - Radii values (r0, r1)
|
|||
/// </remarks>
|
|||
public double GetFWordDelta(int index) |
|||
{ |
|||
if (index < 0 || index >= _totalDeltaCount) |
|||
{ |
|||
return 0.0; |
|||
} |
|||
|
|||
// FWORD: No conversion needed, deltas are in design units
|
|||
return this[index]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a delta as an F2DOT14 value (fixed-point 2.14 format).
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the delta.</param>
|
|||
/// <returns>The delta value as a double after F2DOT14 conversion, or 0.0 if index is out of range.</returns>
|
|||
/// <remarks>
|
|||
/// F2DOT14 deltas are used for:
|
|||
/// - Scale values (scaleX, scaleY, scale)
|
|||
/// - Rotation angles (angle)
|
|||
/// - Skew angles (xAngle, yAngle)
|
|||
/// - Alpha values (alpha)
|
|||
/// - Gradient stop offsets
|
|||
/// </remarks>
|
|||
public double GetF2Dot14Delta(int index) |
|||
{ |
|||
if (index < 0 || index >= _totalDeltaCount) |
|||
{ |
|||
return 0.0; |
|||
} |
|||
|
|||
// F2DOT14: Signed fixed-point with 2 integer bits and 14 fractional bits
|
|||
// Convert by dividing by 2^14 (16384)
|
|||
return this[index] / 16384.0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a delta as a Fixed value (fixed-point 16.16 format).
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the delta.</param>
|
|||
/// <returns>The delta value as a double after Fixed conversion, or 0.0 if index is out of range.</returns>
|
|||
/// <remarks>
|
|||
/// Fixed deltas are used for:
|
|||
/// - Affine2x3 matrix components (xx, yx, xy, yy, dx, dy)
|
|||
///
|
|||
/// Note: This assumes the delta is stored as 16-bit but represents a 16.16 fixed-point conceptually.
|
|||
/// In practice, Affine2x3 variations may need special handling depending on the font's encoding.
|
|||
/// </remarks>
|
|||
public double GetFixedDelta(int index) |
|||
{ |
|||
if (index < 0 || index >= _totalDeltaCount) |
|||
{ |
|||
return 0.0; |
|||
} |
|||
|
|||
// Fixed: 16.16 format
|
|||
// Convert by dividing by 2^16 (65536)
|
|||
return this[index] / 65536.0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets all deltas as 16-bit values, converting byte deltas to short.
|
|||
/// Note: This method allocates an array.
|
|||
/// </summary>
|
|||
/// <returns>An array containing all deltas as 16-bit signed integers.</returns>
|
|||
public short[] ToArray() |
|||
{ |
|||
if (_totalDeltaCount == 0) |
|||
{ |
|||
return Array.Empty<short>(); |
|||
} |
|||
|
|||
var result = new short[_totalDeltaCount]; |
|||
|
|||
// Copy word deltas
|
|||
var wordDeltas = WordDeltas; |
|||
for (int i = 0; i < wordDeltas.Length; i++) |
|||
{ |
|||
result[i] = wordDeltas[i]; |
|||
} |
|||
|
|||
// Convert and copy byte deltas
|
|||
var byteDeltas = ByteDeltas; |
|||
for (int i = 0; i < byteDeltas.Length; i++) |
|||
{ |
|||
result[_wordDeltaCount + i] = byteDeltas[i]; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,238 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a DeltaSetIndexMap table for COLR v1 font variations.
|
|||
/// Maps glyph/entry indices to variation data indices.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The DeltaSetIndexMap provides mappings from indices (glyph IDs or other) to
|
|||
/// (outer, inner) index pairs for ItemVariationStore lookups. This enables
|
|||
/// efficient access to variation data.
|
|||
///
|
|||
/// See OpenType spec: https://learn.microsoft.com/en-us/typography/opentype/spec/otvarcommonformats#associating-target-items-to-variation-data
|
|||
/// </remarks>
|
|||
internal sealed class DeltaSetIndexMap |
|||
{ |
|||
private readonly ReadOnlyMemory<byte> _data; |
|||
private readonly byte _format; |
|||
private readonly byte _entryFormat; |
|||
private readonly uint _mapCount; |
|||
private readonly uint _mapDataOffset; |
|||
|
|||
private DeltaSetIndexMap( |
|||
ReadOnlyMemory<byte> data, |
|||
byte format, |
|||
byte entryFormat, |
|||
uint mapCount, |
|||
uint mapDataOffset) |
|||
{ |
|||
_data = data; |
|||
_format = format; |
|||
_entryFormat = entryFormat; |
|||
_mapCount = mapCount; |
|||
_mapDataOffset = mapDataOffset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the format of this DeltaSetIndexMap (0 or 1).
|
|||
/// </summary>
|
|||
public byte Format => _format; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of mapping entries.
|
|||
/// </summary>
|
|||
public uint MapCount => _mapCount; |
|||
|
|||
/// <summary>
|
|||
/// Loads a DeltaSetIndexMap from the specified data.
|
|||
/// </summary>
|
|||
/// <param name="data">The raw table data containing the DeltaSetIndexMap.</param>
|
|||
/// <param name="offset">The offset within the data where the DeltaSetIndexMap starts.</param>
|
|||
/// <returns>A DeltaSetIndexMap instance, or null if the data is invalid.</returns>
|
|||
public static DeltaSetIndexMap? Load(ReadOnlyMemory<byte> data, uint offset) |
|||
{ |
|||
if (offset >= data.Length) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var span = data.Span.Slice((int)offset); |
|||
|
|||
// Minimum size check: format (1) + entryFormat (1) + mapCount (varies by format)
|
|||
if (span.Length < 2) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var format = span[0]; |
|||
|
|||
// Only formats 0 and 1 are defined
|
|||
if (format > 1) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var entryFormat = span[1]; |
|||
|
|||
uint mapCount; |
|||
uint mapDataOffset; |
|||
|
|||
if (format == 0) |
|||
{ |
|||
// Format 0:
|
|||
// uint8 format = 0
|
|||
// uint8 entryFormat
|
|||
// uint16 mapCount
|
|||
|
|||
if (span.Length < 4) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
mapCount = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(2)); |
|||
mapDataOffset = 4; // Map data starts immediately after header
|
|||
} |
|||
else // format == 1
|
|||
{ |
|||
// Format 1:
|
|||
// uint8 format = 1
|
|||
// uint8 entryFormat
|
|||
// uint32 mapCount
|
|||
|
|||
if (span.Length < 6) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
mapCount = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(2)); |
|||
mapDataOffset = 6; // Map data starts immediately after header
|
|||
} |
|||
|
|||
return new DeltaSetIndexMap(data.Slice((int)offset), format, entryFormat, mapCount, mapDataOffset); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the (outer, inner) delta set index pair for the specified map index.
|
|||
/// </summary>
|
|||
/// <param name="mapIndex">The index to look up (e.g., glyph ID).</param>
|
|||
/// <param name="outerIndex">The outer index for ItemVariationStore lookup.</param>
|
|||
/// <param name="innerIndex">The inner index for ItemVariationStore lookup.</param>
|
|||
/// <returns>True if the lookup was successful; otherwise false.</returns>
|
|||
public bool TryGetDeltaSetIndex(uint mapIndex, out ushort outerIndex, out ushort innerIndex) |
|||
{ |
|||
outerIndex = 0; |
|||
innerIndex = 0; |
|||
|
|||
// If mapIndex is out of range, return false
|
|||
if (mapIndex >= _mapCount) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = _data.Span; |
|||
var dataOffset = (int)_mapDataOffset; |
|||
|
|||
// entryFormat specifies the size and packing of entries:
|
|||
// Bits 0-3: Size of inner index minus 1 (in bytes)
|
|||
// Bits 4-7: Size of outer index minus 1 (in bytes)
|
|||
//
|
|||
// Common values:
|
|||
// 0x00 = 1-byte inner, 1-byte outer (2 bytes total)
|
|||
// 0x10 = 1-byte inner, 2-byte outer (3 bytes total)
|
|||
// 0x01 = 2-byte inner, 1-byte outer (3 bytes total)
|
|||
// 0x11 = 2-byte inner, 2-byte outer (4 bytes total)
|
|||
|
|||
var innerSizeBytes = (_entryFormat & 0x0F) + 1; |
|||
var outerSizeBytes = ((_entryFormat >> 4) & 0x0F) + 1; |
|||
var entrySize = innerSizeBytes + outerSizeBytes; |
|||
|
|||
var entryOffset = dataOffset + (int)(mapIndex * entrySize); |
|||
|
|||
// Ensure we have enough data for this entry
|
|||
if (entryOffset + entrySize > span.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var entrySpan = span.Slice(entryOffset, entrySize); |
|||
|
|||
// Read outer index (comes first)
|
|||
switch (outerSizeBytes) |
|||
{ |
|||
case 1: |
|||
outerIndex = entrySpan[0]; |
|||
break; |
|||
case 2: |
|||
outerIndex = BinaryPrimitives.ReadUInt16BigEndian(entrySpan); |
|||
break; |
|||
case 3: |
|||
outerIndex = (ushort)((entrySpan[0] << 16) | (entrySpan[1] << 8) | entrySpan[2]); |
|||
break; |
|||
case 4: |
|||
outerIndex = (ushort)BinaryPrimitives.ReadUInt32BigEndian(entrySpan); |
|||
break; |
|||
default: |
|||
return false; |
|||
} |
|||
|
|||
// Read inner index (comes after outer)
|
|||
var innerSpan = entrySpan.Slice(outerSizeBytes); |
|||
switch (innerSizeBytes) |
|||
{ |
|||
case 1: |
|||
innerIndex = innerSpan[0]; |
|||
break; |
|||
case 2: |
|||
innerIndex = BinaryPrimitives.ReadUInt16BigEndian(innerSpan); |
|||
break; |
|||
case 3: |
|||
innerIndex = (ushort)((innerSpan[0] << 16) | (innerSpan[1] << 8) | innerSpan[2]); |
|||
break; |
|||
case 4: |
|||
innerIndex = (ushort)BinaryPrimitives.ReadUInt32BigEndian(innerSpan); |
|||
break; |
|||
default: |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a complete delta set for the specified variation index.
|
|||
/// </summary>
|
|||
/// <param name="itemVariationStore">The ItemVariationStore to retrieve deltas from.</param>
|
|||
/// <param name="variationIndex">The index to look up in the DeltaSetIndexMap.</param>
|
|||
/// <param name="deltaSet">A DeltaSet ref struct providing format-aware access to deltas.</param>
|
|||
/// <returns>True if variation deltas were found; otherwise false.</returns>
|
|||
/// <remarks>
|
|||
/// This method uses the DeltaSetIndexMap to map the variation index to an (outer, inner) index pair,
|
|||
/// then retrieves the corresponding delta set from the ItemVariationStore.
|
|||
/// The DeltaSet ref struct provides allocation-free access to both word and byte deltas.
|
|||
/// </remarks>
|
|||
public bool TryGetVariationDeltaSet( |
|||
ItemVariationStore itemVariationStore, |
|||
uint variationIndex, |
|||
out DeltaSet deltaSet) |
|||
{ |
|||
deltaSet = DeltaSet.Empty; |
|||
|
|||
if (itemVariationStore == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// Map the variation index to (outer, inner) indices
|
|||
if (!TryGetDeltaSetIndex(variationIndex, out var outerIndex, out var innerIndex)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// Delegate to ItemVariationStore
|
|||
return itemVariationStore.TryGetDeltaSet(outerIndex, innerIndex, out deltaSet); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,180 @@ |
|||
using System; |
|||
using Avalonia.Media.Immutable; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Helper class for creating gradient brushes with proper normalization.
|
|||
/// Based on fontations gradient normalization logic from traversal.rs.
|
|||
/// </summary>
|
|||
internal static class GradientBrushHelper |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a linear gradient brush with normalization.
|
|||
/// Converts from P0, P1, P2 representation to a simple two-point gradient.
|
|||
/// </summary>
|
|||
public static IBrush CreateLinearGradient( |
|||
Point p0, |
|||
Point p1, |
|||
Point p2, |
|||
Immutable.ImmutableGradientStop[] stops, |
|||
GradientSpreadMethod extend) |
|||
{ |
|||
// If no stops or single stop, return solid color
|
|||
if (stops.Length == 0) |
|||
return new ImmutableSolidColorBrush(Colors.Transparent); |
|||
|
|||
if (stops.Length == 1) |
|||
return new ImmutableSolidColorBrush(stops[0].Color); |
|||
|
|||
// If p0p1 or p0p2 are degenerate, use first color
|
|||
var p0ToP1 = p1 - p0; |
|||
var p0ToP2 = p2 - p0; |
|||
|
|||
if (IsDegenerate(p0ToP1) || IsDegenerate(p0ToP2) || |
|||
Math.Abs(CrossProduct(p0ToP1, p0ToP2)) < 1e-6) |
|||
{ |
|||
return new ImmutableSolidColorBrush(stops[0].Color); |
|||
} |
|||
|
|||
// Compute P3 as orthogonal projection of p0->p1 onto perpendicular to p0->p2
|
|||
var perpToP2 = new Vector(p0ToP2.Y, -p0ToP2.X); |
|||
var p3 = p0 + ProjectOnto(p0ToP1, perpToP2); |
|||
|
|||
// Stops are already ImmutableGradientStop[], pass directly (arrays implement IReadOnlyList)
|
|||
return new ImmutableLinearGradientBrush( |
|||
gradientStops: stops, |
|||
startPoint: new RelativePoint(p0, RelativeUnit.Absolute), |
|||
endPoint: new RelativePoint(p3, RelativeUnit.Absolute), |
|||
spreadMethod: extend |
|||
); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a radial gradient brush with normalization.
|
|||
/// </summary>
|
|||
public static IBrush CreateRadialGradient( |
|||
Point c0, |
|||
double r0, |
|||
Point c1, |
|||
double r1, |
|||
Immutable.ImmutableGradientStop[] stops, |
|||
GradientSpreadMethod extend) |
|||
{ |
|||
if (stops.Length == 0) |
|||
return new ImmutableSolidColorBrush(Colors.Transparent); |
|||
|
|||
if (stops.Length == 1) |
|||
return new ImmutableSolidColorBrush(stops[0].Color); |
|||
|
|||
// Note: Negative radii can occur after normalization
|
|||
// The client should handle truncation at the 0 position
|
|||
|
|||
// Stops are already ImmutableGradientStop[], pass directly (arrays implement IReadOnlyList)
|
|||
return new ImmutableRadialGradientBrush( |
|||
stops, |
|||
center: new RelativePoint(c0, RelativeUnit.Absolute), |
|||
gradientOrigin: new RelativePoint(c1, RelativeUnit.Absolute), |
|||
radiusX: new RelativeScalar(r0, RelativeUnit.Absolute), |
|||
radiusY: new RelativeScalar(r1, RelativeUnit.Absolute), |
|||
spreadMethod: extend |
|||
); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a conic (sweep) gradient brush with angle normalization.
|
|||
/// Angles are converted from counter-clockwise to clockwise for the shader.
|
|||
/// </summary>
|
|||
public static IBrush CreateConicGradient( |
|||
Point center, |
|||
double startAngle, |
|||
double endAngle, |
|||
Immutable.ImmutableGradientStop[] stops, |
|||
GradientSpreadMethod extend) |
|||
{ |
|||
if (stops.Length == 0) |
|||
return new ImmutableSolidColorBrush(Colors.Transparent); |
|||
|
|||
if (stops.Length == 1) |
|||
return new ImmutableSolidColorBrush(stops[0].Color); |
|||
|
|||
// OpenType 1.9.1 adds a shift to ease 0-360 degree specification
|
|||
var startAngleDeg = startAngle * 180.0 + 180.0; |
|||
var endAngleDeg = endAngle * 180.0 + 180.0; |
|||
|
|||
// Convert from counter-clockwise to clockwise
|
|||
startAngleDeg = 360.0 - startAngleDeg; |
|||
endAngleDeg = 360.0 - endAngleDeg; |
|||
|
|||
var finalStops = stops; |
|||
|
|||
// Swap if needed to ensure start < end
|
|||
if (startAngleDeg > endAngleDeg) |
|||
{ |
|||
(startAngleDeg, endAngleDeg) = (endAngleDeg, startAngleDeg); |
|||
|
|||
// Reverse stops - only allocate if we need to reverse
|
|||
finalStops = ReverseStops(stops); |
|||
} |
|||
|
|||
// If start == end and not Pad mode, nothing should be drawn
|
|||
if (Math.Abs(startAngleDeg - endAngleDeg) < 1e-6 && extend != GradientSpreadMethod.Pad) |
|||
{ |
|||
return new ImmutableSolidColorBrush(Colors.Transparent); |
|||
} |
|||
|
|||
return new ImmutableConicGradientBrush( |
|||
finalStops, |
|||
center: new RelativePoint(center, RelativeUnit.Absolute), |
|||
angle: startAngleDeg, |
|||
spreadMethod: extend |
|||
); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reverses gradient stops without LINQ to minimize allocations.
|
|||
/// ImmutableGradientStop constructor is (Color, double offset).
|
|||
/// </summary>
|
|||
private static Immutable.ImmutableGradientStop[] ReverseStops(Immutable.ImmutableGradientStop[] stops) |
|||
{ |
|||
var length = stops.Length; |
|||
var reversed = new Immutable.ImmutableGradientStop[length]; |
|||
|
|||
// Reverse in-place without LINQ
|
|||
for (int i = 0; i < length; i++) |
|||
{ |
|||
var originalStop = stops[length - 1 - i]; |
|||
// ImmutableGradientStop constructor: (Color color, double offset)
|
|||
reversed[i] = new Immutable.ImmutableGradientStop(1.0 - originalStop.Offset, originalStop.Color); |
|||
} |
|||
|
|||
return reversed; |
|||
} |
|||
|
|||
private static bool IsDegenerate(Vector v) |
|||
{ |
|||
return Math.Abs(v.X) < 1e-6 && Math.Abs(v.Y) < 1e-6; |
|||
} |
|||
|
|||
private static double CrossProduct(Vector a, Vector b) |
|||
{ |
|||
return a.X * b.Y - a.Y * b.X; |
|||
} |
|||
|
|||
private static double DotProduct(Vector a, Vector b) |
|||
{ |
|||
return a.X * b.X + a.Y * b.Y; |
|||
} |
|||
|
|||
private static Vector ProjectOnto(Vector vector, Vector onto) |
|||
{ |
|||
var length = Math.Sqrt(onto.X * onto.X + onto.Y * onto.Y); |
|||
if (length < 1e-6) |
|||
return new Vector(0, 0); |
|||
|
|||
var normalized = onto / length; |
|||
var scale = DotProduct(vector, onto) / length; |
|||
return normalized * scale; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
using Avalonia.Media.Immutable; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
internal interface IColorPainter |
|||
{ |
|||
/// <summary>
|
|||
/// Pushes the specified transformation matrix onto the current transformation stack.
|
|||
/// </summary>
|
|||
/// <remarks>Use this method to temporarily modify the coordinate system for drawing operations.
|
|||
/// To restore the previous transformation, call the corresponding pop method if available. Transformations are
|
|||
/// applied in a last-in, first-out manner.</remarks>
|
|||
/// <param name="transform">The transformation matrix to apply to subsequent drawing operations. The matrix defines how coordinates are
|
|||
/// transformed, such as by translation, rotation, or scaling.</param>
|
|||
void PushTransform(Matrix transform); |
|||
|
|||
/// <summary>
|
|||
/// Removes the most recently applied transformation from the transformation stack.
|
|||
/// </summary>
|
|||
/// <remarks>Call this method to revert to the previous transformation state. Typically used in
|
|||
/// graphics or rendering contexts to restore the coordinate system after applying temporary transformations.
|
|||
/// Calling this method when the transformation stack is empty may result in an error or undefined behavior,
|
|||
/// depending on the implementation.</remarks>
|
|||
void PopTransform(); |
|||
|
|||
/// <summary>
|
|||
/// Pushes a new drawing layer onto the stack using the specified composite mode.
|
|||
/// </summary>
|
|||
/// <remarks>Use this method to isolate drawing operations on a separate layer, which can then be
|
|||
/// composited with the underlying content according to the specified mode. Layers should typically be paired
|
|||
/// with a corresponding pop operation to restore the previous drawing state.</remarks>
|
|||
/// <param name="mode">The blending mode to use when compositing the new layer with existing content.</param>
|
|||
void PushLayer(CompositeMode mode); |
|||
|
|||
/// <summary>
|
|||
/// Removes the topmost layer from the current layer stack.
|
|||
/// </summary>
|
|||
/// <remarks>Call this method to revert to the previous layer after pushing a new one. If there
|
|||
/// are no layers to remove, the behavior may depend on the implementation; consult the specific documentation
|
|||
/// for details.</remarks>
|
|||
void PopLayer(); |
|||
|
|||
/// <summary>
|
|||
/// Establishes a new clipping region that restricts drawing to the specified rectangle.
|
|||
/// </summary>
|
|||
/// <remarks>Subsequent drawing operations are limited to the area defined by the clipping region
|
|||
/// until the region is removed. Clipping regions can typically be nested; ensure that each call to this method
|
|||
/// is balanced with a corresponding call to remove the clipping region, if required by the
|
|||
/// implementation.</remarks>
|
|||
/// <param name="clipBox">The rectangle that defines the boundaries of the clipping region. Only drawing operations within this area
|
|||
/// will be visible.</param>
|
|||
void PushClip(Rect clipBox); |
|||
|
|||
/// <summary>
|
|||
/// Removes the most recently added clip from the stack.
|
|||
/// </summary>
|
|||
/// <remarks>Call this method to revert the last PushClip operation and restore the previous
|
|||
/// clipping region. If there are no clips on the stack, calling this method has no effect.</remarks>
|
|||
void PopClip(); |
|||
|
|||
/// <summary>
|
|||
/// Fills the current path with a solid color.
|
|||
/// </summary>
|
|||
/// <param name="color"></param>
|
|||
void FillSolid(Color color); |
|||
|
|||
/// <summary>
|
|||
/// Fills the current path with a linear gradient defined by the specified points and gradient stops.
|
|||
/// </summary>
|
|||
/// <remarks>The gradient is interpolated between the specified points using the provided gradient
|
|||
/// stops. The spread method determines how the gradient is rendered outside the range defined by the start and
|
|||
/// end points.</remarks>
|
|||
/// <param name="p0">The starting point of the linear gradient in the coordinate space.</param>
|
|||
/// <param name="p1">The ending point of the linear gradient in the coordinate space.</param>
|
|||
/// <param name="stops">An array of gradient stops that define the colors and their positions along the gradient. Cannot be null or
|
|||
/// empty.</param>
|
|||
/// <param name="extend">Specifies how the gradient is extended beyond the start and end points, using the defined spread method.</param>
|
|||
void FillLinearGradient(Point p0, Point p1, GradientStop[] stops, GradientSpreadMethod extend); |
|||
|
|||
/// <summary>
|
|||
/// Fills the current path with a radial gradient defined by two circles and a set of gradient stops.
|
|||
/// </summary>
|
|||
/// <remarks>The gradient transitions from the color at the starting circle to the color at the
|
|||
/// ending circle, interpolating colors as defined by the gradient stops. The spread method determines how the
|
|||
/// gradient is rendered outside the circles' bounds.</remarks>
|
|||
/// <param name="c0">The center point of the starting circle for the gradient.</param>
|
|||
/// <param name="r0">The radius of the starting circle. Must be non-negative.</param>
|
|||
/// <param name="c1">The center point of the ending circle for the gradient.</param>
|
|||
/// <param name="r1">The radius of the ending circle. Must be non-negative.</param>
|
|||
/// <param name="stops">An array of gradient stops that define the colors and their positions within the gradient. Cannot be null or
|
|||
/// empty.</param>
|
|||
/// <param name="extend">Specifies how the gradient is extended beyond its normal range.</param>
|
|||
void FillRadialGradient(Point c0, double r0, Point c1, double r1, GradientStop[] stops, GradientSpreadMethod extend); |
|||
|
|||
/// <summary>
|
|||
/// Fills the current path with a conic gradient defined by the given center point, angle range, color stops,
|
|||
/// and spread method.
|
|||
/// </summary>
|
|||
/// <remarks>The conic gradient is drawn by interpolating colors between the specified stops along
|
|||
/// the angular range from <paramref name="startAngle"/> to <paramref name="endAngle"/>. The behavior outside
|
|||
/// this range is determined by the <paramref name="extend"/> parameter.</remarks>
|
|||
/// <param name="center">The center point of the conic gradient, specified in the coordinate space of the drawing surface.</param>
|
|||
/// <param name="startAngle">The starting angle, in degrees, at which the gradient begins. Measured clockwise from the positive X-axis.</param>
|
|||
/// <param name="endAngle">The ending angle, in degrees, at which the gradient ends. Measured clockwise from the positive X-axis. Must
|
|||
/// be greater than or equal to <paramref name="startAngle"/>.</param>
|
|||
/// <param name="stops">An array of <see cref="GradientStop"/> objects that define the colors and their positions within the
|
|||
/// gradient. Must contain at least two elements.</param>
|
|||
/// <param name="extend">A value that specifies how the gradient is extended beyond its normal range, as defined by the <see
|
|||
/// cref="GradientSpreadMethod"/> enumeration.</param>
|
|||
void FillConicGradient(Point center, double startAngle, double endAngle, GradientStop[] stops, GradientSpreadMethod extend); |
|||
|
|||
/// <summary>
|
|||
/// Pushes a glyph outline onto the painter's current state as the active path for subsequent fill operations.
|
|||
/// </summary>
|
|||
/// <param name="glyphId"></param>
|
|||
void Glyph(ushort glyphId); |
|||
} |
|||
} |
|||
@ -0,0 +1,170 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an ItemVariationStore for OpenType font variations.
|
|||
/// Stores delta values that can be applied to font data based on variation axis coordinates.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The ItemVariationStore is used in multiple OpenType tables (COLR, GDEF, GPOS, etc.)
|
|||
/// to provide variation data for variable fonts. It organizes deltas into a two-level
|
|||
/// hierarchy: ItemVariationData arrays (outer level) containing DeltaSets (inner level).
|
|||
///
|
|||
/// See OpenType spec: https://learn.microsoft.com/en-us/typography/opentype/spec/otvarcommonformats#item-variation-store
|
|||
/// </remarks>
|
|||
internal sealed class ItemVariationStore |
|||
{ |
|||
private readonly ReadOnlyMemory<byte> _data; |
|||
private readonly uint _baseOffset; |
|||
private readonly ushort _format; |
|||
private readonly uint _variationRegionListOffset; |
|||
private readonly ushort _itemVariationDataCount; |
|||
|
|||
private ItemVariationStore( |
|||
ReadOnlyMemory<byte> data, |
|||
uint baseOffset, |
|||
ushort format, |
|||
uint variationRegionListOffset, |
|||
ushort itemVariationDataCount) |
|||
{ |
|||
_data = data; |
|||
_baseOffset = baseOffset; |
|||
_format = format; |
|||
_variationRegionListOffset = variationRegionListOffset; |
|||
_itemVariationDataCount = itemVariationDataCount; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the format of this ItemVariationStore (currently only 1 is defined).
|
|||
/// </summary>
|
|||
public ushort Format => _format; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of ItemVariationData arrays in this store.
|
|||
/// </summary>
|
|||
public ushort ItemVariationDataCount => _itemVariationDataCount; |
|||
|
|||
/// <summary>
|
|||
/// Loads an ItemVariationStore from the specified data.
|
|||
/// </summary>
|
|||
/// <param name="data">The complete table data (e.g., COLR table data).</param>
|
|||
/// <param name="offset">The offset within the data where the ItemVariationStore starts.</param>
|
|||
/// <returns>An ItemVariationStore instance, or null if the data is invalid.</returns>
|
|||
public static ItemVariationStore? Load(ReadOnlyMemory<byte> data, uint offset) |
|||
{ |
|||
if (offset == 0 || offset >= data.Length) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var span = data.Span.Slice((int)offset); |
|||
|
|||
// ItemVariationStore format:
|
|||
// uint16 format (must be 1)
|
|||
// Offset32 variationRegionListOffset
|
|||
// uint16 itemVariationDataCount
|
|||
// Offset32 itemVariationDataOffsets[itemVariationDataCount]
|
|||
|
|||
if (span.Length < 8) // format (2) + variationRegionListOffset (4) + itemVariationDataCount (2)
|
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var format = BinaryPrimitives.ReadUInt16BigEndian(span); |
|||
if (format != 1) |
|||
{ |
|||
return null; // Only format 1 is defined
|
|||
} |
|||
|
|||
var variationRegionListOffset = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(2)); |
|||
var itemVariationDataCount = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(6)); |
|||
|
|||
return new ItemVariationStore( |
|||
data, |
|||
offset, |
|||
format, |
|||
variationRegionListOffset, |
|||
itemVariationDataCount); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a complete delta set for the specified (outer, inner) index pair.
|
|||
/// </summary>
|
|||
/// <param name="outerIndex">The outer index (ItemVariationData index).</param>
|
|||
/// <param name="innerIndex">The inner index (DeltaSet index within ItemVariationData).</param>
|
|||
/// <param name="deltaSet">A DeltaSet ref struct providing format-aware access to deltas.</param>
|
|||
/// <returns>True if the delta set was found; otherwise false.</returns>
|
|||
/// <remarks>
|
|||
/// This method returns a DeltaSet ref struct that provides allocation-free access to both
|
|||
/// word deltas (16-bit) and byte deltas (8-bit), along with methods for uniform access.
|
|||
/// </remarks>
|
|||
public bool TryGetDeltaSet(ushort outerIndex, ushort innerIndex, out DeltaSet deltaSet) |
|||
{ |
|||
deltaSet = DeltaSet.Empty; |
|||
|
|||
// Validate outer index
|
|||
if (outerIndex >= _itemVariationDataCount) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = _data.Span; |
|||
var storeSpan = span.Slice((int)_baseOffset); |
|||
|
|||
// Read the offset to the ItemVariationData for the outer index
|
|||
var offsetsStart = 8; |
|||
var itemDataOffsetPos = offsetsStart + (outerIndex * 4); |
|||
|
|||
if (itemDataOffsetPos + 4 > storeSpan.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var itemVariationDataOffset = BinaryPrimitives.ReadUInt32BigEndian(storeSpan.Slice(itemDataOffsetPos)); |
|||
var absoluteItemDataOffset = _baseOffset + itemVariationDataOffset; |
|||
|
|||
if (absoluteItemDataOffset >= _data.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var itemDataSpan = span.Slice((int)absoluteItemDataOffset); |
|||
|
|||
if (itemDataSpan.Length < 6) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var itemCount = BinaryPrimitives.ReadUInt16BigEndian(itemDataSpan); |
|||
var wordDeltaCount = BinaryPrimitives.ReadUInt16BigEndian(itemDataSpan.Slice(2)); |
|||
var regionIndexCount = BinaryPrimitives.ReadUInt16BigEndian(itemDataSpan.Slice(4)); |
|||
|
|||
if (innerIndex >= itemCount) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var longWordCount = wordDeltaCount; |
|||
var shortDeltaCount = regionIndexCount - wordDeltaCount; |
|||
var deltaSetSize = (longWordCount * 2) + shortDeltaCount; |
|||
|
|||
var regionIndexesSize = regionIndexCount * 2; |
|||
var deltaSetsStart = 6 + regionIndexesSize; |
|||
var targetDeltaSetOffset = deltaSetsStart + (innerIndex * deltaSetSize); |
|||
|
|||
if (targetDeltaSetOffset + deltaSetSize > itemDataSpan.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var deltaSetSpan = itemDataSpan.Slice(targetDeltaSetOffset, deltaSetSize); |
|||
|
|||
// Create DeltaSet with the raw data
|
|||
deltaSet = new DeltaSet(deltaSetSpan, wordDeltaCount, regionIndexCount); |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,47 @@ |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Type alias for the paint decycler that tracks visited glyphs.
|
|||
/// </summary>
|
|||
internal class PaintDecycler : Decycler<ushort> |
|||
{ |
|||
/// <summary>
|
|||
/// Maximum depth for paint graph traversal.
|
|||
/// This limit matches HB_MAX_NESTING_LEVEL used in HarfBuzz.
|
|||
/// </summary>
|
|||
public const int MaxTraversalDepth = 64; |
|||
|
|||
private static readonly ObjectPool<PaintDecycler> Pool = new ObjectPool<PaintDecycler>( |
|||
factory: () => new PaintDecycler(), |
|||
validator: decycler => |
|||
{ |
|||
decycler.Reset(); |
|||
return true; |
|||
}, |
|||
maxSize: 32); |
|||
|
|||
public PaintDecycler() : base(MaxTraversalDepth) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Rents a PaintDecycler from the pool.
|
|||
/// </summary>
|
|||
/// <returns>A pooled PaintDecycler instance.</returns>
|
|||
public static PaintDecycler Rent() |
|||
{ |
|||
return Pool.Rent(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a PaintDecycler to the pool.
|
|||
/// </summary>
|
|||
/// <param name="decycler">The decycler to return to the pool.</param>
|
|||
public static void Return(PaintDecycler decycler) |
|||
{ |
|||
Pool.Return(decycler); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
internal static class PaintParser |
|||
{ |
|||
/// <summary>
|
|||
/// Tries to parse a Paint from the given data at the specified offset.
|
|||
/// </summary>
|
|||
public static bool TryParse(ReadOnlySpan<byte> data, uint offset, in ColrContext context, in PaintDecycler decycler, [NotNullWhen(true)] out Paint? paint) |
|||
{ |
|||
paint = null; |
|||
|
|||
if (offset >= data.Length || data.Length - offset < 1) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = data.Slice((int)offset); |
|||
|
|||
var format = span[0]; |
|||
|
|||
if (format > 32) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return format switch |
|||
{ |
|||
1 => ColrLayers.TryParse(span, offset, in context, in decycler, out paint), |
|||
2 => Solid.TryParse(span, in context, out paint), |
|||
3 => SolidVar.TryParse(span, in context, out paint), |
|||
4 => LinearGradient.TryParse(span, offset, in context, out paint), |
|||
5 => LinearGradientVar.TryParse(span, offset, in context, out paint), |
|||
6 => RadialGradient.TryParse(span, offset, in context, out paint), |
|||
7 => RadialGradientVar.TryParse(span, offset, in context, out paint), |
|||
8 => SweepGradient.TryParse(span, offset, in context, out paint), |
|||
9 => SweepGradientVar.TryParse(span, offset, in context, out paint), |
|||
10 => Glyph.TryParse(span, offset, in context, in decycler, out paint), |
|||
11 => ColrGlyph.TryParse(span, in context, in decycler, out paint), |
|||
12 or 13 => Transform.TryParse(span, format, offset, in context, in decycler, out paint), |
|||
14 => Translate.TryParse(span, offset, in context, in decycler, out paint), |
|||
15 => TranslateVar.TryParse(span, offset, in context, in decycler, out paint), |
|||
16 => Scale.TryParse(span, offset, in context, in decycler, out paint), |
|||
17 => ScaleVar.TryParse(span, offset, in context, in decycler, out paint), |
|||
18 => ScaleAroundCenter.TryParse(span, offset, in context, in decycler, out paint), |
|||
19 => ScaleAroundCenterVar.TryParse(span, offset, in context, in decycler, out paint), |
|||
20 => ScaleUniform.TryParse(span, offset, in context, in decycler, out paint), |
|||
21 => ScaleUniformVar.TryParse(span, offset, in context, in decycler, out paint), |
|||
22 => ScaleUniformAroundCenter.TryParse(span, offset, in context, in decycler, out paint), |
|||
23 => ScaleUniformAroundCenterVar.TryParse(span, offset, in context, in decycler, out paint), |
|||
24 => Rotate.TryParse(span, offset, in context, in decycler, out paint), |
|||
25 => RotateVar.TryParse(span, offset, in context, in decycler, out paint), |
|||
26 => RotateAroundCenter.TryParse(span, offset, in context, in decycler, out paint), |
|||
27 => RotateAroundCenterVar.TryParse(span, offset, in context, in decycler, out paint), |
|||
28 => Skew.TryParse(span, offset, in context, in decycler, out paint), |
|||
29 => SkewVar.TryParse(span, offset, in context, in decycler, out paint), |
|||
30 => SkewAroundCenter.TryParse(span, offset, in context, in decycler, out paint), |
|||
31 => SkewAroundCenterVar.TryParse(span, offset, in context, in decycler, out paint), |
|||
32 => Composite.TryParse(span, offset, in context, in decycler, out paint), |
|||
_ => false |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,203 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Helper methods for parsing paint data.
|
|||
/// </summary>
|
|||
internal static class PaintParsingHelpers |
|||
{ |
|||
public static uint ReadOffset24(ReadOnlySpan<byte> span) |
|||
{ |
|||
// 24-bit offset (3 bytes, BIG-ENDIAN)
|
|||
return ((uint)span[0] << 16) | ((uint)span[1] << 8) | span[2]; |
|||
} |
|||
|
|||
public static double F2Dot14ToDouble(short value) |
|||
{ |
|||
// F2DOT14: signed fixed-point number with 2 integer bits and 14 fractional bits
|
|||
return value / 16384.0; |
|||
} |
|||
|
|||
public static double FixedToDouble(int value) |
|||
{ |
|||
// Fixed 16.16 format
|
|||
return value / 65536.0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies the numeric format used to represent delta values in font tables.
|
|||
/// </summary>
|
|||
/// <remarks>Use this enumeration to indicate how a delta value should be interpreted or converted
|
|||
/// when processing font data. Each member corresponds to a specific numeric representation commonly found in
|
|||
/// OpenType and TrueType font tables.</remarks>
|
|||
public enum DeltaTargetType |
|||
{ |
|||
/// <summary>FWORD - design units (int16) - no conversion needed</summary>
|
|||
FWORD, |
|||
/// <summary>F2DOT14 - fixed-point with 2.14 format (divide by 16384)</summary>
|
|||
F2Dot14, |
|||
/// <summary>Fixed - fixed-point with 16.16 format (divide by 65536)</summary>
|
|||
Fixed |
|||
} |
|||
|
|||
public static double ConvertDelta(int deltaValue, DeltaTargetType targetType) |
|||
{ |
|||
return targetType switch |
|||
{ |
|||
DeltaTargetType.FWORD => deltaValue, |
|||
DeltaTargetType.F2Dot14 => F2Dot14ToDouble((short)deltaValue), |
|||
DeltaTargetType.Fixed => FixedToDouble(deltaValue), |
|||
_ => deltaValue |
|||
}; |
|||
} |
|||
|
|||
public static Matrix ParseAffine2x3(ReadOnlySpan<byte> span) |
|||
{ |
|||
// Format 12 layout: [format][paintOffset][transformOffset]
|
|||
if (span.Length < 7) |
|||
{ |
|||
return Matrix.Identity; |
|||
} |
|||
|
|||
var transformOffset = PaintParsingHelpers.ReadOffset24(span.Slice(4)); |
|||
|
|||
if (transformOffset > (uint)span.Length || span.Length - (int)transformOffset < 24) |
|||
{ |
|||
return Matrix.Identity; |
|||
} |
|||
|
|||
var transformSpan = span.Slice((int)transformOffset, 24); |
|||
|
|||
var xx = FixedToDouble(BinaryPrimitives.ReadInt32BigEndian(transformSpan)); |
|||
var yx = FixedToDouble(BinaryPrimitives.ReadInt32BigEndian(transformSpan.Slice(4))); |
|||
var xy = FixedToDouble(BinaryPrimitives.ReadInt32BigEndian(transformSpan.Slice(8))); |
|||
var yy = FixedToDouble(BinaryPrimitives.ReadInt32BigEndian(transformSpan.Slice(12))); |
|||
var dx = FixedToDouble(BinaryPrimitives.ReadInt32BigEndian(transformSpan.Slice(16))); |
|||
var dy = FixedToDouble(BinaryPrimitives.ReadInt32BigEndian(transformSpan.Slice(20))); |
|||
|
|||
return new Matrix(xx, yx, xy, yy, dx, dy); |
|||
} |
|||
|
|||
public static bool TryParseColorLine( |
|||
ReadOnlySpan<byte> data, |
|||
uint offset, |
|||
in ColrContext context, |
|||
bool isVarColorLine, |
|||
out Immutable.ImmutableGradientStop[] stops, |
|||
out GradientSpreadMethod extend) |
|||
{ |
|||
stops = Array.Empty<Immutable.ImmutableGradientStop>(); |
|||
extend = GradientSpreadMethod.Pad; |
|||
|
|||
if (offset >= data.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var span = data.Slice((int)offset); |
|||
|
|||
if (span.Length < 3) // extend (1) + numStops (2)
|
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
extend = span[0] switch |
|||
{ |
|||
1 => GradientSpreadMethod.Repeat, |
|||
2 => GradientSpreadMethod.Reflect, |
|||
_ => GradientSpreadMethod.Pad |
|||
}; |
|||
|
|||
var numStops = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(1)); |
|||
|
|||
// Validate numStops is reasonable
|
|||
// Gradients with more than 256 stops are likely corrupt data
|
|||
if (numStops == 0) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// ColorStop is 6 bytes, VarColorStop is 10 bytes (each has varIndexBase)
|
|||
int stopSize = isVarColorLine ? 10 : 6; |
|||
|
|||
// Ensure we have enough data for all stops
|
|||
var requiredLength = 3 + (numStops * stopSize); |
|||
if (span.Length < requiredLength) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var tempStops = new Immutable.ImmutableGradientStop[numStops]; |
|||
|
|||
int stopOffset = 3; |
|||
for (int i = 0; i < numStops; i++) |
|||
{ |
|||
// Both ColorStop and VarColorStop have the same first 6 bytes:
|
|||
// F2DOT14 stopOffset (2), uint16 paletteIndex (2), F2DOT14 alpha (2)
|
|||
// VarColorStop adds: uint32 varIndexBase (4) - which we ignore for now
|
|||
|
|||
var stopPos = F2Dot14ToDouble(BinaryPrimitives.ReadInt16BigEndian(span.Slice(stopOffset))); |
|||
|
|||
// Clamp stopPos to valid [0, 1] range
|
|||
// According to OpenType spec, stops should be in [0,1] but font data may have issues
|
|||
stopPos = Math.Clamp(stopPos, 0.0, 1.0); |
|||
|
|||
var paletteIndex = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(stopOffset + 2)); |
|||
var alpha = F2Dot14ToDouble(BinaryPrimitives.ReadInt16BigEndian(span.Slice(stopOffset + 4))); |
|||
|
|||
// Clamp alpha to valid [0, 1] range
|
|||
alpha = Math.Clamp(alpha, 0.0, 1.0); |
|||
|
|||
if (!context.CpalTable.TryGetColor(context.PaletteIndex, paletteIndex, out var color)) |
|||
{ |
|||
color = Colors.Black; |
|||
} |
|||
|
|||
color = Color.FromArgb((byte)(color.A * alpha), color.R, color.G, color.B); |
|||
tempStops[i] = new Immutable.ImmutableGradientStop(stopPos, color); |
|||
|
|||
stopOffset += stopSize; |
|||
} |
|||
|
|||
// Sort stops by offset (required for proper gradient rendering)
|
|||
Array.Sort(tempStops, (a, b) => a.Offset.CompareTo(b.Offset)); |
|||
|
|||
// Remove consecutive duplicate stops (same offset AND color)
|
|||
// NOTE: We preserve stops with the same offset but different colors (hard color transitions)
|
|||
var deduplicatedList = new List<Immutable.ImmutableGradientStop>(numStops); |
|||
const double epsilon = 1e-6; |
|||
|
|||
for (int i = 0; i < tempStops.Length; i++) |
|||
{ |
|||
var stop = tempStops[i]; |
|||
|
|||
// Always add the first stop
|
|||
if (i == 0) |
|||
{ |
|||
deduplicatedList.Add(stop); |
|||
continue; |
|||
} |
|||
|
|||
var previous = tempStops[i - 1]; |
|||
bool sameOffset = Math.Abs(stop.Offset - previous.Offset) < epsilon; |
|||
bool sameColor = stop.Color.Equals(previous.Color); |
|||
|
|||
// Only skip if BOTH offset and color are the same (true duplicate)
|
|||
// Keep stops with same offset but different color (hard color transition)
|
|||
if (sameOffset && sameColor) |
|||
{ |
|||
// This is a true duplicate - skip it
|
|||
continue; |
|||
} |
|||
|
|||
deduplicatedList.Add(stop); |
|||
} |
|||
|
|||
stops = deduplicatedList.ToArray(); |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,653 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia.Media.Immutable; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Resolves Paint definitions into ResolvedPaint by applying variation deltas and normalization.
|
|||
/// </summary>
|
|||
internal static class PaintResolver |
|||
{ |
|||
/// <summary>
|
|||
/// Resolves a paint graph by evaluating variable and composite paint nodes into their fully realized, static forms
|
|||
/// using the provided color context.
|
|||
/// </summary>
|
|||
/// <remarks>This method recursively traverses the paint graph, resolving all variable, transform, and
|
|||
/// composite nodes into their static equivalents. The returned paint can be used for rendering or further
|
|||
/// processing without requiring additional context or variation data.</remarks>
|
|||
/// <param name="paint">The root paint node to resolve. This may be a variable, composite, or transform paint type.</param>
|
|||
/// <param name="context">The color context used to evaluate variable paint nodes and apply variation deltas.</param>
|
|||
/// <returns>A new paint instance representing the fully resolved, static form of the input paint. The returned paint will
|
|||
/// not contain any variable or unresolved composite nodes.</returns>
|
|||
/// <exception cref="NotSupportedException">Thrown if the type of the provided paint node is not recognized or supported for resolution.</exception>
|
|||
public static Paint ResolvePaint(Paint paint, in ColrContext context) |
|||
{ |
|||
switch (paint) |
|||
{ |
|||
// Format 1: ColrLayers
|
|||
case ColrLayers colrLayers: |
|||
{ |
|||
var resolvedLayers = new Paint[colrLayers.Layers.Count]; |
|||
|
|||
for (int i = 0; i < colrLayers.Layers.Count; i++) |
|||
{ |
|||
resolvedLayers[i] = ResolvePaint(colrLayers.Layers[i], context); |
|||
} |
|||
|
|||
return new ColrLayers(resolvedLayers); |
|||
} |
|||
|
|||
// Format 2: Solid
|
|||
case Solid solid: |
|||
return new ResolvedSolid(solid.Color); |
|||
|
|||
// Format 3: VarSolid
|
|||
case SolidVar varSolid: |
|||
return new ResolvedSolid( |
|||
context.ApplyAlphaDelta(varSolid.Color, varSolid.VarIndexBase, 0) |
|||
); |
|||
|
|||
// Format 4: LinearGradient
|
|||
case LinearGradient linearGrad: |
|||
return ResolveLinearGradient(linearGrad, context); |
|||
|
|||
// Format 5: VarLinearGradient
|
|||
case LinearGradientVar varLinearGrad: |
|||
return ResolveLinearGradient( |
|||
varLinearGrad, |
|||
context, |
|||
varLinearGrad.VarIndexBase |
|||
); |
|||
|
|||
// Format 6: RadialGradient
|
|||
case RadialGradient radialGrad: |
|||
return ResolveRadialGradient(radialGrad, context); |
|||
|
|||
// Format 7: VarRadialGradient
|
|||
case RadialGradientVar varRadialGrad: |
|||
return ResolveRadialGradient( |
|||
varRadialGrad, |
|||
context, |
|||
varRadialGrad.VarIndexBase |
|||
); |
|||
|
|||
// Format 8: SweepGradient
|
|||
case SweepGradient sweepGrad: |
|||
return ResolveSweepGradient(sweepGrad, context); |
|||
|
|||
// Format 9: VarSweepGradient
|
|||
case SweepGradientVar varSweepGrad: |
|||
return ResolveSweepGradient( |
|||
varSweepGrad, |
|||
context, |
|||
varSweepGrad.VarIndexBase |
|||
); |
|||
|
|||
// Format 10: Glyph
|
|||
case Glyph glyph: |
|||
return new Glyph(glyph.GlyphId, ResolvePaint(glyph.Paint, context)); |
|||
|
|||
// Format 11: ColrGlyph
|
|||
case ColrGlyph colrGlyph: |
|||
return ResolvePaintColrGlyph(colrGlyph, context); |
|||
|
|||
// Format 12: Transform
|
|||
case Transform transform: |
|||
return new ResolvedTransform( |
|||
transform.Matrix, |
|||
ResolvePaint(transform.Inner, context) |
|||
); |
|||
|
|||
// Format 13: VarTransform
|
|||
case TransformVar varTransform: |
|||
return new ResolvedTransform( |
|||
context.ApplyAffineDeltas(varTransform.Matrix, varTransform.VarIndexBase), |
|||
ResolvePaint(varTransform.Inner, context) |
|||
); |
|||
|
|||
// Format 14: Translate
|
|||
case Translate translate: |
|||
return new ResolvedTransform( |
|||
Matrix.CreateTranslation(translate.Dx, translate.Dy), |
|||
ResolvePaint(translate.Inner, context) |
|||
); |
|||
|
|||
// Format 15: VarTranslate
|
|||
case TranslateVar varTranslate: |
|||
{ |
|||
var dx = varTranslate.Dx; |
|||
var dy = varTranslate.Dy; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varTranslate.VarIndexBase, out var deltaSet)) |
|||
{ |
|||
// Translate deltas are FWORD (design units)
|
|||
if (deltaSet.Count > 0) |
|||
dx += deltaSet.GetFWordDelta(0); |
|||
if (deltaSet.Count > 1) |
|||
dy += deltaSet.GetFWordDelta(1); |
|||
} |
|||
|
|||
return new ResolvedTransform( |
|||
Matrix.CreateTranslation(dx, dy), |
|||
ResolvePaint(varTranslate.Inner, context) |
|||
); |
|||
} |
|||
|
|||
// Format 16: Scale
|
|||
case Scale scale: |
|||
return new ResolvedTransform( |
|||
Matrix.CreateScale(scale.Sx, scale.Sy), |
|||
ResolvePaint(scale.Inner, context) |
|||
); |
|||
|
|||
// Format 17: VarScale
|
|||
case ScaleVar varScale: |
|||
{ |
|||
var sx = varScale.Sx; |
|||
var sy = varScale.Sy; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varScale.VarIndexBase, out var deltaSet)) |
|||
{ |
|||
// Scale deltas are F2DOT14
|
|||
if (deltaSet.Count > 0) |
|||
sx += deltaSet.GetF2Dot14Delta(0); |
|||
if (deltaSet.Count > 1) |
|||
sy += deltaSet.GetF2Dot14Delta(1); |
|||
} |
|||
|
|||
return new ResolvedTransform( |
|||
Matrix.CreateScale(sx, sy), |
|||
ResolvePaint(varScale.Inner, context) |
|||
); |
|||
} |
|||
|
|||
// Format 18: ScaleAroundCenter
|
|||
case ScaleAroundCenter scaleCenter: |
|||
return new ResolvedTransform( |
|||
CreateScaleAroundCenter(scaleCenter.Sx, scaleCenter.Sy, scaleCenter.Center), |
|||
ResolvePaint(scaleCenter.Inner, context) |
|||
); |
|||
|
|||
// Format 19: VarScaleAroundCenter
|
|||
case ScaleAroundCenterVar varScaleCenter: |
|||
{ |
|||
var sx = varScaleCenter.Sx; |
|||
var sy = varScaleCenter.Sy; |
|||
var centerX = varScaleCenter.Center.X; |
|||
var centerY = varScaleCenter.Center.Y; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varScaleCenter.VarIndexBase, out var deltaSet)) |
|||
{ |
|||
// Scale deltas are F2DOT14
|
|||
if (deltaSet.Count > 0) |
|||
sx += deltaSet.GetF2Dot14Delta(0); |
|||
if (deltaSet.Count > 1) |
|||
sy += deltaSet.GetF2Dot14Delta(1); |
|||
// Center coordinate deltas are FWORD
|
|||
if (deltaSet.Count > 2) |
|||
centerX += deltaSet.GetFWordDelta(2); |
|||
if (deltaSet.Count > 3) |
|||
centerY += deltaSet.GetFWordDelta(3); |
|||
} |
|||
|
|||
return new ResolvedTransform( |
|||
CreateScaleAroundCenter(sx, sy, new Point(centerX, centerY)), |
|||
ResolvePaint(varScaleCenter.Inner, context) |
|||
); |
|||
} |
|||
|
|||
// Format 20: ScaleUniform
|
|||
case ScaleUniform scaleUniform: |
|||
return new ResolvedTransform( |
|||
Matrix.CreateScale(scaleUniform.Scale, scaleUniform.Scale), |
|||
ResolvePaint(scaleUniform.Inner, context) |
|||
); |
|||
|
|||
// Format 21: VarScaleUniform
|
|||
case ScaleUniformVar varScaleUniform: |
|||
{ |
|||
var scale = varScaleUniform.Scale; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varScaleUniform.VarIndexBase, out var deltaSet)) |
|||
{ |
|||
// Scale delta is F2DOT14
|
|||
if (deltaSet.Count > 0) |
|||
scale += deltaSet.GetF2Dot14Delta(0); |
|||
} |
|||
|
|||
return new ResolvedTransform( |
|||
Matrix.CreateScale(scale, scale), |
|||
ResolvePaint(varScaleUniform.Inner, context) |
|||
); |
|||
} |
|||
|
|||
// Format 22: ScaleUniformAroundCenter
|
|||
case ScaleUniformAroundCenter scaleUniformCenter: |
|||
return new ResolvedTransform( |
|||
CreateScaleAroundCenter( |
|||
scaleUniformCenter.Scale, |
|||
scaleUniformCenter.Scale, |
|||
scaleUniformCenter.Center |
|||
), |
|||
ResolvePaint(scaleUniformCenter.Inner, context) |
|||
); |
|||
|
|||
// Format 23: VarScaleUniformAroundCenter
|
|||
case ScaleUniformAroundCenterVar varScaleUniformCenter: |
|||
{ |
|||
var scale = varScaleUniformCenter.Scale; |
|||
var centerX = varScaleUniformCenter.Center.X; |
|||
var centerY = varScaleUniformCenter.Center.Y; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varScaleUniformCenter.VarIndexBase, out var deltaSet)) |
|||
{ |
|||
// Scale delta is F2DOT14
|
|||
if (deltaSet.Count > 0) |
|||
scale += deltaSet.GetF2Dot14Delta(0); |
|||
// Center coordinate deltas are FWORD
|
|||
if (deltaSet.Count > 1) |
|||
centerX += deltaSet.GetFWordDelta(1); |
|||
if (deltaSet.Count > 2) |
|||
centerY += deltaSet.GetFWordDelta(2); |
|||
} |
|||
|
|||
return new ResolvedTransform( |
|||
CreateScaleAroundCenter(scale, scale, new Point(centerX, centerY)), |
|||
ResolvePaint(varScaleUniformCenter.Inner, context) |
|||
); |
|||
} |
|||
|
|||
// Format 24: Rotate
|
|||
case Rotate rotate: |
|||
return new ResolvedTransform( |
|||
CreateRotation(rotate.Angle), |
|||
ResolvePaint(rotate.Inner, context) |
|||
); |
|||
|
|||
// Format 25: VarRotate
|
|||
case RotateVar varRotate: |
|||
{ |
|||
var angle = varRotate.Angle; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varRotate.VarIndexBase, out var deltaSet)) |
|||
{ |
|||
// Angle delta is F2DOT14: 180° per 1.0, so multiply by π to convert to radians
|
|||
if (deltaSet.Count > 0) |
|||
angle += deltaSet.GetF2Dot14Delta(0) * Math.PI; |
|||
} |
|||
|
|||
return new ResolvedTransform( |
|||
CreateRotation(angle), |
|||
ResolvePaint(varRotate.Inner, context) |
|||
); |
|||
} |
|||
|
|||
// Format 26: RotateAroundCenter
|
|||
case RotateAroundCenter rotateCenter: |
|||
return new ResolvedTransform( |
|||
CreateRotation(rotateCenter.Angle, rotateCenter.Center), |
|||
ResolvePaint(rotateCenter.Inner, context) |
|||
); |
|||
|
|||
// Format 27: VarRotateAroundCenter
|
|||
case RotateAroundCenterVar varRotateCenter: |
|||
{ |
|||
var angle = varRotateCenter.Angle; |
|||
var centerX = varRotateCenter.Center.X; |
|||
var centerY = varRotateCenter.Center.Y; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varRotateCenter.VarIndexBase, out var deltaSet)) |
|||
{ |
|||
// Angle delta is F2DOT14: 180° per 1.0, so multiply by π to convert to radians
|
|||
if (deltaSet.Count > 0) |
|||
angle += deltaSet.GetF2Dot14Delta(0) * Math.PI; |
|||
// Center coordinate deltas are FWORD
|
|||
if (deltaSet.Count > 1) |
|||
centerX += deltaSet.GetFWordDelta(1); |
|||
if (deltaSet.Count > 2) |
|||
centerY += deltaSet.GetFWordDelta(2); |
|||
} |
|||
|
|||
return new ResolvedTransform( |
|||
CreateRotation(angle, new Point(centerX, centerY)), |
|||
ResolvePaint(varRotateCenter.Inner, context) |
|||
); |
|||
} |
|||
|
|||
// Format 28: Skew
|
|||
case Skew skew: |
|||
return new ResolvedTransform( |
|||
CreateSkew(skew.XAngle, skew.YAngle, new Point()), |
|||
ResolvePaint(skew.Inner, context) |
|||
); |
|||
|
|||
// Format 29: VarSkew
|
|||
case SkewVar varSkew: |
|||
{ |
|||
var xAngle = varSkew.XAngle; |
|||
var yAngle = varSkew.YAngle; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varSkew.VarIndexBase, out var deltaSet)) |
|||
{ |
|||
// Angle deltas are F2DOT14: 180° per 1.0, so multiply by π to convert to radians
|
|||
if (deltaSet.Count > 0) |
|||
xAngle += deltaSet.GetF2Dot14Delta(0) * Math.PI; |
|||
if (deltaSet.Count > 1) |
|||
yAngle += deltaSet.GetF2Dot14Delta(1) * Math.PI; |
|||
} |
|||
|
|||
return new ResolvedTransform( |
|||
CreateSkew(xAngle, yAngle, new Point()), |
|||
ResolvePaint(varSkew.Inner, context) |
|||
); |
|||
} |
|||
|
|||
// Format 30: SkewAroundCenter
|
|||
case SkewAroundCenter skewCenter: |
|||
return new ResolvedTransform( |
|||
CreateSkew(skewCenter.XAngle, skewCenter.YAngle, skewCenter.Center), |
|||
ResolvePaint(skewCenter.Inner, context) |
|||
); |
|||
|
|||
// Format 31: VarSkewAroundCenter
|
|||
case SkewAroundCenterVar varSkewCenter: |
|||
{ |
|||
var xAngle = varSkewCenter.XAngle; |
|||
var yAngle = varSkewCenter.YAngle; |
|||
var centerX = varSkewCenter.Center.X; |
|||
var centerY = varSkewCenter.Center.Y; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varSkewCenter.VarIndexBase, out var deltaSet)) |
|||
{ |
|||
// Angle deltas are F2DOT14: 180° per 1.0, so multiply by π to convert to radians
|
|||
if (deltaSet.Count > 0) |
|||
xAngle += deltaSet.GetF2Dot14Delta(0) * Math.PI; |
|||
if (deltaSet.Count > 1) |
|||
yAngle += deltaSet.GetF2Dot14Delta(1) * Math.PI; |
|||
// Center coordinate deltas are FWORD
|
|||
if (deltaSet.Count > 2) |
|||
centerX += deltaSet.GetFWordDelta(2); |
|||
if (deltaSet.Count > 3) |
|||
centerY += deltaSet.GetFWordDelta(3); |
|||
} |
|||
|
|||
return new ResolvedTransform( |
|||
CreateSkew(xAngle, yAngle, new Point(centerX, centerY)), |
|||
ResolvePaint(varSkewCenter.Inner, context) |
|||
); |
|||
} |
|||
|
|||
// Format 32: Composite
|
|||
case Composite composite: |
|||
return new Composite( |
|||
ResolvePaint(composite.Backdrop, context), |
|||
ResolvePaint(composite.Source, context), |
|||
composite.Mode |
|||
); |
|||
|
|||
default: |
|||
throw new NotSupportedException($"Unknown paint type: {paint.GetType().Name}"); |
|||
} |
|||
} |
|||
|
|||
internal static Paint ResolvePaintColrGlyph(ColrGlyph colrGlyph, ColrContext context) |
|||
{ |
|||
var glyphId = colrGlyph.GlyphId; |
|||
// Resolve inner paint
|
|||
var resolvedInner = ResolvePaint(colrGlyph.Inner, context); |
|||
|
|||
// Wrap in a clip box if present
|
|||
if (context.ColrTable.TryGetClipBox(glyphId, out var clipBox)) |
|||
{ |
|||
return new ResolvedClipBox(clipBox, resolvedInner); |
|||
} |
|||
|
|||
return resolvedInner; |
|||
} |
|||
|
|||
private static ResolvedPaint ResolveLinearGradient(LinearGradient grad, ColrContext context) |
|||
{ |
|||
var stops = context.NormalizeColorStops(grad.Stops); |
|||
|
|||
return NormalizeLinearGradient(grad.P0, grad.P1, grad.P2, stops, grad.Extend); |
|||
} |
|||
|
|||
private static ResolvedPaint ResolveLinearGradient(LinearGradientVar grad, ColrContext context, uint varIndexBase) |
|||
{ |
|||
var p0 = grad.P0; |
|||
var p1 = grad.P1; |
|||
var p2 = grad.P2; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varIndexBase, out var deltaSet)) |
|||
{ |
|||
// Gradient coordinate deltas are FWORD (design units)
|
|||
if (deltaSet.Count > 0) |
|||
p0 = new Point(p0.X + deltaSet.GetFWordDelta(0), p0.Y); |
|||
if (deltaSet.Count > 1) |
|||
p0 = new Point(p0.X, p0.Y + deltaSet.GetFWordDelta(1)); |
|||
if (deltaSet.Count > 2) |
|||
p1 = new Point(p1.X + deltaSet.GetFWordDelta(2), p1.Y); |
|||
if (deltaSet.Count > 3) |
|||
p1 = new Point(p1.X, p1.Y + deltaSet.GetFWordDelta(3)); |
|||
if (deltaSet.Count > 4) |
|||
p2 = new Point(p2.X + deltaSet.GetFWordDelta(4), p2.Y); |
|||
if (deltaSet.Count > 5) |
|||
p2 = new Point(p2.X, p2.Y + deltaSet.GetFWordDelta(5)); |
|||
} |
|||
|
|||
var stops = context.ResolveColorStops(grad.Stops, varIndexBase); |
|||
return NormalizeLinearGradient(p0, p1, p2, stops, grad.Extend); |
|||
} |
|||
|
|||
private static ResolvedPaint ResolveRadialGradient(RadialGradient grad, ColrContext context) |
|||
{ |
|||
var stops = context.NormalizeColorStops(grad.Stops); |
|||
return new ResolvedRadialGradient(grad.C0, grad.R0, grad.C1, grad.R1, stops, grad.Extend); |
|||
} |
|||
|
|||
private static ResolvedPaint ResolveRadialGradient(RadialGradientVar grad, ColrContext context, uint varIndexBase) |
|||
{ |
|||
var c0 = grad.C0; |
|||
var r0 = grad.R0; |
|||
var c1 = grad.C1; |
|||
var r1 = grad.R1; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varIndexBase, out var deltaSet)) |
|||
{ |
|||
// Center coordinate deltas and radii deltas are FWORD (design units)
|
|||
if (deltaSet.Count > 0) |
|||
c0 = new Point(c0.X + deltaSet.GetFWordDelta(0), c0.Y); |
|||
if (deltaSet.Count > 1) |
|||
c0 = new Point(c0.X, c0.Y + deltaSet.GetFWordDelta(1)); |
|||
if (deltaSet.Count > 2) |
|||
r0 += deltaSet.GetFWordDelta(2); |
|||
if (deltaSet.Count > 3) |
|||
c1 = new Point(c1.X + deltaSet.GetFWordDelta(3), c1.Y); |
|||
if (deltaSet.Count > 4) |
|||
c1 = new Point(c1.X, c1.Y + deltaSet.GetFWordDelta(4)); |
|||
if (deltaSet.Count > 5) |
|||
r1 += deltaSet.GetFWordDelta(5); |
|||
} |
|||
|
|||
var stops = context.ResolveColorStops(grad.Stops, varIndexBase); |
|||
return new ResolvedRadialGradient(c0, r0, c1, r1, stops, grad.Extend); |
|||
} |
|||
|
|||
private static ResolvedPaint ResolveSweepGradient(SweepGradient grad, ColrContext context) |
|||
{ |
|||
var stops = context.NormalizeColorStops(grad.Stops); |
|||
return NormalizeConicGradient(grad.Center, grad.StartAngle, grad.EndAngle, stops, grad.Extend); |
|||
} |
|||
|
|||
private static ResolvedPaint ResolveSweepGradient(SweepGradientVar grad, ColrContext context, uint varIndexBase) |
|||
{ |
|||
var center = grad.Center; |
|||
var startAngle = grad.StartAngle; |
|||
var endAngle = grad.EndAngle; |
|||
|
|||
if (context.ColrTable.TryGetVariationDeltaSet(varIndexBase, out var deltaSet)) |
|||
{ |
|||
|
|||
// Center coordinate deltas are FWORD (design units)
|
|||
if (deltaSet.Count > 0) |
|||
center = new Point(center.X + deltaSet.GetFWordDelta(0), center.Y); |
|||
if (deltaSet.Count > 1) |
|||
center = new Point(center.X, center.Y + deltaSet.GetFWordDelta(1)); |
|||
// Angle deltas are F2DOT14: 180° per 1.0, so multiply by π to convert to radians
|
|||
if (deltaSet.Count > 2) |
|||
startAngle += deltaSet.GetF2Dot14Delta(2) * Math.PI; |
|||
if (deltaSet.Count > 3) |
|||
endAngle += deltaSet.GetF2Dot14Delta(3) * Math.PI; |
|||
} |
|||
|
|||
var stops = context.ResolveColorStops(grad.Stops, varIndexBase); |
|||
return NormalizeConicGradient(center, startAngle, endAngle, stops, grad.Extend); |
|||
} |
|||
|
|||
private static ResolvedPaint NormalizeLinearGradient( |
|||
Point p0, Point p1, Point p2, |
|||
GradientStop[] stops, |
|||
GradientSpreadMethod extend) |
|||
{ |
|||
// If no stops or single stop, return solid color
|
|||
if (stops.Length == 0) |
|||
return new ResolvedSolid(Colors.Transparent); |
|||
|
|||
if (stops.Length == 1) |
|||
return new ResolvedSolid(stops[0].Color); |
|||
|
|||
// If p0p1 or p0p2 are degenerate, use first color
|
|||
var p0ToP1 = p1 - p0; |
|||
var p0ToP2 = p2 - p0; |
|||
|
|||
if (IsDegenerate(p0ToP1) || IsDegenerate(p0ToP2) || |
|||
Math.Abs(CrossProduct(p0ToP1, p0ToP2)) < 1e-6) |
|||
{ |
|||
return new ResolvedSolid(stops[0].Color); |
|||
} |
|||
|
|||
// Compute P3 as orthogonal projection of p0->p1 onto perpendicular to p0->p2
|
|||
var perpToP2 = new Vector(p0ToP2.Y, -p0ToP2.X); |
|||
var p3 = p0 + ProjectOnto(p0ToP1, perpToP2); |
|||
|
|||
return new ResolvedLinearGradient(p0, p3, stops, extend); |
|||
} |
|||
|
|||
private static ResolvedPaint NormalizeConicGradient( |
|||
Point center, |
|||
double startAngle, |
|||
double endAngle, |
|||
GradientStop[] stops, |
|||
GradientSpreadMethod extend) |
|||
{ |
|||
if (stops.Length == 0) |
|||
return new ResolvedSolid(Colors.Transparent); |
|||
|
|||
if (stops.Length == 1) |
|||
return new ResolvedSolid(stops[0].Color); |
|||
|
|||
// OpenType 1.9.1 adds a shift to ease 0-360 degree specification
|
|||
var startAngleDeg = startAngle * 180.0 + 180.0; |
|||
var endAngleDeg = endAngle * 180.0 + 180.0; |
|||
|
|||
// Convert from counter-clockwise to clockwise
|
|||
startAngleDeg = 360.0 - startAngleDeg; |
|||
endAngleDeg = 360.0 - endAngleDeg; |
|||
|
|||
var finalStops = stops; |
|||
|
|||
// Swap if needed to ensure start < end
|
|||
if (startAngleDeg > endAngleDeg) |
|||
{ |
|||
(startAngleDeg, endAngleDeg) = (endAngleDeg, startAngleDeg); |
|||
|
|||
// Reverse stops - only allocate if we need to reverse
|
|||
finalStops = ReverseStops(stops); |
|||
} |
|||
|
|||
// If start == end and not Pad mode, nothing should be drawn
|
|||
if (Math.Abs(startAngleDeg - endAngleDeg) < 1e-6 && extend != GradientSpreadMethod.Pad) |
|||
{ |
|||
return new ResolvedSolid(Colors.Transparent); |
|||
} |
|||
|
|||
return new ResolvedConicGradient(center, startAngleDeg, endAngleDeg, finalStops, extend); |
|||
} |
|||
|
|||
private static GradientStop[] ReverseStops(GradientStop[] stops) |
|||
{ |
|||
var length = stops.Length; |
|||
var reversed = new GradientStop[length]; |
|||
|
|||
for (int i = 0; i < length; i++) |
|||
{ |
|||
var originalStop = stops[length - 1 - i]; |
|||
reversed[i] = new GradientStop(1.0 - originalStop.Offset, originalStop.Color); |
|||
} |
|||
|
|||
return reversed; |
|||
} |
|||
|
|||
private static Matrix CreateScaleAroundCenter(double sx, double sy, Point center) |
|||
{ |
|||
return Matrix.CreateTranslation(-center.X, -center.Y) * |
|||
Matrix.CreateScale(sx, sy) * |
|||
Matrix.CreateTranslation(center.X, center.Y); |
|||
} |
|||
|
|||
private static Matrix CreateRotation(double angleRadians) |
|||
{ |
|||
return Matrix.CreateRotation(angleRadians); |
|||
} |
|||
|
|||
private static Matrix CreateRotation(double angleRadians, Point center) |
|||
{ |
|||
return Matrix.CreateTranslation(-center.X, -center.Y) * |
|||
Matrix.CreateRotation(angleRadians) * |
|||
Matrix.CreateTranslation(center.X, center.Y); |
|||
} |
|||
|
|||
private static Matrix CreateSkew(double xAngleRadians, double yAngleRadians, Point center) |
|||
{ |
|||
var skewMatrix = new Matrix( |
|||
1.0, Math.Tan(yAngleRadians), |
|||
Math.Tan(xAngleRadians), 1.0, |
|||
0.0, 0.0 |
|||
); |
|||
|
|||
if (center == default) |
|||
return skewMatrix; |
|||
|
|||
return Matrix.CreateTranslation(-center.X, -center.Y) * |
|||
skewMatrix * |
|||
Matrix.CreateTranslation(center.X, center.Y); |
|||
} |
|||
|
|||
private static bool IsDegenerate(Vector v) |
|||
{ |
|||
return Math.Abs(v.X) < 1e-6 && Math.Abs(v.Y) < 1e-6; |
|||
} |
|||
|
|||
private static double CrossProduct(Vector a, Vector b) |
|||
{ |
|||
return a.X * b.Y - a.Y * b.X; |
|||
} |
|||
|
|||
private static double DotProduct(Vector a, Vector b) |
|||
{ |
|||
return a.X * b.X + a.Y * b.Y; |
|||
} |
|||
|
|||
private static Vector ProjectOnto(Vector vector, Vector onto) |
|||
{ |
|||
var length = Math.Sqrt(onto.X * onto.X + onto.Y * onto.Y); |
|||
if (length < 1e-6) |
|||
return new Vector(0, 0); |
|||
|
|||
var normalized = onto / length; |
|||
var scale = DotProduct(vector, onto) / length; |
|||
return normalized * scale; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
/// <summary>
|
|||
/// Traverses a resolved paint tree and calls the appropriate methods on the painter.
|
|||
/// </summary>
|
|||
internal static class PaintTraverser |
|||
{ |
|||
public static void Traverse(Paint paint, IColorPainter painter, Matrix currentMatrix) |
|||
{ |
|||
switch (paint) |
|||
{ |
|||
case ResolvedSolid fill: |
|||
painter.FillSolid(fill.Color); |
|||
break; |
|||
|
|||
case ResolvedClipBox clip: |
|||
painter.PushClip(clip.Box); |
|||
Traverse(clip.Inner, painter, currentMatrix); |
|||
painter.PopClip(); |
|||
break; |
|||
|
|||
case ResolvedLinearGradient linearGrad: |
|||
painter.FillLinearGradient( |
|||
linearGrad.P0, |
|||
linearGrad.P1, |
|||
linearGrad.Stops, |
|||
linearGrad.Extend); |
|||
break; |
|||
|
|||
case ResolvedRadialGradient radialGrad: |
|||
painter.FillRadialGradient( |
|||
radialGrad.C0, |
|||
radialGrad.R0, |
|||
radialGrad.C1, |
|||
radialGrad.R1, |
|||
radialGrad.Stops, |
|||
radialGrad.Extend); |
|||
break; |
|||
|
|||
case ResolvedConicGradient conicGrad: |
|||
painter.FillConicGradient( |
|||
conicGrad.Center, |
|||
conicGrad.StartAngle, |
|||
conicGrad.EndAngle, |
|||
conicGrad.Stops, |
|||
conicGrad.Extend); |
|||
break; |
|||
|
|||
case ResolvedTransform t: |
|||
painter.PushTransform(t.Matrix); |
|||
Traverse(t.Inner, painter, t.Matrix * currentMatrix); |
|||
painter.PopTransform(); |
|||
break; |
|||
|
|||
case ColrLayers layers: |
|||
foreach (var child in layers.Layers) |
|||
Traverse(child, painter, currentMatrix); |
|||
break; |
|||
|
|||
case Glyph glyph: |
|||
painter.Glyph(glyph.GlyphId); |
|||
Traverse(glyph.Paint, painter, currentMatrix); |
|||
break; |
|||
|
|||
case Composite comp: |
|||
painter.PushLayer(comp.Mode); |
|||
Traverse(comp.Backdrop, painter, currentMatrix); |
|||
Traverse(comp.Source, painter, currentMatrix); |
|||
painter.PopLayer(); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reflection; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Colr |
|||
{ |
|||
internal record class ResolvedPaint : Paint; |
|||
|
|||
internal record class ResolvedSolid(Color Color) : ResolvedPaint; |
|||
|
|||
internal record class ResolvedLinearGradient( |
|||
Point P0, |
|||
Point P1, |
|||
GradientStop[] Stops, |
|||
GradientSpreadMethod Extend) : ResolvedPaint; |
|||
|
|||
internal record class ResolvedRadialGradient( |
|||
Point C0, |
|||
double R0, |
|||
Point C1, |
|||
double R1, |
|||
GradientStop[] Stops, |
|||
GradientSpreadMethod Extend) : ResolvedPaint; |
|||
|
|||
internal record class ResolvedConicGradient( |
|||
Point Center, |
|||
double StartAngle, |
|||
double EndAngle, |
|||
GradientStop[] Stops, |
|||
GradientSpreadMethod Extend) : ResolvedPaint; |
|||
|
|||
internal record class ResolvedTransform(Matrix Matrix, Paint Inner) : ResolvedPaint; |
|||
|
|||
internal record class ResolvedClipBox(Rect Box, Paint Inner) : ResolvedPaint; |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Glyf |
|||
{ |
|||
[Flags] |
|||
internal enum CompositeFlags : ushort |
|||
{ |
|||
ArgsAreWords = 0x0001, |
|||
ArgsAreXYValues = 0x0002, |
|||
RoundXYToGrid = 0x0004, |
|||
WeHaveAScale = 0x0008, |
|||
MoreComponents = 0x0020, |
|||
WeHaveAnXAndYScale = 0x0040, |
|||
WeHaveATwoByTwo = 0x0080, |
|||
WeHaveInstructions = 0x0100, |
|||
UseMyMetrics = 0x0200, |
|||
OverlapCompound = 0x0400, |
|||
Reserved = 0x1000, // must be ignored
|
|||
ScaledComponentOffset = 0x2000, |
|||
UnscaledComponentOffset = 0x4000 |
|||
} |
|||
} |
|||
@ -0,0 +1,190 @@ |
|||
using System; |
|||
using System.Buffers; |
|||
using System.Buffers.Binary; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Glyf |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a composite glyph that references one or more component glyphs.
|
|||
/// </summary>
|
|||
/// <remarks>This struct holds references to rented arrays that must be returned via Dispose.
|
|||
/// The struct should be disposed after consuming the component data.</remarks>
|
|||
internal readonly ref struct CompositeGlyph |
|||
{ |
|||
// Most composite glyphs have fewer than 8 components
|
|||
private const int EstimatedComponentCount = 8; |
|||
|
|||
// Rented buffer for glyph components
|
|||
private readonly GlyphComponent[]? _rentedBuffer; |
|||
|
|||
/// <summary>
|
|||
/// Gets the span of glyph components that make up this composite glyph.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<GlyphComponent> Components { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the instruction data (currently unused).
|
|||
/// </summary>
|
|||
public ReadOnlySpan<byte> Instructions { get; } |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the CompositeGlyph class using the specified glyph components and an optional
|
|||
/// rented buffer.
|
|||
/// </summary>
|
|||
/// <remarks>The rented buffer, if supplied, is managed internally and should not be accessed or
|
|||
/// modified by the caller after construction. The CompositeGlyph instance does not take ownership of the
|
|||
/// buffer's lifetime.</remarks>
|
|||
/// <param name="components">A read-only span containing the glyph components that make up the composite glyph. The span must remain
|
|||
/// valid for the lifetime of the CompositeGlyph instance.</param>
|
|||
/// <param name="rentedBuffer">An optional array used as a rented buffer for internal storage. If provided, the buffer may be used to
|
|||
/// optimize memory usage.</param>
|
|||
private CompositeGlyph(ReadOnlySpan<GlyphComponent> components, GlyphComponent[]? rentedBuffer) |
|||
{ |
|||
Components = components; |
|||
Instructions = default; |
|||
_rentedBuffer = rentedBuffer; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a CompositeGlyph from the raw glyph data.
|
|||
/// </summary>
|
|||
/// <param name="data">The raw glyph data from the glyf table.</param>
|
|||
/// <returns>A CompositeGlyph instance with components backed by a rented buffer.</returns>
|
|||
/// <remarks>The caller must call Dispose() to return the rented buffer to the pool.</remarks>
|
|||
public static CompositeGlyph Create(ReadOnlySpan<byte> data) |
|||
{ |
|||
// Rent a buffer for components (most composite glyphs have < 8 components)
|
|||
var componentsBuffer = ArrayPool<GlyphComponent>.Shared.Rent(EstimatedComponentCount); |
|||
int componentCount = 0; |
|||
|
|||
try |
|||
{ |
|||
int offset = 0; |
|||
bool moreComponents; |
|||
|
|||
do |
|||
{ |
|||
// Read flags and glyph index
|
|||
CompositeFlags flags = (CompositeFlags)BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2)); |
|||
offset += 2; |
|||
|
|||
ushort glyphIndex = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2)); |
|||
offset += 2; |
|||
|
|||
short arg1 = 0, arg2 = 0; |
|||
|
|||
// Read arguments
|
|||
if ((flags & CompositeFlags.ArgsAreWords) != 0) |
|||
{ |
|||
arg1 = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)); |
|||
offset += 2; |
|||
arg2 = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)); |
|||
offset += 2; |
|||
} |
|||
else |
|||
{ |
|||
// Arguments are indices
|
|||
arg1 = (sbyte)data[offset++]; |
|||
arg2 = (sbyte)data[offset++]; |
|||
} |
|||
|
|||
// Optional transformation
|
|||
float scale = 1.0f; |
|||
float scaleX = 1.0f, scaleY = 1.0f; |
|||
float scale01 = 0.0f, scale10 = 0.0f; |
|||
|
|||
// Uniform scale
|
|||
if ((flags & CompositeFlags.WeHaveAScale) != 0) |
|||
{ |
|||
scale = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)) / 16384f; |
|||
offset += 2; |
|||
} |
|||
// Separate x and y scales
|
|||
else if ((flags & CompositeFlags.WeHaveAnXAndYScale) != 0) |
|||
{ |
|||
scaleX = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)) / 16384f; |
|||
offset += 2; |
|||
scaleY = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)) / 16384f; |
|||
offset += 2; |
|||
} |
|||
// Two by two transformation matrix
|
|||
else if ((flags & CompositeFlags.WeHaveATwoByTwo) != 0) |
|||
{ |
|||
scaleX = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)) / 16384f; |
|||
offset += 2; |
|||
scale01 = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)) / 16384f; |
|||
offset += 2; |
|||
scale10 = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)) / 16384f; |
|||
offset += 2; |
|||
scaleY = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)) / 16384f; |
|||
offset += 2; |
|||
} |
|||
|
|||
// Grow buffer if needed
|
|||
if (componentCount >= componentsBuffer.Length) |
|||
{ |
|||
var oldBuffer = componentsBuffer; |
|||
var newSize = componentsBuffer.Length * 2; |
|||
|
|||
componentsBuffer = ArrayPool<GlyphComponent>.Shared.Rent(newSize); |
|||
|
|||
oldBuffer.AsSpan(0, componentCount).CopyTo(componentsBuffer); |
|||
|
|||
ArrayPool<GlyphComponent>.Shared.Return(oldBuffer); |
|||
} |
|||
|
|||
componentsBuffer[componentCount++] = new GlyphComponent |
|||
{ |
|||
Flags = flags, |
|||
GlyphIndex = glyphIndex, |
|||
Arg1 = arg1, |
|||
Arg2 = arg2, |
|||
Scale = scale, |
|||
ScaleX = scaleX, |
|||
ScaleY = scaleY, |
|||
Scale01 = scale01, |
|||
Scale10 = scale10 |
|||
}; |
|||
|
|||
moreComponents = (flags & CompositeFlags.MoreComponents) != 0; |
|||
} while (moreComponents); |
|||
|
|||
// Instructions if present (currently unused)
|
|||
//ReadOnlySpan<byte> instructions = ReadOnlySpan<byte>.Empty;
|
|||
//if (componentCount > 0 && (componentsBuffer[componentCount - 1].Flags & CompositeFlags.WeHaveInstructions) != 0)
|
|||
//{
|
|||
// ushort instrLen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2));
|
|||
// offset += 2;
|
|||
// instructions = data.Slice(offset, instrLen);
|
|||
//}
|
|||
|
|||
// Return a CompositeGlyph with the rented buffer
|
|||
// The caller is responsible for calling Dispose() to return the buffer
|
|||
return new CompositeGlyph( |
|||
componentsBuffer.AsSpan(0, componentCount), |
|||
componentsBuffer |
|||
); |
|||
} |
|||
catch |
|||
{ |
|||
// On exception, return the buffer immediately
|
|||
ArrayPool<GlyphComponent>.Shared.Return(componentsBuffer); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the rented buffer to the ArrayPool.
|
|||
/// </summary>
|
|||
/// <remarks>This method should be called when the CompositeGlyph is no longer needed
|
|||
/// to ensure the rented buffer is returned to the pool.</remarks>
|
|||
public void Dispose() |
|||
{ |
|||
if (_rentedBuffer != null) |
|||
{ |
|||
ArrayPool<GlyphComponent>.Shared.Return(_rentedBuffer); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,556 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.InteropServices; |
|||
using System.Diagnostics; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Utilities; |
|||
|
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts; |
|||
using Avalonia.Media.Fonts.Tables; |
|||
using Avalonia.Media.Fonts.Tables.Colr; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Glyf |
|||
{ |
|||
/// <summary>
|
|||
/// Reader for the 'glyf' table. Provides on-demand access to individual glyph data using the 'loca' index.
|
|||
/// Designed for high-performance lookups on the hot path.
|
|||
/// </summary>
|
|||
internal sealed class GlyfTable |
|||
{ |
|||
internal const string TableName = "glyf"; |
|||
|
|||
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); |
|||
|
|||
private readonly ReadOnlyMemory<byte> _glyfData; |
|||
private readonly LocaTable _locaTable; |
|||
|
|||
private GlyfTable(ReadOnlyMemory<byte> glyfData, LocaTable locaTable) |
|||
{ |
|||
_glyfData = glyfData; |
|||
_locaTable = locaTable; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the total number of glyphs defined in the font.
|
|||
/// </summary>
|
|||
public int GlyphCount => _locaTable.GlyphCount; |
|||
|
|||
/// <summary>
|
|||
/// Attempts to load the 'glyf' table from the specified font data.
|
|||
/// </summary>
|
|||
/// <remarks>This method does not throw an exception if the 'glyf' table cannot be loaded.
|
|||
/// Instead, it returns <see langword="false"/> and sets <paramref name="glyfTable"/> to <see
|
|||
/// langword="null"/>.</remarks>
|
|||
/// <param name="glyphTypeface">The glyph typeface from which to retrieve the 'glyf' table.</param>
|
|||
/// <param name="head">The 'head' table containing font header information required for loading the 'glyf' table.</param>
|
|||
/// <param name="maxp">The 'maxp' table providing maximum profile information needed to interpret the 'glyf' table.</param>
|
|||
/// <param name="glyfTable">When this method returns, contains the loaded 'glyf' table if successful; otherwise, <see langword="null"/>.
|
|||
/// This parameter is passed uninitialized.</param>
|
|||
/// <returns><see langword="true"/> if the 'glyf' table was successfully loaded; otherwise, <see langword="false"/>.</returns>
|
|||
public static bool TryLoad(GlyphTypeface glyphTypeface, HeadTable head, MaxpTable maxp, out GlyfTable? glyfTable) |
|||
{ |
|||
glyfTable = null; |
|||
|
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var glyfTableData)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var locaTable = LocaTable.Load(glyphTypeface, head, maxp); |
|||
|
|||
if (locaTable == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
glyfTable = new GlyfTable(glyfTableData, locaTable); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to retrieve the raw glyph data for the specified glyph index.
|
|||
/// </summary>
|
|||
/// <remarks>If the glyph exists but has no data (for example, a missing or empty glyph), the
|
|||
/// method returns true and sets the out parameter to an empty memory region. If the glyph index is invalid or
|
|||
/// out of range, the method returns false and the out parameter is set to an empty memory region.</remarks>
|
|||
/// <param name="glyphIndex">The zero-based index of the glyph to retrieve data for.</param>
|
|||
/// <param name="data">When this method returns, contains the glyph data as a read-only memory region if the glyph exists;
|
|||
/// otherwise, contains an empty memory region.</param>
|
|||
/// <returns>true if the glyph data was found and assigned to the out parameter; otherwise, false.</returns>
|
|||
public bool TryGetGlyphData(int glyphIndex, out ReadOnlyMemory<byte> data) |
|||
{ |
|||
if (!_locaTable.TryGetOffsets(glyphIndex, out var start, out var end)) |
|||
{ |
|||
data = ReadOnlyMemory<byte>.Empty; |
|||
return false; |
|||
} |
|||
|
|||
if (start == end) |
|||
{ |
|||
data = ReadOnlyMemory<byte>.Empty; |
|||
return true; |
|||
} |
|||
|
|||
// Additional safety check for glyf table bounds
|
|||
if (start < 0 || end > _glyfData.Length || start > end) |
|||
{ |
|||
data = ReadOnlyMemory<byte>.Empty; |
|||
|
|||
return false; |
|||
} |
|||
|
|||
data = _glyfData.Slice(start, end - start); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <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.
|
|||
/// </summary>
|
|||
public bool TryBuildGlyphGeometry(int glyphIndex, Matrix transform, IGeometryContext context) |
|||
{ |
|||
var decycler = GlyphDecycler.Rent(); |
|||
|
|||
try |
|||
{ |
|||
return TryBuildGlyphGeometryInternal(glyphIndex, context, transform, decycler); |
|||
} |
|||
catch (DecyclerException ex) |
|||
{ |
|||
if (Logger.TryGet(LogEventLevel.Warning, LogArea.Visual, out var log)) |
|||
{ |
|||
log.Log(this, "Glyph {0} processing failed: {1}", glyphIndex, ex.Message); |
|||
} |
|||
return false; |
|||
} |
|||
catch |
|||
{ |
|||
return false; |
|||
} |
|||
finally |
|||
{ |
|||
GlyphDecycler.Return(decycler); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Builds the geometry for a simple glyph by processing its contours and converting them into geometry commands.
|
|||
/// </summary>
|
|||
/// <param name="simpleGlyph">The simple glyph containing contour data, flags, and coordinates.</param>
|
|||
/// <param name="context">The geometry context that receives the constructed glyph geometry.</param>
|
|||
/// <param name="transform">The transformation matrix to apply to all coordinates.</param>
|
|||
/// <returns>true if the glyph geometry was successfully built; otherwise, false.</returns>
|
|||
private static bool BuildSimpleGlyphGeometry(SimpleGlyph simpleGlyph, IGeometryContext context, Matrix transform) |
|||
{ |
|||
try |
|||
{ |
|||
var endPtsOfContours = simpleGlyph.EndPtsOfContours; |
|||
|
|||
if (endPtsOfContours.Length == 0) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var flags = simpleGlyph.Flags; |
|||
var xCoords = simpleGlyph.XCoordinates; |
|||
var yCoords = simpleGlyph.YCoordinates; |
|||
|
|||
var startPointIndex = 0; |
|||
|
|||
for (var contourIndex = 0; contourIndex < endPtsOfContours.Length; contourIndex++) |
|||
{ |
|||
var endPointIndex = endPtsOfContours[contourIndex]; |
|||
var pointCount = endPointIndex - startPointIndex + 1; |
|||
|
|||
if (pointCount == 0) |
|||
{ |
|||
startPointIndex = endPointIndex + 1; |
|||
continue; |
|||
} |
|||
|
|||
// Check if first point is on-curve
|
|||
var firstFlag = flags[startPointIndex]; |
|||
var firstIsOnCurve = (firstFlag & GlyphFlag.OnCurvePoint) != 0; |
|||
|
|||
if (firstIsOnCurve) |
|||
{ |
|||
// Normal case: start from first on-curve point
|
|||
var firstPoint = new Point(xCoords[startPointIndex], yCoords[startPointIndex]); |
|||
context.BeginFigure(transform.Transform(firstPoint), true); |
|||
|
|||
// Start processing from the next point (or wrap to first if only one point)
|
|||
int i = pointCount == 1 ? startPointIndex : startPointIndex + 1; |
|||
int processingStartIndex = i; |
|||
|
|||
var maxSegments = Math.Max(1, pointCount * 3); |
|||
var segmentsProcessed = 0; |
|||
|
|||
while (segmentsProcessed++ < maxSegments) |
|||
{ |
|||
// Wrap index to contour range
|
|||
int currentIdx = startPointIndex + ((i - startPointIndex) % pointCount); |
|||
|
|||
var curFlag = flags[currentIdx]; |
|||
var curIsOnCurve = (curFlag & GlyphFlag.OnCurvePoint) != 0; |
|||
var curPoint = new Point(xCoords[currentIdx], yCoords[currentIdx]); |
|||
|
|||
if (curIsOnCurve) |
|||
{ |
|||
// Simple line to on-curve point
|
|||
context.LineTo(transform.Transform(curPoint)); |
|||
i++; |
|||
} |
|||
else |
|||
{ |
|||
// Current is off-curve, look ahead
|
|||
int nextIdx = startPointIndex + ((i + 1 - startPointIndex) % pointCount); |
|||
var nextFlag = flags[nextIdx]; |
|||
var nextIsOnCurve = (nextFlag & GlyphFlag.OnCurvePoint) != 0; |
|||
var nextPoint = new Point(xCoords[nextIdx], yCoords[nextIdx]); |
|||
|
|||
if (nextIsOnCurve) |
|||
{ |
|||
// Quadratic curve to next on-curve point
|
|||
context.QuadraticBezierTo( |
|||
transform.Transform(curPoint), |
|||
transform.Transform(nextPoint) |
|||
); |
|||
// Advance past the on-curve point
|
|||
i += 2; |
|||
} |
|||
else |
|||
{ |
|||
// Two consecutive off-curve points -> implied midpoint
|
|||
var impliedX = (curPoint.X + nextPoint.X) / 2.0; |
|||
var impliedY = (curPoint.Y + nextPoint.Y) / 2.0; |
|||
var impliedPoint = new Point(impliedX, impliedY); |
|||
|
|||
context.QuadraticBezierTo( |
|||
transform.Transform(curPoint), |
|||
transform.Transform(impliedPoint) |
|||
); |
|||
// Advance to the second off-curve point for next iteration
|
|||
i++; |
|||
} |
|||
} |
|||
|
|||
// Check if we've wrapped back to start
|
|||
int checkIdx = startPointIndex + ((i - startPointIndex) % pointCount); |
|||
if (checkIdx == processingStartIndex && segmentsProcessed > 0) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
context.EndFigure(true); |
|||
} |
|||
else |
|||
{ |
|||
// First point is off-curve -> create implied start between last and first
|
|||
var lastIdx = endPointIndex; |
|||
var firstX = xCoords[startPointIndex]; |
|||
var firstY = yCoords[startPointIndex]; |
|||
var lastX = xCoords[lastIdx]; |
|||
var lastY = yCoords[lastIdx]; |
|||
|
|||
var impliedStartX = (lastX + firstX) / 2.0; |
|||
var impliedStartY = (lastY + firstY) / 2.0; |
|||
var impliedStart = new Point(impliedStartX, impliedStartY); |
|||
|
|||
context.BeginFigure(transform.Transform(impliedStart), true); |
|||
|
|||
int idxWalker = 0; // offset from startPointIndex
|
|||
var maxSegments = pointCount * 3; |
|||
var segmentsProcessed = 0; |
|||
|
|||
while (segmentsProcessed++ < maxSegments) |
|||
{ |
|||
int curIdx = startPointIndex + idxWalker; |
|||
int nextIdxOffset = idxWalker == pointCount - 1 ? 0 : idxWalker + 1; |
|||
int nextIdx = startPointIndex + nextIdxOffset; |
|||
|
|||
var curFlag = flags[curIdx]; |
|||
var curIsOnCurve = (curFlag & GlyphFlag.OnCurvePoint) != 0; |
|||
var curPoint = new Point(xCoords[curIdx], yCoords[curIdx]); |
|||
|
|||
if (curIsOnCurve) |
|||
{ |
|||
context.LineTo(transform.Transform(curPoint)); |
|||
idxWalker = nextIdxOffset; |
|||
} |
|||
else |
|||
{ |
|||
var nextFlag = flags[nextIdx]; |
|||
var nextIsOnCurve = (nextFlag & GlyphFlag.OnCurvePoint) != 0; |
|||
var nextPoint = new Point(xCoords[nextIdx], yCoords[nextIdx]); |
|||
|
|||
if (nextIsOnCurve) |
|||
{ |
|||
context.QuadraticBezierTo( |
|||
transform.Transform(curPoint), |
|||
transform.Transform(nextPoint) |
|||
); |
|||
idxWalker = nextIdxOffset == pointCount - 1 ? 0 : nextIdxOffset + 1; |
|||
} |
|||
else |
|||
{ |
|||
// Two consecutive off-curve points -> implied midpoint
|
|||
var impliedX = (curPoint.X + nextPoint.X) / 2.0; |
|||
var impliedY = (curPoint.Y + nextPoint.Y) / 2.0; |
|||
var impliedPoint = new Point(impliedX, impliedY); |
|||
|
|||
context.QuadraticBezierTo( |
|||
transform.Transform(curPoint), |
|||
transform.Transform(impliedPoint) |
|||
); |
|||
idxWalker = nextIdxOffset == pointCount - 1 ? 0 : nextIdxOffset + 1; |
|||
} |
|||
} |
|||
|
|||
// Stop when we've wrapped back to the beginning
|
|||
if (idxWalker == 0 && segmentsProcessed > 1) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
context.EndFigure(true); |
|||
} |
|||
|
|||
startPointIndex = endPointIndex + 1; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
finally |
|||
{ |
|||
// Return rented buffers to pool
|
|||
simpleGlyph.Dispose(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a transformation matrix for a composite glyph component based on its flags and transformation parameters.
|
|||
/// </summary>
|
|||
/// <param name="component">The glyph component containing transformation information.</param>
|
|||
/// <returns>A transformation matrix that should be applied to the component glyph.</returns>
|
|||
private static Matrix CreateComponentTransform(GlyphComponent component) |
|||
{ |
|||
var flags = component.Flags; |
|||
|
|||
double tx = 0, ty = 0; |
|||
|
|||
if ((flags & CompositeFlags.ArgsAreXYValues) != 0) |
|||
{ |
|||
tx = component.Arg1; |
|||
ty = component.Arg2; |
|||
} |
|||
|
|||
double m11, m12, m21, m22; |
|||
|
|||
if ((flags & CompositeFlags.WeHaveAScale) != 0) |
|||
{ |
|||
m11 = m22 = component.Scale; |
|||
m12 = m21 = 0; |
|||
} |
|||
else if ((flags & CompositeFlags.WeHaveAnXAndYScale) != 0) |
|||
{ |
|||
m11 = component.ScaleX; |
|||
m22 = component.ScaleY; |
|||
m12 = m21 = 0; |
|||
} |
|||
else if ((flags & CompositeFlags.WeHaveATwoByTwo) != 0) |
|||
{ |
|||
m11 = component.ScaleX; |
|||
m12 = component.Scale01; |
|||
m21 = component.Scale10; |
|||
m22 = component.ScaleY; |
|||
} |
|||
else |
|||
{ |
|||
m11 = m22 = 1.0; |
|||
m12 = m21 = 0; |
|||
} |
|||
|
|||
return new Matrix(m11, m12, m21, m22, tx, ty); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to build the geometry for the specified glyph and adds it to the provided geometry context.
|
|||
/// </summary>
|
|||
/// <remarks>This method processes both simple and composite glyphs. For composite glyphs,
|
|||
/// recursion is used and the visited set prevents cycles. The method returns false if the glyph is empty,
|
|||
/// invalid, or has already been processed.</remarks>
|
|||
/// <param name="glyphIndex">The index of the glyph to process. Must correspond to a valid glyph in the font.</param>
|
|||
/// <param name="context">The geometry context that receives the constructed glyph geometry.</param>
|
|||
/// <param name="transform">The transformation matrix to apply to the glyph geometry.</param>
|
|||
/// <param name="decycler">A <see cref="GlyphDecycler"/> instance used to prevent infinite recursion when building composite glyphs.</param>
|
|||
/// <returns>true if the glyph geometry was successfully built and added to the context; otherwise, false.</returns>
|
|||
private bool TryBuildGlyphGeometryInternal(int glyphIndex, IGeometryContext context, Matrix transform, GlyphDecycler decycler) |
|||
{ |
|||
using var guard = decycler.Enter(glyphIndex); |
|||
|
|||
if (!TryGetGlyphData(glyphIndex, out var glyphData) || glyphData.IsEmpty) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var descriptor = new GlyphDescriptor(glyphData); |
|||
|
|||
if (descriptor.IsSimpleGlyph) |
|||
{ |
|||
return BuildSimpleGlyphGeometry(descriptor.SimpleGlyph, context, transform); |
|||
} |
|||
else |
|||
{ |
|||
return BuildCompositeGlyphGeometry(descriptor.CompositeGlyph, context, transform, decycler); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Builds the geometry for a composite glyph by recursively processing its components.
|
|||
/// </summary>
|
|||
/// <param name="compositeGlyph">The composite glyph containing component references and transformations.</param>
|
|||
/// <param name="context">The geometry context that receives the constructed glyph geometry.</param>
|
|||
/// <param name="transform">The transformation matrix to apply to all component glyphs.</param>
|
|||
/// <param name="decycler">A <see cref="GlyphDecycler"/> instance used to prevent infinite recursion when building composite glyphs.</param>
|
|||
/// <returns>true if at least one component was successfully processed; otherwise, false.</returns>
|
|||
private bool BuildCompositeGlyphGeometry(CompositeGlyph compositeGlyph, IGeometryContext context, Matrix transform, GlyphDecycler decycler) |
|||
{ |
|||
try |
|||
{ |
|||
var components = compositeGlyph.Components; |
|||
|
|||
if (components.Length == 0) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var hasGeometry = false; |
|||
|
|||
foreach (var component in components) |
|||
{ |
|||
var componentTransform = CreateComponentTransform(component); |
|||
var combinedTransform = componentTransform * transform; |
|||
|
|||
var wrappedContext = new TransformingGeometryContext(context, combinedTransform); |
|||
|
|||
if (TryBuildGlyphGeometryInternal(component.GlyphIndex, wrappedContext, Matrix.Identity, decycler)) |
|||
{ |
|||
hasGeometry = true; |
|||
} |
|||
} |
|||
|
|||
return hasGeometry; |
|||
} |
|||
finally |
|||
{ |
|||
// Return rented buffer to pool
|
|||
compositeGlyph.Dispose(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Wrapper that applies a matrix transform to coordinates before delegating to the real context.
|
|||
/// </summary>
|
|||
private sealed class TransformingGeometryContext : IGeometryContext |
|||
{ |
|||
private readonly IGeometryContext _inner; |
|||
private readonly Matrix _matrix; |
|||
|
|||
public TransformingGeometryContext(IGeometryContext inner, Matrix matrix) |
|||
{ |
|||
_inner = inner; |
|||
_matrix = matrix; |
|||
} |
|||
|
|||
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked = true) |
|||
{ |
|||
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log)) |
|||
{ |
|||
log.Log(_inner, "ArcTo {0} {1} rot={2} large={3} sweep={4}", point, size, rotationAngle, isLargeArc, sweepDirection); |
|||
} |
|||
|
|||
var tp = _matrix.Transform(point); |
|||
|
|||
_inner.ArcTo(tp, size, rotationAngle, isLargeArc, sweepDirection, isStroked); |
|||
} |
|||
|
|||
public void BeginFigure(Point startPoint, bool isFilled = true) |
|||
{ |
|||
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log)) |
|||
{ |
|||
log.Log(_inner, "BeginFigure {0} filled={1}", startPoint, isFilled); |
|||
} |
|||
|
|||
var tp = _matrix.Transform(startPoint); |
|||
|
|||
_inner.BeginFigure(tp, isFilled); |
|||
} |
|||
|
|||
public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked = true) |
|||
{ |
|||
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log)) |
|||
{ |
|||
log.Log(_inner, "CubicBezierTo cp1={0} cp2={1} end={2}", controlPoint1, controlPoint2, endPoint); |
|||
} |
|||
|
|||
_inner.CubicBezierTo(_matrix.Transform(controlPoint1), _matrix.Transform(controlPoint2), _matrix.Transform(endPoint), isStroked); |
|||
} |
|||
|
|||
public void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked = true) |
|||
{ |
|||
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log)) |
|||
{ |
|||
log.Log(_inner, "QuadraticBezierTo cp={0} end={1}", controlPoint, endPoint); |
|||
} |
|||
|
|||
_inner.QuadraticBezierTo(_matrix.Transform(controlPoint), _matrix.Transform(endPoint), isStroked); |
|||
} |
|||
|
|||
public void LineTo(Point endPoint, bool isStroked = true) |
|||
{ |
|||
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log)) |
|||
{ |
|||
log.Log(_inner, "LineTo {0}", endPoint); |
|||
} |
|||
|
|||
_inner.LineTo(_matrix.Transform(endPoint), isStroked); |
|||
} |
|||
|
|||
public void EndFigure(bool isClosed) |
|||
{ |
|||
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log)) |
|||
{ |
|||
log.Log(_inner, "EndFigure closed={0}", isClosed); |
|||
} |
|||
|
|||
_inner.EndFigure(isClosed); |
|||
} |
|||
|
|||
public void SetFillRule(FillRule fillRule) |
|||
{ |
|||
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log)) |
|||
{ |
|||
log.Log(_inner, "SetFillRule {0}", fillRule); |
|||
} |
|||
|
|||
_inner.SetFillRule(fillRule); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (Logger.TryGet(LogEventLevel.Debug, LogArea.Visual, out var log)) |
|||
{ |
|||
log.Log(_inner, "Dispose TransformingGeometryContext"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
namespace Avalonia.Media.Fonts.Tables.Glyf |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a single component of a composite glyph, including its transformation and positioning information.
|
|||
/// </summary>
|
|||
/// <remarks>A composite glyph is constructed from one or more components, each referencing another glyph
|
|||
/// and specifying how it should be transformed and positioned within the composite. This structure encapsulates the
|
|||
/// data required to interpret a component according to the OpenType or TrueType font specifications. It is intended
|
|||
/// for internal use when parsing or processing composite glyph outlines.</remarks>
|
|||
internal readonly struct GlyphComponent |
|||
{ |
|||
public CompositeFlags Flags { get; init; } |
|||
public ushort GlyphIndex { get; init; } |
|||
public short Arg1 { get; init; } |
|||
public short Arg2 { get; init; } |
|||
public float Scale { get; init; } |
|||
public float ScaleX { get; init; } |
|||
public float ScaleY { get; init; } |
|||
public float Scale01 { get; init; } |
|||
public float Scale10 { get; init; } |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
using Avalonia.Media.Fonts.Tables.Colr; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Glyf |
|||
{ |
|||
/// <summary>
|
|||
/// Type alias for the glyph decycler that tracks visited glyphs during composite glyph processing.
|
|||
/// </summary>
|
|||
internal class GlyphDecycler : Decycler<int> |
|||
{ |
|||
/// <summary>
|
|||
/// Maximum depth for glyph graph traversal.
|
|||
/// This limit prevents stack overflow from deeply nested composite glyphs.
|
|||
/// </summary>
|
|||
public const int MaxTraversalDepth = 64; |
|||
|
|||
private static readonly ObjectPool<GlyphDecycler> Pool = new ObjectPool<GlyphDecycler>( |
|||
factory: () => new GlyphDecycler(), |
|||
validator: decycler => |
|||
{ |
|||
decycler.Reset(); |
|||
return true; |
|||
}, |
|||
maxSize: 16); |
|||
|
|||
public GlyphDecycler() : base(MaxTraversalDepth) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Rents a GlyphDecycler from the pool.
|
|||
/// </summary>
|
|||
/// <returns>A pooled GlyphDecycler instance.</returns>
|
|||
public static GlyphDecycler Rent() |
|||
{ |
|||
return Pool.Rent(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a GlyphDecycler to the pool.
|
|||
/// </summary>
|
|||
/// <param name="decycler">The decycler to return to the pool.</param>
|
|||
public static void Return(GlyphDecycler decycler) |
|||
{ |
|||
Pool.Return(decycler); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.InteropServices; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Glyf |
|||
{ |
|||
/// <summary>
|
|||
/// Represents the descriptor for a glyph in a font, providing access to its outline and bounding metrics.
|
|||
/// </summary>
|
|||
/// <remarks>A glyph descriptor exposes contour and bounding box information for a glyph, as well as
|
|||
/// access to its simple or composite outline data. Use the properties to determine the glyph type and retrieve the
|
|||
/// corresponding outline representation.</remarks>
|
|||
internal class GlyphDescriptor |
|||
{ |
|||
private readonly ReadOnlyMemory<byte> _glyphData; |
|||
|
|||
public GlyphDescriptor(ReadOnlyMemory<byte> data) |
|||
{ |
|||
var span = data.Span; |
|||
|
|||
NumberOfContours = BinaryPrimitives.ReadInt16BigEndian(span.Slice(0, 2)); |
|||
|
|||
var xMin = BinaryPrimitives.ReadInt16BigEndian(span.Slice(2, 2)); |
|||
var yMin = BinaryPrimitives.ReadInt16BigEndian(span.Slice(4, 2)); |
|||
var xMax = BinaryPrimitives.ReadInt16BigEndian(span.Slice(6, 2)); |
|||
var yMax = BinaryPrimitives.ReadInt16BigEndian(span.Slice(8, 2)); |
|||
|
|||
// Store as Rect - note: coordinates are in font design units
|
|||
ConservativeBounds = new Rect(xMin, yMin, xMax - xMin, yMax - yMin); |
|||
|
|||
_glyphData = data.Slice(10); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of contours in the glyph.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// If the value is greater than or equal to zero, the glyph is a simple glyph.
|
|||
/// If the value is negative (typically -1), the glyph is a composite glyph.
|
|||
/// </remarks>
|
|||
public short NumberOfContours { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the conservative bounding box for the glyph in font design units.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This represents the minimum bounding rectangle that contains all points in the glyph outline.
|
|||
/// The coordinates are in the font's coordinate system (design units), not scaled to any particular size.
|
|||
/// For proper rendering, these coordinates should be transformed by the font matrix and scaled
|
|||
/// by the font rendering size.
|
|||
/// </remarks>
|
|||
public Rect ConservativeBounds { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether this glyph is a simple glyph (as opposed to a composite glyph).
|
|||
/// </summary>
|
|||
public bool IsSimpleGlyph => NumberOfContours >= 0; |
|||
|
|||
/// <summary>
|
|||
/// Gets the simple glyph outline data.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This property should only be accessed if <see cref="IsSimpleGlyph"/> is true.
|
|||
/// The returned struct holds references to rented arrays and must be disposed.
|
|||
/// </remarks>
|
|||
public SimpleGlyph SimpleGlyph => SimpleGlyph.Create(_glyphData.Span, NumberOfContours); |
|||
|
|||
/// <summary>
|
|||
/// Gets the composite glyph outline data.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This property should only be accessed if <see cref="IsSimpleGlyph"/> is false.
|
|||
/// The returned struct holds references to rented arrays and must be disposed.
|
|||
/// </remarks>
|
|||
public CompositeGlyph CompositeGlyph => CompositeGlyph.Create(_glyphData.Span); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Glyf |
|||
{ |
|||
[Flags] |
|||
internal enum GlyphFlag : byte |
|||
{ |
|||
None = 0x00, |
|||
OnCurvePoint = 0x01, |
|||
XShortVector = 0x02, |
|||
YShortVector = 0x04, |
|||
Repeat = 0x08, |
|||
XIsSameOrPositiveXShortVector = 0x10, |
|||
YIsSameOrPositiveYShortVector = 0x20, |
|||
Reserved1 = 0x40, |
|||
Reserved2 = 0x80 |
|||
} |
|||
} |
|||
@ -0,0 +1,275 @@ |
|||
using System; |
|||
using System.Buffers; |
|||
using System.Buffers.Binary; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Glyf |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a simple glyph outline as defined in the TrueType font format, including contour endpoints,
|
|||
/// instructions, flags, and coordinate data. Provides access to the parsed glyph data for rendering or analysis.
|
|||
/// </summary>
|
|||
/// <remarks>A simple glyph consists of one or more contours, each defined by a sequence of points. This
|
|||
/// struct exposes the raw data necessary to interpret the glyph's shape, including the endpoints of each contour,
|
|||
/// the TrueType instructions for hinting, per-point flags, and the X and Y coordinates of each point. The struct is
|
|||
/// intended for low-level font processing and is not thread-safe. This struct holds references to rented arrays
|
|||
/// that must be returned via Dispose.</remarks>
|
|||
internal readonly ref struct SimpleGlyph |
|||
{ |
|||
// Rented buffers for flags
|
|||
private readonly GlyphFlag[]? _rentedFlags; |
|||
// Rented buffers for y-coordinates
|
|||
private readonly short[]? _rentedXCoords; |
|||
// Rented buffers for x-coordinates
|
|||
private readonly short[]? _rentedYCoords; |
|||
|
|||
/// <summary>
|
|||
/// Gets the indices of the last point in each contour within the glyph outline.
|
|||
/// </summary>
|
|||
/// <remarks>The indices are zero-based and correspond to positions in the glyph's point array.
|
|||
/// The number of elements indicates the number of contours in the glyph.</remarks>
|
|||
public ReadOnlySpan<ushort> EndPtsOfContours { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the instruction data.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<byte> Instructions { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the collection of flags associated with the glyph.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<GlyphFlag> Flags { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the X coordinates.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<short> XCoordinates { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the Y coordinates.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<short> YCoordinates { get; } |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the SimpleGlyph class using the specified contour endpoints, instructions,
|
|||
/// flags, and coordinate data.
|
|||
/// </summary>
|
|||
/// <remarks>The rented arrays, if supplied, are used to optimize memory usage and may be returned
|
|||
/// to a pool after use. Callers should not access or modify these arrays after passing them to the
|
|||
/// constructor.</remarks>
|
|||
/// <param name="endPtsOfContours">A read-only span containing the indices of the last point in each contour. The values define the structure
|
|||
/// of the glyph's outline.</param>
|
|||
/// <param name="instructions">A read-only span containing the TrueType instructions associated with the glyph. These instructions control
|
|||
/// glyph rendering and hinting.</param>
|
|||
/// <param name="flags">A read-only span of flags describing the attributes of each glyph point, such as whether a point is on-curve
|
|||
/// or off-curve.</param>
|
|||
/// <param name="xCoordinates">A read-only span containing the X coordinates for each glyph point, in font units.</param>
|
|||
/// <param name="yCoordinates">A read-only span containing the Y coordinates for each glyph point, in font units.</param>
|
|||
/// <param name="rentedFlags">An optional array of GlyphFlag values used for temporary storage. If provided, the array may be reused
|
|||
/// internally to reduce allocations.</param>
|
|||
/// <param name="rentedXCoords">An optional array of short values used for temporary storage of X coordinates. If provided, the array may be
|
|||
/// reused internally to reduce allocations.</param>
|
|||
/// <param name="rentedYCoords">An optional array of short values used for temporary storage of Y coordinates. If provided, the array may be
|
|||
/// reused internally to reduce allocations.</param>
|
|||
private SimpleGlyph( |
|||
ReadOnlySpan<ushort> endPtsOfContours, |
|||
ReadOnlySpan<byte> instructions, |
|||
ReadOnlySpan<GlyphFlag> flags, |
|||
ReadOnlySpan<short> xCoordinates, |
|||
ReadOnlySpan<short> yCoordinates, |
|||
GlyphFlag[]? rentedFlags, |
|||
short[]? rentedXCoords, |
|||
short[]? rentedYCoords) |
|||
{ |
|||
EndPtsOfContours = endPtsOfContours; |
|||
Instructions = instructions; |
|||
Flags = flags; |
|||
XCoordinates = xCoordinates; |
|||
YCoordinates = yCoordinates; |
|||
_rentedFlags = rentedFlags; |
|||
_rentedXCoords = rentedXCoords; |
|||
_rentedYCoords = rentedYCoords; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="SimpleGlyph"/> structure by parsing glyph data from the specified
|
|||
/// byte span.
|
|||
/// </summary>
|
|||
/// <remarks>The returned <see cref="SimpleGlyph"/> uses buffers rented from array pools for
|
|||
/// performance. Callers are responsible for disposing or returning these buffers if required by the <see
|
|||
/// cref="SimpleGlyph"/> implementation. The method does not validate the integrity of the glyph data beyond the
|
|||
/// contour count; malformed data may result in exceptions or undefined behavior.</remarks>
|
|||
/// <param name="data">A read-only span of bytes containing the raw glyph data to parse. The data must be formatted according to
|
|||
/// the TrueType simple glyph specification.</param>
|
|||
/// <param name="numberOfContours">The number of contours in the glyph. Must be greater than zero; otherwise, a default value is returned.</param>
|
|||
/// <returns>A <see cref="SimpleGlyph"/> instance representing the parsed glyph data. Returns the default value if
|
|||
/// <paramref name="numberOfContours"/> is less than or equal to zero.</returns>
|
|||
public static SimpleGlyph Create(ReadOnlySpan<byte> data, int numberOfContours) |
|||
{ |
|||
if (numberOfContours <= 0) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
// Endpoints of contours
|
|||
var endPtsOfContours = new ushort[numberOfContours]; |
|||
var endPtsBytes = data.Slice(0, numberOfContours * 2); |
|||
|
|||
for (int i = 0; i < numberOfContours; i++) |
|||
{ |
|||
endPtsOfContours[i] = BinaryPrimitives.ReadUInt16BigEndian(endPtsBytes.Slice(i * 2, 2)); |
|||
} |
|||
|
|||
// Instructions
|
|||
int instructionsOffset = numberOfContours * 2; |
|||
ushort instructionsLength = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(instructionsOffset, 2)); |
|||
var instructions = data.Slice(instructionsOffset + 2, instructionsLength); |
|||
|
|||
// Number of points
|
|||
int numPoints = endPtsOfContours[numberOfContours - 1] + 1; |
|||
|
|||
// Rent buffers
|
|||
int flagsOffset = instructionsOffset + 2 + instructionsLength; |
|||
var flagsBuffer = ArrayPool<GlyphFlag>.Shared.Rent(numPoints); |
|||
var xCoordsBuffer = ArrayPool<short>.Shared.Rent(numPoints); |
|||
var yCoordsBuffer = ArrayPool<short>.Shared.Rent(numPoints); |
|||
|
|||
try |
|||
{ |
|||
// Decode flags
|
|||
int flagIndex = 0; |
|||
int offset = flagsOffset; |
|||
|
|||
while (flagIndex < numPoints) |
|||
{ |
|||
var flag = (GlyphFlag)data[offset++]; |
|||
flagsBuffer[flagIndex++] = flag; |
|||
|
|||
// Repeat flag
|
|||
if ((flag & GlyphFlag.Repeat) != 0) |
|||
{ |
|||
// Read repeat count
|
|||
byte repeatCount = data[offset++]; |
|||
|
|||
// Repeat the flag
|
|||
for (int i = 0; i < repeatCount && flagIndex < numPoints; i++) |
|||
{ |
|||
flagsBuffer[flagIndex++] = flag; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Decode X coordinates
|
|||
short x = 0; |
|||
|
|||
for (int i = 0; i < numPoints; i++) |
|||
{ |
|||
var flag = flagsBuffer[i]; |
|||
|
|||
// Short vector
|
|||
if ((flag & GlyphFlag.XShortVector) != 0) |
|||
{ |
|||
byte dx = data[offset++]; |
|||
|
|||
if ((flag & GlyphFlag.XIsSameOrPositiveXShortVector) != 0) |
|||
{ |
|||
x += (short)dx; |
|||
} |
|||
else |
|||
{ |
|||
x -= (short)dx; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Not a short vector
|
|||
if ((flag & GlyphFlag.XIsSameOrPositiveXShortVector) == 0) |
|||
{ |
|||
short dx = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)); |
|||
offset += 2; |
|||
x += dx; |
|||
} |
|||
} |
|||
|
|||
xCoordsBuffer[i] = x; |
|||
} |
|||
|
|||
// Decode Y coordinates
|
|||
short y = 0; |
|||
|
|||
for (int i = 0; i < numPoints; i++) |
|||
{ |
|||
var flag = flagsBuffer[i]; |
|||
|
|||
// Short vector
|
|||
if ((flag & GlyphFlag.YShortVector) != 0) |
|||
{ |
|||
byte dy = data[offset++]; |
|||
if ((flag & GlyphFlag.YIsSameOrPositiveYShortVector) != 0) |
|||
{ |
|||
y += (short)dy; |
|||
} |
|||
else |
|||
{ |
|||
y -= (short)dy; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Not a short vector
|
|||
if ((flag & GlyphFlag.YIsSameOrPositiveYShortVector) == 0) |
|||
{ |
|||
short dy = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)); |
|||
offset += 2; |
|||
y += dy; |
|||
} |
|||
} |
|||
|
|||
yCoordsBuffer[i] = y; |
|||
} |
|||
|
|||
return new SimpleGlyph( |
|||
endPtsOfContours, |
|||
instructions, |
|||
flagsBuffer.AsSpan(0, numPoints), |
|||
xCoordsBuffer.AsSpan(0, numPoints), |
|||
yCoordsBuffer.AsSpan(0, numPoints), |
|||
flagsBuffer, |
|||
xCoordsBuffer, |
|||
yCoordsBuffer |
|||
); |
|||
} |
|||
catch |
|||
{ |
|||
// On exception, return buffers immediately
|
|||
ArrayPool<GlyphFlag>.Shared.Return(flagsBuffer); |
|||
ArrayPool<short>.Shared.Return(xCoordsBuffer); |
|||
ArrayPool<short>.Shared.Return(yCoordsBuffer); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the rented buffers to the ArrayPool.
|
|||
/// </summary>
|
|||
/// <remarks>This method should be called when the SimpleGlyph is no longer needed
|
|||
/// to ensure the rented buffers are returned to the pool.</remarks>
|
|||
public void Dispose() |
|||
{ |
|||
if (_rentedFlags != null) |
|||
{ |
|||
ArrayPool<GlyphFlag>.Shared.Return(_rentedFlags); |
|||
} |
|||
|
|||
if (_rentedXCoords != null) |
|||
{ |
|||
ArrayPool<short>.Shared.Return(_rentedXCoords); |
|||
} |
|||
|
|||
if (_rentedYCoords != null) |
|||
{ |
|||
ArrayPool<short>.Shared.Return(_rentedYCoords); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables |
|||
{ |
|||
/// <summary>
|
|||
/// Provides efficient access to the 'loca' (Index to Location) table without pre-allocating an array.
|
|||
/// The loca table stores offsets into the glyf table for each glyph.
|
|||
/// </summary>
|
|||
internal sealed class LocaTable |
|||
{ |
|||
internal const string TableName = "loca"; |
|||
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); |
|||
|
|||
private readonly ReadOnlyMemory<byte> _data; |
|||
private readonly int _glyphCount; |
|||
private readonly bool _isShortFormat; |
|||
|
|||
private LocaTable(ReadOnlyMemory<byte> data, int glyphCount, bool isShortFormat) |
|||
{ |
|||
_data = data; |
|||
_glyphCount = glyphCount; |
|||
_isShortFormat = isShortFormat; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of glyphs in the font.
|
|||
/// </summary>
|
|||
public int GlyphCount => _glyphCount; |
|||
|
|||
/// <summary>
|
|||
/// Loads the loca table from the specified typeface.
|
|||
/// </summary>
|
|||
/// <param name="glyphTypeface">The glyph typeface to load from.</param>
|
|||
/// <param name="head">The head table containing the index format.</param>
|
|||
/// <param name="maxp">The maxp table containing the glyph count.</param>
|
|||
/// <returns>A LocaTable instance, or null if the table cannot be loaded.</returns>
|
|||
public static LocaTable? Load(GlyphTypeface glyphTypeface, HeadTable head, MaxpTable maxp) |
|||
{ |
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var locaData)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var isShortFormat = head.IndexToLocFormat == 0; |
|||
var glyphCount = maxp.NumGlyphs; |
|||
|
|||
// Validate table size
|
|||
var expectedSize = isShortFormat ? (glyphCount + 1) * 2 : (glyphCount + 1) * 4; |
|||
|
|||
if (locaData.Length < expectedSize) |
|||
{ |
|||
// Table is shorter than expected, but we can still work with what we have
|
|||
// The GetOffset method will handle out-of-bounds access
|
|||
} |
|||
|
|||
return new LocaTable(locaData, glyphCount, isShortFormat); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the start and end offsets for the specified glyph index.
|
|||
/// </summary>
|
|||
/// <param name="glyphIndex">The glyph index (0-based).</param>
|
|||
/// <param name="start">The start offset into the glyf table.</param>
|
|||
/// <param name="end">The end offset into the glyf table.</param>
|
|||
/// <returns>True if the offsets were retrieved successfully; otherwise, false.</returns>
|
|||
public bool TryGetOffsets(int glyphIndex, out int start, out int end) |
|||
{ |
|||
if ((uint)glyphIndex >= (uint)_glyphCount) |
|||
{ |
|||
start = 0; |
|||
end = 0; |
|||
return false; |
|||
} |
|||
|
|||
start = GetOffset(glyphIndex); |
|||
end = GetOffset(glyphIndex + 1); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the offset for the specified glyph index into the glyf table.
|
|||
/// </summary>
|
|||
/// <param name="glyphIndex">The glyph index (0-based).</param>
|
|||
/// <returns>The offset into the glyf table, or 0 if the index is out of range.</returns>
|
|||
private int GetOffset(int glyphIndex) |
|||
{ |
|||
if ((uint)glyphIndex > (uint)_glyphCount) // Note: allows glyphCount for the end offset
|
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
var span = _data.Span; |
|||
|
|||
if (_isShortFormat) |
|||
{ |
|||
var byteOffset = glyphIndex * 2; |
|||
|
|||
if (byteOffset + 2 > span.Length) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
// Short format: uint16 values stored divided by 2
|
|||
var value = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(byteOffset, 2)); |
|||
return value * 2; |
|||
} |
|||
else |
|||
{ |
|||
var byteOffset = glyphIndex * 4; |
|||
|
|||
if (byteOffset + 4 > span.Length) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
// Long format: uint32 values
|
|||
var value = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(byteOffset, 4)); |
|||
|
|||
// Clamp to int.MaxValue to avoid overflow
|
|||
return value > int.MaxValue ? int.MaxValue : (int)value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Specifies the format used to render a glyph, such as outline, color layers, SVG, or bitmap.
|
|||
/// </summary>
|
|||
/// <remarks>Use this enumeration to determine or specify how a glyph should be drawn, depending on the
|
|||
/// font's supported formats. The value corresponds to the glyph's representation in the font file, which may affect
|
|||
/// rendering capabilities and visual appearance.</remarks>
|
|||
public enum GlyphDrawingType |
|||
{ |
|||
Outline, // glyf / CFF / CFF2
|
|||
ColorLayers, // COLR/CPAL
|
|||
Svg, // SVG table
|
|||
Bitmap // sbix / CBDT / EBDT
|
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
public interface IGlyphDrawing |
|||
{ |
|||
Rect Bounds { get; } |
|||
|
|||
void Draw(DrawingContext context, Point origin); |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Provides a thread-safe object pool with size limits and object validation.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of objects to pool.</typeparam>
|
|||
internal sealed class ObjectPool<T> where T : class |
|||
{ |
|||
private readonly Func<T> _factory; |
|||
private readonly Func<T, bool>? _validator; |
|||
private readonly ConcurrentBag<T> _items; |
|||
private readonly int _maxSize; |
|||
private int _count; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ObjectPool{T}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="factory">Factory function to create new instances.</param>
|
|||
/// <param name="validator">Optional validator to clean and validate objects before returning to the pool. Return false to discard the object.</param>
|
|||
/// <param name="maxSize">Maximum number of objects to keep in the pool. Default is 32.</param>
|
|||
public ObjectPool(Func<T> factory, Func<T, bool>? validator = null, int maxSize = 32) |
|||
{ |
|||
_factory = factory ?? throw new ArgumentNullException(nameof(factory)); |
|||
_validator = validator; |
|||
_maxSize = maxSize; |
|||
_items = new ConcurrentBag<T>(); |
|||
_count = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Rents an object from the pool or creates a new one if the pool is empty.
|
|||
/// </summary>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public T Rent() |
|||
{ |
|||
if (_items.TryTake(out var item)) |
|||
{ |
|||
System.Threading.Interlocked.Decrement(ref _count); |
|||
return item; |
|||
} |
|||
|
|||
return _factory(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns an object to the pool if it passes validation and the pool is not full.
|
|||
/// </summary>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public void Return(T item) |
|||
{ |
|||
if (item == null) |
|||
return; |
|||
|
|||
// Validate and clean the object
|
|||
if (_validator != null && !_validator(item)) |
|||
return; |
|||
|
|||
// Check if pool is full (fast check without lock)
|
|||
if (_count >= _maxSize) |
|||
return; |
|||
|
|||
// Try to increment count, but check again in case of race condition
|
|||
var currentCount = System.Threading.Interlocked.Increment(ref _count); |
|||
|
|||
if (currentCount <= _maxSize) |
|||
{ |
|||
_items.Add(item); |
|||
} |
|||
else |
|||
{ |
|||
// Pool is full, decrement and discard
|
|||
System.Threading.Interlocked.Decrement(ref _count); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts; |
|||
using HarfBuzzSharp; |
|||
using SharpDX.DirectWrite; |
|||
|
|||
namespace Avalonia.Direct2D1.Media |
|||
{ |
|||
internal class DWriteTypeface : IPlatformTypeface |
|||
{ |
|||
private bool _isDisposed; |
|||
|
|||
public DWriteTypeface(SharpDX.DirectWrite.Font font) |
|||
{ |
|||
DWFont = font; |
|||
|
|||
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>(); |
|||
|
|||
Weight = (Avalonia.Media.FontWeight)DWFont.Weight; |
|||
|
|||
Style = (Avalonia.Media.FontStyle)DWFont.Style; |
|||
|
|||
Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch; |
|||
} |
|||
|
|||
private static uint SwapBytes(uint x) |
|||
{ |
|||
x = (x >> 16) | (x << 16); |
|||
|
|||
return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8); |
|||
} |
|||
|
|||
public SharpDX.DirectWrite.Font DWFont { get; } |
|||
|
|||
public FontFace1 FontFace { get; } |
|||
|
|||
public Face Face { get; } |
|||
|
|||
public HarfBuzzSharp.Font Font { get; } |
|||
|
|||
public Avalonia.Media.FontWeight Weight { get; } |
|||
|
|||
public Avalonia.Media.FontStyle Style { get; } |
|||
|
|||
public Avalonia.Media.FontStretch Stretch { get; } |
|||
|
|||
private void Dispose(bool disposing) |
|||
{ |
|||
if (_isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_isDisposed = true; |
|||
|
|||
if (!disposing) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Font?.Dispose(); |
|||
Face?.Dispose(); |
|||
FontFace?.Dispose(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Dispose(true); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table) |
|||
{ |
|||
table = default; |
|||
|
|||
var dwTag = (int)SwapBytes((uint)tag); |
|||
|
|||
if (FontFace.TryGetFontTable(dwTag, out var tableData, out _)) |
|||
{ |
|||
table = tableData.ToArray(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public bool TryGetStream([NotNullWhen(true)] out Stream stream) |
|||
{ |
|||
stream = default; |
|||
|
|||
var files = FontFace.GetFiles(); |
|||
|
|||
if (files.Length > 0) |
|||
{ |
|||
var file = files[0]; |
|||
|
|||
var referenceKey = file.GetReferenceKey(); |
|||
|
|||
stream = referenceKey.ToDataStream(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,243 @@ |
|||
using System; |
|||
using Avalonia.Media.Fonts.Tables.Colr; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Media.Fonts.Tables.Colr |
|||
{ |
|||
public class DeltaSetIndexMapTests |
|||
{ |
|||
[Fact] |
|||
public void Load_Format0_WithValidData_ShouldSucceed() |
|||
{ |
|||
// DeltaSetIndexMap Format 0:
|
|||
// uint8 format = 0
|
|||
// uint8 entryFormat = 0x00 (1 byte inner, 1 byte outer)
|
|||
// uint16 mapCount = 3
|
|||
// Map data: [outer0, inner0], [outer1, inner1], [outer2, inner2]
|
|||
var data = new byte[] |
|||
{ |
|||
0, // format = 0
|
|||
0x00, // entryFormat = 0x00 (1 byte each)
|
|||
0, 3, // mapCount = 3 (uint16 big-endian)
|
|||
// Map entries (outer, inner):
|
|||
0, 0, // entry 0: outer=0, inner=0
|
|||
0, 1, // entry 1: outer=0, inner=1
|
|||
1, 0 // entry 2: outer=1, inner=0
|
|||
}; |
|||
|
|||
var map = DeltaSetIndexMap.Load(data, 0); |
|||
|
|||
Assert.NotNull(map); |
|||
Assert.Equal(0, map!.Format); |
|||
Assert.Equal(3u, map.MapCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Load_Format1_WithValidData_ShouldSucceed() |
|||
{ |
|||
// DeltaSetIndexMap Format 1:
|
|||
// uint8 format = 1
|
|||
// uint8 entryFormat = 0x11 (2 bytes inner, 2 bytes outer)
|
|||
// uint32 mapCount = 2
|
|||
var data = new byte[] |
|||
{ |
|||
1, // format = 1
|
|||
0x11, // entryFormat = 0x11 (2 bytes each)
|
|||
0, 0, 0, 2, // mapCount = 2 (uint32 big-endian)
|
|||
// Map entries (outer, inner):
|
|||
0, 5, 0, 10, // entry 0: outer=5, inner=10
|
|||
0, 6, 0, 20 // entry 1: outer=6, inner=20
|
|||
}; |
|||
|
|||
var map = DeltaSetIndexMap.Load(data, 0); |
|||
|
|||
Assert.NotNull(map); |
|||
Assert.Equal(1, map!.Format); |
|||
Assert.Equal(2u, map.MapCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void TryGetDeltaSetIndex_WithFormat0_ShouldReturnCorrectIndices() |
|||
{ |
|||
var data = new byte[] |
|||
{ |
|||
0, // format = 0
|
|||
0x00, // entryFormat = 0x00
|
|||
0, 3, // mapCount = 3
|
|||
0, 0, // entry 0: outer=0, inner=0
|
|||
0, 1, // entry 1: outer=0, inner=1
|
|||
1, 0 // entry 2: outer=1, inner=0
|
|||
}; |
|||
|
|||
var map = DeltaSetIndexMap.Load(data, 0); |
|||
Assert.NotNull(map); |
|||
|
|||
// Test entry 0
|
|||
Assert.True(map!.TryGetDeltaSetIndex(0, out var outer0, out var inner0)); |
|||
Assert.Equal(0, outer0); |
|||
Assert.Equal(0, inner0); |
|||
|
|||
// Test entry 1
|
|||
Assert.True(map.TryGetDeltaSetIndex(1, out var outer1, out var inner1)); |
|||
Assert.Equal(0, outer1); |
|||
Assert.Equal(1, inner1); |
|||
|
|||
// Test entry 2
|
|||
Assert.True(map.TryGetDeltaSetIndex(2, out var outer2, out var inner2)); |
|||
Assert.Equal(1, outer2); |
|||
Assert.Equal(0, inner2); |
|||
} |
|||
|
|||
[Fact] |
|||
public void TryGetDeltaSetIndex_WithFormat1And2ByteEntries_ShouldReturnCorrectIndices() |
|||
{ |
|||
var data = new byte[] |
|||
{ |
|||
1, // format = 1
|
|||
0x11, // entryFormat = 0x11 (2 bytes each)
|
|||
0, 0, 0, 2, // mapCount = 2
|
|||
0, 5, 0, 10, // entry 0: outer=5, inner=10
|
|||
0, 6, 0, 20 // entry 1: outer=6, inner=20
|
|||
}; |
|||
|
|||
var map = DeltaSetIndexMap.Load(data, 0); |
|||
Assert.NotNull(map); |
|||
|
|||
// Test entry 0
|
|||
Assert.True(map!.TryGetDeltaSetIndex(0, out var outer0, out var inner0)); |
|||
Assert.Equal(5, outer0); |
|||
Assert.Equal(10, inner0); |
|||
|
|||
// Test entry 1
|
|||
Assert.True(map.TryGetDeltaSetIndex(1, out var outer1, out var inner1)); |
|||
Assert.Equal(6, outer1); |
|||
Assert.Equal(20, inner1); |
|||
} |
|||
|
|||
[Fact] |
|||
public void TryGetDeltaSetIndex_WithOutOfRangeIndex_ShouldReturnFalse() |
|||
{ |
|||
var data = new byte[] |
|||
{ |
|||
0, // format = 0
|
|||
0x00, // entryFormat = 0x00
|
|||
0, 2, // mapCount = 2
|
|||
0, 0, |
|||
0, 1 |
|||
}; |
|||
|
|||
var map = DeltaSetIndexMap.Load(data, 0); |
|||
Assert.NotNull(map); |
|||
|
|||
// Try to access index 2, which is out of range (map has only 2 entries: 0 and 1)
|
|||
Assert.False(map!.TryGetDeltaSetIndex(2, out _, out _)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Load_WithInvalidFormat_ShouldReturnNull() |
|||
{ |
|||
var data = new byte[] |
|||
{ |
|||
2, // format = 2 (invalid)
|
|||
0x00, |
|||
0, 1 |
|||
}; |
|||
|
|||
var map = DeltaSetIndexMap.Load(data, 0); |
|||
|
|||
Assert.Null(map); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Load_WithInsufficientData_ShouldReturnNull() |
|||
{ |
|||
var data = new byte[] { 0 }; // Only format byte, missing entryFormat and mapCount
|
|||
|
|||
var map = DeltaSetIndexMap.Load(data, 0); |
|||
|
|||
Assert.Null(map); |
|||
} |
|||
|
|||
[Fact] |
|||
public void TryGetDeltaSetIndex_WithMixedEntryFormat_ShouldReturnCorrectIndices() |
|||
{ |
|||
// entryFormat = 0x10 means 1-byte inner, 2-byte outer (3 bytes per entry)
|
|||
var data = new byte[] |
|||
{ |
|||
0, // format = 0
|
|||
0x10, // entryFormat = 0x10
|
|||
0, 2, // mapCount = 2
|
|||
0, 5, 10, // entry 0: outer=5 (2 bytes), inner=10 (1 byte)
|
|||
0, 6, 20 // entry 1: outer=6 (2 bytes), inner=20 (1 byte)
|
|||
}; |
|||
|
|||
var map = DeltaSetIndexMap.Load(data, 0); |
|||
Assert.NotNull(map); |
|||
|
|||
// Test entry 0
|
|||
Assert.True(map!.TryGetDeltaSetIndex(0, out var outer0, out var inner0)); |
|||
Assert.Equal(5, outer0); |
|||
Assert.Equal(10, inner0); |
|||
|
|||
// Test entry 1
|
|||
Assert.True(map.TryGetDeltaSetIndex(1, out var outer1, out var inner1)); |
|||
Assert.Equal(6, outer1); |
|||
Assert.Equal(20, inner1); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Load_ShouldReturnSameInstance_WhenCalledMultipleTimes() |
|||
{ |
|||
// This test verifies that the same byte array always produces consistent results
|
|||
// The DeltaSetIndexMap.Load method should be deterministic
|
|||
var data = new byte[] |
|||
{ |
|||
0, // format = 0
|
|||
0x00, // entryFormat = 0x00
|
|||
0, 2, // mapCount = 2
|
|||
0, 0, |
|||
0, 1 |
|||
}; |
|||
|
|||
var map1 = DeltaSetIndexMap.Load(data, 0); |
|||
var map2 = DeltaSetIndexMap.Load(data, 0); |
|||
|
|||
Assert.NotNull(map1); |
|||
Assert.NotNull(map2); |
|||
|
|||
// Verify both instances return the same data
|
|||
Assert.True(map1!.TryGetDeltaSetIndex(0, out var outer1, out var inner1)); |
|||
Assert.True(map2!.TryGetDeltaSetIndex(0, out var outer2, out var inner2)); |
|||
|
|||
Assert.Equal(outer1, outer2); |
|||
Assert.Equal(inner1, inner2); |
|||
} |
|||
|
|||
[Fact] |
|||
public void TryGetDeltaSetIndex_WithLargeIndices_ShouldHandleCorrectly() |
|||
{ |
|||
// Test with larger outer/inner indices to verify 2-byte reading
|
|||
var data = new byte[] |
|||
{ |
|||
0, // format = 0
|
|||
0x11, // entryFormat = 0x11 (2 bytes each)
|
|||
0, 2, // mapCount = 2
|
|||
0x01, 0x00, 0x02, 0x00, // entry 0: outer=256, inner=512
|
|||
0xFF, 0xFF, 0xFF, 0xFF // entry 1: outer=65535, inner=65535
|
|||
}; |
|||
|
|||
var map = DeltaSetIndexMap.Load(data, 0); |
|||
Assert.NotNull(map); |
|||
|
|||
// Test entry 0
|
|||
Assert.True(map!.TryGetDeltaSetIndex(0, out var outer0, out var inner0)); |
|||
Assert.Equal(256, outer0); |
|||
Assert.Equal(512, inner0); |
|||
|
|||
// Test entry 1
|
|||
Assert.True(map.TryGetDeltaSetIndex(1, out var outer1, out var inner1)); |
|||
Assert.Equal(65535, outer1); |
|||
Assert.Equal(65535, inner1); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,712 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts.Tables.Colr; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
using GradientStop = Avalonia.Media.Fonts.Tables.Colr.GradientStop; |
|||
|
|||
namespace Avalonia.Skia.UnitTests.Media |
|||
{ |
|||
public class ColrTableTests |
|||
{ |
|||
[Fact] |
|||
public void Should_Load_COLR_Table_If_Present() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Color Emoji"); |
|||
|
|||
// Try to get glyph typeface - may or may not have COLR
|
|||
if (FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)) |
|||
{ |
|||
Assert.True(ColrTable.TryLoad(glyphTypeface, out var colrTable)); |
|||
|
|||
Assert.True(colrTable.Version <= 1); |
|||
Assert.True(colrTable.BaseGlyphCount >= 0); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Load_CPAL_Table_If_Present() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Color Emoji"); |
|||
|
|||
if (FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)) |
|||
{ |
|||
Assert.True(CpalTable.TryLoad(glyphTypeface, out var cpalTable)); |
|||
|
|||
Assert.True(cpalTable.Version <= 1); |
|||
Assert.True(cpalTable.PaletteCount > 0); |
|||
Assert.True(cpalTable.PaletteEntryCount > 0); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Return_Null_For_Missing_COLR_Table() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
// Using a font that definitely doesn't have COLR
|
|||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); |
|||
|
|||
if (FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)) |
|||
{ |
|||
Assert.False(ColrTable.TryLoad(glyphTypeface, out _)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Get_Layers_For_Color_Glyph() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Color Emoji"); |
|||
|
|||
if (FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)) |
|||
{ |
|||
Assert.True(ColrTable.TryLoad(glyphTypeface, out var colrTable)); |
|||
Assert.True(CpalTable.TryLoad(glyphTypeface, out var cpalTable)); |
|||
|
|||
// Try to get layers for a hypothetical color glyph
|
|||
ushort testGlyphId = 0; |
|||
|
|||
if (colrTable.TryGetBaseGlyphRecord(testGlyphId, out var baseRecord)) |
|||
{ |
|||
Assert.True(baseRecord.NumLayers > 0); |
|||
|
|||
// Get each layer
|
|||
for (int i = 0; i < baseRecord.NumLayers; i++) |
|||
{ |
|||
if (colrTable.TryGetLayerRecord(baseRecord.FirstLayerIndex + i, out var layer)) |
|||
{ |
|||
// Get the color for this layer
|
|||
if (cpalTable.TryGetColor(0, layer.PaletteIndex, out var color)) |
|||
{ |
|||
Assert.NotEqual(default, color); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
[Fact] |
|||
public void Should_Verify_Transform_Coordinate_Conversion_Preserves_Final_Bounds() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Color Emoji"); |
|||
|
|||
if (!FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Assert.True(ColrTable.TryLoad(glyphTypeface, out var colrTable)); |
|||
Assert.True(CpalTable.TryLoad(glyphTypeface, out var cpalTable)); |
|||
Assert.True(colrTable.HasV1Data); |
|||
|
|||
// Test with clock emoji 🕐 (U+1F550)
|
|||
const int clockCodepoint = 0x1F550; |
|||
var clockGlyphId = glyphTypeface.CharacterToGlyphMap[clockCodepoint]; |
|||
|
|||
if (clockGlyphId == 0 || !colrTable.TryGetBaseGlyphV1Record(clockGlyphId, out var baseGlyphRecord)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(ColrTable.Tag, out var colrData)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var context = new ColrContext(glyphTypeface, colrTable, cpalTable, 0); |
|||
var decycler = new PaintDecycler(); |
|||
|
|||
var rootPaintOffset = colrTable.GetAbsolutePaintOffset(baseGlyphRecord.PaintOffset); |
|||
|
|||
if (!PaintParser.TryParse(colrData.Span, rootPaintOffset, in context, in decycler, out var rootPaint)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Resolve the paint (this applies coordinate space conversion)
|
|||
var resolvedPaint = PaintResolver.ResolvePaint(rootPaint, in context); |
|||
|
|||
// Create a bounds tracking painter to measure actual rendered bounds
|
|||
var boundsPainter = new BoundsTrackingPainter(glyphTypeface); |
|||
|
|||
// Traverse the paint graph
|
|||
PaintTraverser.Traverse(resolvedPaint, boundsPainter, Matrix.Identity); |
|||
|
|||
var finalBounds = boundsPainter.GetFinalBounds(); |
|||
|
|||
// Verify bounds are reasonable (not collapsed or inverted)
|
|||
Assert.True(finalBounds.Width > 0, "Final bounds should have positive width"); |
|||
Assert.True(finalBounds.Height > 0, "Final bounds should have positive height"); |
|||
|
|||
// The bounds should be roughly square for a clock face (allow some tolerance)
|
|||
var aspectRatio = finalBounds.Width / finalBounds.Height; |
|||
Assert.True(aspectRatio > 0.5 && aspectRatio < 2.0, |
|||
$"Clock emoji aspect ratio should be roughly square, got {aspectRatio:F2}"); |
|||
|
|||
// Verify that transforms didn't cause bounds to become excessively large or small
|
|||
// Typical emoji glyph bounds in font units are in the range of 0-2048
|
|||
Assert.True(finalBounds.Width < 10000, "Bounds width should not be excessively large"); |
|||
Assert.True(finalBounds.Height < 10000, "Bounds height should not be excessively large"); |
|||
Assert.True(finalBounds.Width > 10, "Bounds width should not be too small"); |
|||
Assert.True(finalBounds.Height > 10, "Bounds height should not be too small"); |
|||
|
|||
System.Diagnostics.Debug.WriteLine($"Clock emoji 🕐 final bounds: {finalBounds}"); |
|||
System.Diagnostics.Debug.WriteLine($" Width: {finalBounds.Width:F2}, Height: {finalBounds.Height:F2}"); |
|||
System.Diagnostics.Debug.WriteLine($" Aspect ratio: {aspectRatio:F2}"); |
|||
System.Diagnostics.Debug.WriteLine($" Total glyphs rendered: {boundsPainter.GlyphCount}"); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Verify_Bridge_At_Night_Emoji_Transform_Coordinate_Conversion_Preserves_Final_Bounds() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Color Emoji"); |
|||
|
|||
if (!FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Assert.True(ColrTable.TryLoad(glyphTypeface, out var colrTable)); |
|||
Assert.True(CpalTable.TryLoad(glyphTypeface, out var cpalTable)); |
|||
Assert.True(colrTable.HasV1Data); |
|||
|
|||
// Test with bridge at night emoji 🌉 (U+1F309)
|
|||
const int bridgeCodepoint = 0x1F309; |
|||
var bridgeGlyphId = glyphTypeface.CharacterToGlyphMap[bridgeCodepoint]; |
|||
|
|||
if (bridgeGlyphId == 0 || !colrTable.TryGetBaseGlyphV1Record(bridgeGlyphId, out var baseGlyphRecord)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(ColrTable.Tag, out var colrData)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var context = new ColrContext(glyphTypeface, colrTable, cpalTable, 0); |
|||
var decycler = new PaintDecycler(); |
|||
|
|||
var rootPaintOffset = colrTable.GetAbsolutePaintOffset(baseGlyphRecord.PaintOffset); |
|||
|
|||
if (!PaintParser.TryParse(colrData.Span, rootPaintOffset, in context, in decycler, out var rootPaint)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Resolve the paint (this applies coordinate space conversion)
|
|||
var resolvedPaint = PaintResolver.ResolvePaint(rootPaint, in context); |
|||
|
|||
// Create a bounds tracking painter to measure actual rendered bounds
|
|||
var boundsPainter = new BoundsTrackingPainter(glyphTypeface); |
|||
|
|||
// Traverse the paint graph
|
|||
PaintTraverser.Traverse(resolvedPaint, boundsPainter, Matrix.Identity); |
|||
|
|||
var finalBounds = boundsPainter.GetFinalBounds(); |
|||
|
|||
// Verify bounds are reasonable (not collapsed or inverted)
|
|||
Assert.True(finalBounds.Width > 0, "Final bounds should have positive width"); |
|||
Assert.True(finalBounds.Height > 0, "Final bounds should have positive height"); |
|||
|
|||
// The bounds should be roughly square for an emoji (allow some tolerance)
|
|||
var aspectRatio = finalBounds.Width / finalBounds.Height; |
|||
Assert.True(aspectRatio > 0.5 && aspectRatio < 2.0, |
|||
$"Bridge at night emoji aspect ratio should be roughly square, got {aspectRatio:F2}"); |
|||
|
|||
// Verify that transforms didn't cause bounds to become excessively large or small
|
|||
// Typical emoji glyph bounds in font units are in the range of 0-2048
|
|||
Assert.True(finalBounds.Width < 10000, "Bounds width should not be excessively large"); |
|||
Assert.True(finalBounds.Height < 10000, "Bounds height should not be excessively large"); |
|||
Assert.True(finalBounds.Width > 10, "Bounds width should not be too small"); |
|||
Assert.True(finalBounds.Height > 10, "Bounds height should not be too small"); |
|||
|
|||
System.Diagnostics.Debug.WriteLine($"Bridge at night emoji 🌉 (U+{bridgeCodepoint:X4}, GlyphID: {bridgeGlyphId}) final bounds: {finalBounds}"); |
|||
System.Diagnostics.Debug.WriteLine($" Width: {finalBounds.Width:F2}, Height: {finalBounds.Height:F2}"); |
|||
System.Diagnostics.Debug.WriteLine($" Aspect ratio: {aspectRatio:F2}"); |
|||
System.Diagnostics.Debug.WriteLine($" Total glyphs rendered: {boundsPainter.GlyphCount}"); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Analyze_Bridge_At_Night_Emoji_Paint_Graph_And_Verify_Transform_Accumulation() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Color Emoji"); |
|||
|
|||
if (!FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Assert.True(ColrTable.TryLoad(glyphTypeface, out var colrTable)); |
|||
Assert.True(CpalTable.TryLoad(glyphTypeface, out var cpalTable)); |
|||
Assert.True(colrTable.HasV1Data); |
|||
|
|||
// Parse cmap to find glyph ID for U+1F309 (🌉 Bridge at Night)
|
|||
const int bridgeCodepoint = 0x1F309; |
|||
var bridgeGlyphId = glyphTypeface.CharacterToGlyphMap[bridgeCodepoint]; |
|||
bool foundGlyph = bridgeGlyphId != 0; |
|||
|
|||
if (!foundGlyph) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Verify this glyph has a COLR v1 record
|
|||
if (!colrTable.TryGetBaseGlyphV1Record(bridgeGlyphId, out var baseGlyphRecord)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Get the COLR data for parsing
|
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(ColrTable.Tag, out var colrData)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Create context for paint parsing
|
|||
var context = new ColrContext( |
|||
glyphTypeface, |
|||
colrTable, |
|||
cpalTable, |
|||
0); |
|||
|
|||
var decycler = new PaintDecycler(); |
|||
|
|||
// Parse the root paint
|
|||
var rootPaintOffset = colrTable.GetAbsolutePaintOffset(baseGlyphRecord.PaintOffset); |
|||
|
|||
if (!PaintParser.TryParse(colrData.Span, rootPaintOffset, in context, in decycler, out var rootPaint)) |
|||
{ |
|||
Assert.Fail("Failed to parse root paint for bridge at night emoji"); |
|||
return; |
|||
} |
|||
|
|||
// Resolve the paint (apply deltas, normalize)
|
|||
var resolvedPaint = PaintResolver.ResolvePaint(rootPaint, in context); |
|||
|
|||
// Create a tracking painter to analyze the paint graph
|
|||
var trackingPainter = new TransformTrackingPainter(); |
|||
|
|||
// Traverse the paint graph
|
|||
PaintTraverser.Traverse(resolvedPaint, trackingPainter, Matrix.Identity); |
|||
|
|||
// Analyze the results
|
|||
Assert.NotEmpty(trackingPainter.TransformStack); |
|||
|
|||
// Output diagnostic information
|
|||
System.Diagnostics.Debug.WriteLine($"Bridge at night emoji 🌉 (U+{bridgeCodepoint:X4}, GlyphID: {bridgeGlyphId}) paint graph analysis:"); |
|||
System.Diagnostics.Debug.WriteLine($" Total transforms encountered: {trackingPainter.AllTransforms.Count}"); |
|||
System.Diagnostics.Debug.WriteLine($" Maximum transform stack depth: {trackingPainter.MaxStackDepth}"); |
|||
System.Diagnostics.Debug.WriteLine($" Total glyphs encountered: {trackingPainter.GlyphCount}"); |
|||
System.Diagnostics.Debug.WriteLine($" Total fills encountered: {trackingPainter.FillCount}"); |
|||
System.Diagnostics.Debug.WriteLine($" Total clips encountered: {trackingPainter.ClipCount}"); |
|||
System.Diagnostics.Debug.WriteLine($" Total layers encountered: {trackingPainter.LayerCount}"); |
|||
|
|||
if (trackingPainter.AllTransforms.Count > 0) |
|||
{ |
|||
System.Diagnostics.Debug.WriteLine("\n Transform stack at each level:"); |
|||
for (int i = 0; i < trackingPainter.TransformStack.Count && i < 10; i++) |
|||
{ |
|||
var m = trackingPainter.TransformStack[i]; |
|||
System.Diagnostics.Debug.WriteLine( |
|||
$" Level {i}: [{m.M11:F4}, {m.M12:F4}, {m.M21:F4}, {m.M22:F4}, {m.M31:F4}, {m.M32:F4}]"); |
|||
} |
|||
|
|||
// Verify transforms were properly accumulated if any exist
|
|||
for (int i = 1; i < trackingPainter.TransformStack.Count; i++) |
|||
{ |
|||
var current = trackingPainter.TransformStack[i]; |
|||
var previous = trackingPainter.TransformStack[i - 1]; |
|||
|
|||
// Current transform should be different from previous (transforms accumulated)
|
|||
// Unless the transform was identity
|
|||
var isIdentity = current.M11 == 1 && current.M12 == 0 && |
|||
current.M21 == 0 && current.M22 == 1 && |
|||
current.M31 == 0 && current.M32 == 0; |
|||
|
|||
if (!isIdentity && i > 0) |
|||
{ |
|||
// Verify that transform changed
|
|||
bool transformChanged = |
|||
Math.Abs(current.M11 - previous.M11) > 0.0001 || |
|||
Math.Abs(current.M12 - previous.M12) > 0.0001 || |
|||
Math.Abs(current.M21 - previous.M21) > 0.0001 || |
|||
Math.Abs(current.M22 - previous.M22) > 0.0001 || |
|||
Math.Abs(current.M31 - previous.M31) > 0.0001 || |
|||
Math.Abs(current.M32 - previous.M32) > 0.0001; |
|||
|
|||
Assert.True(transformChanged, |
|||
$"Transform at level {i} should differ from level {i - 1} when accumulating non-identity transforms"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Debug_Bridge_At_Night_Emoji_Paint_Operations_In_Detail() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Color Emoji"); |
|||
|
|||
if (!FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Assert.True(ColrTable.TryLoad(glyphTypeface, out var colrTable)); |
|||
Assert.True(CpalTable.TryLoad(glyphTypeface, out var cpalTable)); |
|||
Assert.True(colrTable.HasV1Data); |
|||
|
|||
// Test with bridge at night emoji 🌉 (U+1F309)
|
|||
const int bridgeCodepoint = 0x1F309; |
|||
var bridgeGlyphId = glyphTypeface.CharacterToGlyphMap[bridgeCodepoint]; |
|||
|
|||
if (bridgeGlyphId == 0 || !colrTable.TryGetBaseGlyphV1Record(bridgeGlyphId, out var baseGlyphRecord)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(ColrTable.Tag, out var colrData)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var context = new ColrContext(glyphTypeface, colrTable, cpalTable, 0); |
|||
var decycler = new PaintDecycler(); |
|||
|
|||
var rootPaintOffset = colrTable.GetAbsolutePaintOffset(baseGlyphRecord.PaintOffset); |
|||
|
|||
if (!PaintParser.TryParse(colrData.Span, rootPaintOffset, in context, in decycler, out var rootPaint)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Resolve the paint
|
|||
var resolvedPaint = PaintResolver.ResolvePaint(rootPaint, in context); |
|||
|
|||
// Create a detailed diagnostic painter
|
|||
var diagnosticPainter = new DetailedDiagnosticPainter(glyphTypeface); |
|||
|
|||
// Traverse the paint graph
|
|||
PaintTraverser.Traverse(resolvedPaint, diagnosticPainter, Matrix.Identity); |
|||
|
|||
// Output all the collected diagnostic information
|
|||
System.Diagnostics.Debug.WriteLine($"\n=== Bridge at Night Emoji 🌉 (U+{bridgeCodepoint:X4}, GlyphID: {bridgeGlyphId}) Detailed Paint Operations ===\n"); |
|||
|
|||
foreach (var op in diagnosticPainter.Operations) |
|||
{ |
|||
System.Diagnostics.Debug.WriteLine(op); |
|||
} |
|||
|
|||
System.Diagnostics.Debug.WriteLine($"\n=== Summary ==="); |
|||
System.Diagnostics.Debug.WriteLine($"Total operations: {diagnosticPainter.Operations.Count}"); |
|||
System.Diagnostics.Debug.WriteLine($"Glyphs rendered: {diagnosticPainter.Operations.Count(o => o.Contains("Glyph"))}"); |
|||
System.Diagnostics.Debug.WriteLine($"Transforms: {diagnosticPainter.Operations.Count(o => o.Contains("PushTransform"))}"); |
|||
System.Diagnostics.Debug.WriteLine($"Fills: {diagnosticPainter.Operations.Count(o => o.Contains("Fill"))}"); |
|||
System.Diagnostics.Debug.WriteLine($"Clips: {diagnosticPainter.Operations.Count(o => o.Contains("Clip"))}"); |
|||
System.Diagnostics.Debug.WriteLine($"Layers: {diagnosticPainter.Operations.Count(o => o.Contains("Layer"))}"); |
|||
|
|||
// Verify we got some operations
|
|||
Assert.NotEmpty(diagnosticPainter.Operations); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Detailed diagnostic painter that logs every operation with full details
|
|||
/// </summary>
|
|||
private class DetailedDiagnosticPainter : IColorPainter |
|||
{ |
|||
private readonly GlyphTypeface _glyphTypeface; |
|||
private readonly Stack<Matrix> _transforms = new Stack<Matrix>(); |
|||
private int _operationIndex = 0; |
|||
public List<string> Operations { get; } = new List<string>(); |
|||
|
|||
public DetailedDiagnosticPainter(GlyphTypeface glyphTypeface) |
|||
{ |
|||
_glyphTypeface = glyphTypeface; |
|||
_transforms.Push(Matrix.Identity); |
|||
} |
|||
|
|||
private string FormatMatrix(Matrix m) |
|||
{ |
|||
return $"[{m.M11:F4}, {m.M12:F4}, {m.M21:F4}, {m.M22:F4}, {m.M31:F4}, {m.M32:F4}]"; |
|||
} |
|||
|
|||
private string FormatPoint(Point p) |
|||
{ |
|||
return $"({p.X:F2}, {p.Y:F2})"; |
|||
} |
|||
|
|||
private string FormatRect(Rect r) |
|||
{ |
|||
return $"({r.X:F2}, {r.Y:F2}, {r.Width:F2}, {r.Height:F2})"; |
|||
} |
|||
|
|||
public void PushTransform(Matrix transform) |
|||
{ |
|||
var current = _transforms.Peek(); |
|||
var accumulated = current * transform; |
|||
_transforms.Push(accumulated); |
|||
|
|||
Operations.Add($"[{_operationIndex++}] PushTransform: {FormatMatrix(transform)}"); |
|||
Operations.Add($" Accumulated: {FormatMatrix(accumulated)}"); |
|||
} |
|||
|
|||
public void PopTransform() |
|||
{ |
|||
if (_transforms.Count > 1) |
|||
{ |
|||
_transforms.Pop(); |
|||
Operations.Add($"[{_operationIndex++}] PopTransform"); |
|||
} |
|||
} |
|||
|
|||
public void PushLayer(CompositeMode mode) |
|||
{ |
|||
Operations.Add($"[{_operationIndex++}] PushLayer: Mode={mode}"); |
|||
} |
|||
|
|||
public void PopLayer() |
|||
{ |
|||
Operations.Add($"[{_operationIndex++}] PopLayer"); |
|||
} |
|||
|
|||
public void PushClip(Rect clipBox) |
|||
{ |
|||
Operations.Add($"[{_operationIndex++}] PushClip: {FormatRect(clipBox)}"); |
|||
} |
|||
|
|||
public void PopClip() |
|||
{ |
|||
Operations.Add($"[{_operationIndex++}] PopClip"); |
|||
} |
|||
|
|||
public void FillSolid(Color color) |
|||
{ |
|||
Operations.Add($"[{_operationIndex++}] FillSolid: Color=#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"); |
|||
} |
|||
|
|||
public void FillLinearGradient(Point p0, Point p1, GradientStop[] stops, GradientSpreadMethod extend) |
|||
{ |
|||
Operations.Add($"[{_operationIndex++}] FillLinearGradient: P0={FormatPoint(p0)}, P1={FormatPoint(p1)}, Stops={stops.Length}, Extend={extend}"); |
|||
} |
|||
|
|||
public void FillRadialGradient(Point c0, double r0, Point c1, double r1, GradientStop[] stops, GradientSpreadMethod extend) |
|||
{ |
|||
Operations.Add($"[{_operationIndex++}] FillRadialGradient: C0={FormatPoint(c0)}, R0={r0:F2}, C1={FormatPoint(c1)}, R1={r1:F2}, Stops={stops.Length}, Extend={extend}"); |
|||
} |
|||
|
|||
public void FillConicGradient(Point center, double startAngle, double endAngle, GradientStop[] stops, GradientSpreadMethod extend) |
|||
{ |
|||
Operations.Add($"[{_operationIndex++}] FillConicGradient: Center={FormatPoint(center)}, Start={startAngle:F2}°, End={endAngle:F2}°, Stops={stops.Length}, Extend={extend}"); |
|||
} |
|||
|
|||
public void Glyph(ushort glyphId) |
|||
{ |
|||
var geometry = _glyphTypeface.GetGlyphOutline(glyphId, Matrix.Identity); |
|||
var bounds = geometry?.Bounds ?? default; |
|||
var transform = _transforms.Peek(); |
|||
var transformedBounds = bounds.TransformToAABB(transform); |
|||
|
|||
Operations.Add($"[{_operationIndex++}] Glyph: GlyphID={glyphId}"); |
|||
Operations.Add($" Original Bounds: {FormatRect(bounds)}"); |
|||
Operations.Add($" Current Transform: {FormatMatrix(transform)}"); |
|||
Operations.Add($" Transformed Bounds: {FormatRect(transformedBounds)}"); |
|||
} |
|||
|
|||
public void ColrGlyph(ushort glyphId) |
|||
{ |
|||
Operations.Add($"[{_operationIndex++}] ColrGlyph: GlyphID={glyphId}"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Custom painter that tracks transform accumulation during paint graph traversal
|
|||
/// </summary>
|
|||
private class TransformTrackingPainter : IColorPainter |
|||
{ |
|||
public List<Matrix> TransformStack { get; } = new List<Matrix>(); |
|||
public List<Matrix> AllTransforms { get; } = new List<Matrix>(); |
|||
public int MaxStackDepth { get; private set; } |
|||
public int GlyphCount { get; private set; } |
|||
public int FillCount { get; private set; } |
|||
public int ClipCount { get; private set; } |
|||
public int LayerCount { get; private set; } |
|||
|
|||
private readonly Stack<Matrix> _activeTransforms = new Stack<Matrix>(); |
|||
|
|||
public TransformTrackingPainter() |
|||
{ |
|||
_activeTransforms.Push(Matrix.Identity); |
|||
TransformStack.Add(Matrix.Identity); |
|||
} |
|||
|
|||
public void PushTransform(Matrix transform) |
|||
{ |
|||
var current = _activeTransforms.Peek(); |
|||
var accumulated = current * transform; |
|||
_activeTransforms.Push(accumulated); |
|||
TransformStack.Add(accumulated); |
|||
AllTransforms.Add(transform); |
|||
|
|||
MaxStackDepth = Math.Max(MaxStackDepth, _activeTransforms.Count); |
|||
} |
|||
|
|||
public void PopTransform() |
|||
{ |
|||
if (_activeTransforms.Count > 1) |
|||
{ |
|||
_activeTransforms.Pop(); |
|||
} |
|||
} |
|||
|
|||
public void PushLayer(CompositeMode mode) |
|||
{ |
|||
LayerCount++; |
|||
} |
|||
|
|||
public void PopLayer() |
|||
{ |
|||
} |
|||
|
|||
public void PushClip(Rect clipBox) |
|||
{ |
|||
ClipCount++; |
|||
} |
|||
|
|||
public void PopClip() |
|||
{ |
|||
} |
|||
|
|||
public void FillSolid(Color color) |
|||
{ |
|||
FillCount++; |
|||
} |
|||
|
|||
public void FillLinearGradient(Point p0, Point p1, GradientStop[] stops, GradientSpreadMethod extend) |
|||
{ |
|||
FillCount++; |
|||
} |
|||
|
|||
public void FillRadialGradient(Point c0, double r0, Point c1, double r1, GradientStop[] stops, GradientSpreadMethod extend) |
|||
{ |
|||
FillCount++; |
|||
} |
|||
|
|||
public void FillConicGradient(Point center, double startAngle, double endAngle, GradientStop[] stops, GradientSpreadMethod extend) |
|||
{ |
|||
FillCount++; |
|||
} |
|||
|
|||
public void Glyph(ushort glyphId) |
|||
{ |
|||
GlyphCount++; |
|||
} |
|||
|
|||
public void ColrGlyph(ushort glyphId) |
|||
{ |
|||
GlyphCount++; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Custom painter that tracks the final bounds of all rendered content
|
|||
/// </summary>
|
|||
private class BoundsTrackingPainter : IColorPainter |
|||
{ |
|||
private readonly GlyphTypeface _glyphTypeface; |
|||
private readonly Stack<Matrix> _transforms = new Stack<Matrix>(); |
|||
private Rect _bounds = default; |
|||
|
|||
public int GlyphCount { get; private set; } |
|||
|
|||
public BoundsTrackingPainter(GlyphTypeface glyphTypeface) |
|||
{ |
|||
_glyphTypeface = glyphTypeface; |
|||
_transforms.Push(Matrix.Identity); |
|||
} |
|||
|
|||
public Rect GetFinalBounds() => _bounds; |
|||
|
|||
public void PushTransform(Matrix transform) |
|||
{ |
|||
var current = _transforms.Peek(); |
|||
_transforms.Push(current * transform); |
|||
} |
|||
|
|||
public void PopTransform() |
|||
{ |
|||
if (_transforms.Count > 1) |
|||
{ |
|||
_transforms.Pop(); |
|||
} |
|||
} |
|||
|
|||
public void PushLayer(CompositeMode mode) { } |
|||
public void PopLayer() { } |
|||
public void PushClip(Rect clipBox) { } |
|||
public void PopClip() { } |
|||
public void FillSolid(Color color) { } |
|||
public void FillLinearGradient(Point p0, Point p1, GradientStop[] stops, GradientSpreadMethod extend) { } |
|||
public void FillRadialGradient(Point c0, double r0, Point c1, double r1, GradientStop[] stops, GradientSpreadMethod extend) { } |
|||
public void FillConicGradient(Point center, double startAngle, double endAngle, GradientStop[] stops, GradientSpreadMethod extend) { } |
|||
|
|||
public void Glyph(ushort glyphId) |
|||
{ |
|||
GlyphCount++; |
|||
|
|||
// Get glyph outline bounds
|
|||
var geometry = _glyphTypeface.GetGlyphOutline(glyphId, Matrix.Identity); |
|||
if (geometry != null) |
|||
{ |
|||
var glyphBounds = geometry.Bounds; |
|||
|
|||
// Transform the bounds by current accumulated transform
|
|||
var transform = _transforms.Peek(); |
|||
var transformedBounds = glyphBounds.TransformToAABB(transform); |
|||
|
|||
// Union with accumulated bounds
|
|||
_bounds = _bounds.IsEmpty() ? transformedBounds : _bounds.Union(transformedBounds); |
|||
} |
|||
} |
|||
|
|||
public void ColrGlyph(ushort glyphId) |
|||
{ |
|||
GlyphCount++; |
|||
} |
|||
} |
|||
|
|||
private static IDisposable Start() |
|||
{ |
|||
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); |
|||
|
|||
return disposable; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue