Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/Avalonia.Base/Media/FontVariationSettings.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
14 changes: 14 additions & 0 deletions src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace Avalonia.Media.Fonts.Tables.Cmap
Expand Down Expand Up @@ -143,5 +144,18 @@ public CodepointRangeEnumerator GetMappedRanges()
{
return new CodepointRangeEnumerator(Format, _format4, _format12Or13);
}

/// <summary>
/// Exposes the character-to-glyph map as an <see cref="IReadOnlyDictionary{TKey, TValue}"/>.
/// </summary>
/// <remarks>This method returns a lightweight wrapper that provides dictionary-like access to the glyph mappings.
/// The wrapper does not allocate memory for storing all mappings; instead, it dynamically computes keys and values
/// from the underlying cmap table using the mapped code point ranges and the GetGlyph method.</remarks>
/// <returns>An <see cref="IReadOnlyDictionary{TKey, TValue}"/> view of this character-to-glyph map.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IReadOnlyDictionary<int, ushort> AsReadOnlyDictionary()
{
return new CharacterToGlyphMapDictionary(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
191 changes: 191 additions & 0 deletions src/Avalonia.Base/Media/Fonts/Tables/Colr/ColorGlyphV1Painter.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
Loading