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;
}
}
}
146 changes: 146 additions & 0 deletions src/Avalonia.Base/Media/Fonts/Tables/Decycler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;

namespace Avalonia.Media.Fonts.Tables
{
/// <summary>
/// Errors that can occur during graph traversal with cycle detection.
/// </summary>
internal enum DecyclerError
{
/// <summary>
/// A cycle was detected in the 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 node and ensures proper cleanup.
/// </summary>
/// <typeparam name="T">The type of the node 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 node ID from the visited set.
/// </summary>
public void Dispose()
{
if (!_exited)
{
_decycler.Exit(_id);
_exited = true;
}
}
}

/// <summary>
/// Tracks visited nodes to detect cycles in a graph (composite glyphs, paint graphs, etc.).
/// Uses a depth limit to prevent stack overflow during recursive traversal.
/// </summary>
/// <typeparam name="T">The type of the node identifier.</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 node with the given ID.
/// Returns a guard that will automatically exit when disposed.
/// </summary>
/// <param name="id">The node 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,
$"Graph depth limit of {_maxDepth} exceeded");
}

if (_visited.Contains(id))
{
throw new DecyclerException(
DecyclerError.CycleDetected,
"Cycle detected in graph");
}

_visited.Add(id);
_currentDepth++;

return new CycleGuard<T>(this, id);
}

/// <summary>
/// Exits a node, removing it from the visited set.
/// Called automatically by CycleGuard.Dispose().
/// </summary>
/// <param name="id">The node 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;
}
}
}
22 changes: 22 additions & 0 deletions src/Avalonia.Base/Media/Fonts/Tables/Glyf/CompositeFlags.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading