ImageSharp/SixLabors.Fonts/Tables/General/Colr/ColrTable.cs
2026-08-03 22:31:27 +02:00

1612 lines
67 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.Fonts.Rendering;
using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
namespace SixLabors.Fonts.Tables.General.Colr {
/// <summary>
/// Represents the OpenType COLR table, which defines color glyph data for both v0 (layer-based)
/// and v1 (paint-based) color fonts.
/// <see href="https://learn.microsoft.com/en-us/typography/opentype/spec/colr"/>
/// </summary>
internal class ColrTable : Table
{
/// <summary>
/// The table tag name "COLR".
/// </summary>
internal const string TableName = "COLR";
/// <summary>
/// The COLR v0 base glyph records mapping glyph IDs to layer ranges.
/// </summary>
private readonly BaseGlyphRecord[] glyphRecords;
/// <summary>
/// The COLR v0 layer records defining color layers.
/// </summary>
private readonly LayerRecord[] layers;
/// <summary>
/// The COLR v1 BaseGlyphList, or <see langword="null"/> if not present.
/// </summary>
private readonly BaseGlyphList? baseGlyphList;
/// <summary>
/// The COLR v1 LayerList, or <see langword="null"/> if not present.
/// </summary>
private readonly LayerList? layerList;
/// <summary>
/// The COLR v1 ClipList, or <see langword="null"/> if not present.
/// </summary>
private readonly ClipList? clipList;
/// <summary>
/// The ItemVariationStore for variable font data, or <see langword="null"/> if not present.
/// </summary>
private readonly ItemVariationStore? itemVariationStore;
/// <summary>
/// The DeltaSetIndexMap array for mapping variation indices, or <see langword="null"/> if not present.
/// </summary>
private readonly DeltaSetIndexMap[]? deltaSetIndexMap;
/// <summary>
/// Cache of resolved paint objects keyed by their COLR-relative offset.
/// </summary>
private readonly Dictionary<uint, Paint>? paintCache;
/// <summary>
/// Initializes a new instance of the <see cref="ColrTable"/> class for COLR v0 data only.
/// </summary>
/// <param name="glyphRecords">The base glyph records.</param>
/// <param name="layers">The layer records.</param>
public ColrTable(
BaseGlyphRecord[] glyphRecords,
LayerRecord[] layers)
: this(glyphRecords, layers, null, null, null, null, null, null, 0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ColrTable"/> class with both v0 and optional v1 data.
/// </summary>
/// <param name="glyphRecords">The COLR v0 base glyph records.</param>
/// <param name="layers">The COLR v0 layer records.</param>
/// <param name="baseGlyphList">The COLR v1 base glyph list, or <see langword="null"/>.</param>
/// <param name="layerList">The COLR v1 layer list, or <see langword="null"/>.</param>
/// <param name="clipList">The COLR v1 clip list, or <see langword="null"/>.</param>
/// <param name="itemVariationStore">The ItemVariationStore for variable font data, or <see langword="null"/>.</param>
/// <param name="deltaSetIndexMap">The DeltaSetIndexMap array, or <see langword="null"/>.</param>
/// <param name="paintCache">The pre-loaded paint cache, or <see langword="null"/>.</param>
/// <param name="version">The COLR table version.</param>
public ColrTable(
BaseGlyphRecord[] glyphRecords,
LayerRecord[] layers,
BaseGlyphList? baseGlyphList,
LayerList? layerList,
ClipList? clipList,
ItemVariationStore? itemVariationStore,
DeltaSetIndexMap[]? deltaSetIndexMap,
Dictionary<uint, Paint>? paintCache = null,
int version = 1)
{
this.glyphRecords = glyphRecords;
this.layers = layers;
this.baseGlyphList = baseGlyphList;
this.layerList = layerList;
this.clipList = clipList;
this.itemVariationStore = itemVariationStore;
this.deltaSetIndexMap = deltaSetIndexMap;
this.paintCache = paintCache;
this.Version = version;
}
/// <summary>
/// Gets the COLR table version (0 or 1).
/// </summary>
public int Version { get; }
/// <summary>
/// Resolves a variation delta for a given variable index using the COLR table's
/// own ItemVariationStore and optional DeltaSetIndexMap.
/// </summary>
/// <param name="processor">The glyph variation processor (null for non-variable fonts).</param>
/// <param name="varIdx">The variable index (VarIndexBase + field offset).</param>
/// <returns>The delta value, or 0 if no variation data is available.</returns>
internal float ResolveDelta(GlyphVariationProcessor? processor, uint varIdx)
{
if (processor is null || this.itemVariationStore is null)
{
return 0;
}
int outer;
int inner;
if (this.deltaSetIndexMap is not null && varIdx < (uint)this.deltaSetIndexMap.Length)
{
DeltaSetIndexMap mapping = this.deltaSetIndexMap[varIdx];
outer = mapping.OuterIndex;
inner = mapping.InnerIndex;
}
else
{
// Implicit mapping: upper 16 bits = outer, lower 16 bits = inner.
outer = (int)(varIdx >> 16);
inner = (int)(varIdx & 0xFFFF);
}
return processor.Delta(this.itemVariationStore, outer, inner);
}
/// <summary>
/// Loads the COLR table from the specified font reader.
/// </summary>
/// <param name="fontReader">The font reader.</param>
/// <returns>The loaded <see cref="ColrTable"/>, or <see langword="null"/> if the table is not present.</returns>
public static ColrTable? Load(FontReader fontReader)
{
if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader))
{
return null;
}
using (binaryReader)
{
return Load(binaryReader);
}
}
/// <summary>
/// Gets the COLR v0 layer records for the specified glyph.
/// </summary>
/// <param name="glyph">The glyph ID.</param>
/// <returns>A span of layer records for the glyph, or an empty span if not found.</returns>
internal Span<LayerRecord> GetLayers(ushort glyph)
{
foreach (BaseGlyphRecord g in this.glyphRecords)
{
if (g.GlyphId == glyph)
{
return this.layers.AsSpan().Slice(g.FirstLayerIndex, g.LayerCount);
}
}
return [];
}
/// <summary>
/// Determines whether the specified glyph has an associated COLR v0 color glyph definition.
/// </summary>
/// <param name="glyphId">The identifier of the glyph to check for a COLR v0 color glyph definition.</param>
/// <returns>
/// <see langword="true"/> if the specified glyph has a COLR v0 color glyph definition; otherwise, <see langword="false"/>.
/// </returns>
public bool ContainsColorV0Glyph(ushort glyphId)
{
for (int i = 0; i < this.glyphRecords.Length; i++)
{
BaseGlyphRecord g = this.glyphRecords[i];
if (g.GlyphId == glyphId)
{
return true;
}
}
return false;
}
/// <summary>
/// Determines whether the specified glyph has an associated COLR v1 color glyph definition.
/// </summary>
/// <param name="glyphId">The identifier of the glyph to check for a COLR v1 color glyph definition.</param>
/// <returns>
/// <see langword="true"/> if the specified glyph has a COLR v1 color glyph definition; otherwise, <see langword="false"/>.
/// </returns>
public bool ContainsColorV1Glyph(ushort glyphId)
{
if (this.baseGlyphList is null || this.layerList is null || this.paintCache is null)
{
return false; // No COLR v1 data
}
return this.TryGetRootPaintOffset(glyphId, out uint _);
}
/// <summary>
/// Attempts to retrieve the set of color layer records associated with the specified glyph.
/// </summary>
/// <param name="glyph">The glyph ID for which to retrieve color layer records.</param>
/// <param name="records">
/// When this method returns, contains a span of <see cref="LayerRecord"/> structures
/// representing the color layers for the specified glyph, if found; otherwise, an empty span.
/// </param>
/// <returns>
/// <see langword="true"/> if color layer records are found for the specified glyph; otherwise,
/// <see langword="false"/>.
/// </returns>
internal bool TryGetColrV0Layers(ushort glyph, out Span<LayerRecord> records)
{
for (int i = 0; i < this.glyphRecords.Length; i++)
{
BaseGlyphRecord g = this.glyphRecords[i];
if (g.GlyphId == glyph)
{
records = this.layers.AsSpan().Slice(g.FirstLayerIndex, g.LayerCount);
return true;
}
}
records = [];
return false;
}
/// <summary>
/// Attempts to resolve and retrieve the list of color glyph layers for the specified glyph ID.
/// </summary>
/// <param name="glyphId">The identifier of the glyph for which to resolve color layers.</param>
/// <param name="processor">The glyph variation processor, or null for non-variable fonts.</param>
/// <param name="layers">
/// When this method returns, contains a list of resolved glyph layers if the operation succeeds; otherwise,
/// <see langword="null"/>. This parameter is passed uninitialized.</param>
/// <returns>
/// <see langword="true"/> if the color glyph layers were successfully resolved; otherwise, <see langword="false"/>.
/// </returns>
internal bool TryGetColrV1Layers(
ushort glyphId,
GlyphVariationProcessor? processor,
[NotNullWhen(true)] out List<ResolvedGlyphLayer>? layers)
{
layers = null;
if (this.baseGlyphList is null || this.layerList is null || this.paintCache is null)
{
return false; // No COLR v1 data
}
// 1) Resolve root paint for the requested base glyph
if (!this.TryGetRootPaintOffset(glyphId, out uint rootOff) || rootOff == 0)
{
return false;
}
if (!this.paintCache.TryGetValue(rootOff, out Paint? root) || root is null)
{
return false;
}
// 2) Flatten paint graph to layers. Start with no current glyph id.
List<ResolvedGlyphLayer> acc = [];
this.FlattenPaintToLayers(root, null, Matrix3x2.Identity, Matrix3x2.Identity, false, CompositeMode.SrcOver, processor, acc);
// 3) If nothing emitted, the graph did not bind any geometry (no PaintGlyph/ColrGlyph reached).
if (acc.Count == 0)
{
layers = null;
return false;
}
layers = acc;
return true;
}
/// <summary>
/// Recursively flattens a COLR v1 paint subtree into <see cref="ResolvedGlyphLayer"/>s.
/// A layer is emitted only when a leaf paint is reached under an active glyph-binding node:
/// <list type="bullet">
/// <item><description><b>PaintGlyph</b> sets the current glyph id to its <c>GlyphId</c> and recurses into its child paint.</description></item>
/// <item><description><b>PaintColrGlyph</b> resolves that glyph's root paint, sets the current glyph id, and recurses.</description></item>
/// <item><description>Wrapper nodes (transform/translate/scale/rotate/skew, var forms) forward the current glyph id unchanged.</description></item>
/// <item><description><b>PaintComposite</b> flattens both branches independently, forwarding the current glyph id to each.</description></item>
/// <item><description>Leaf paints (solid/linear/radial/sweep, var forms) emit a layer only if <paramref name="currentGlyphId"/> has a value.</description></item>
/// </list>
/// </summary>
/// <param name="node">The paint node to flatten.</param>
/// <param name="currentGlyphId">
/// The glyph id whose outline will receive the paint. Set by <c>PaintGlyph</c>/<c>PaintColrGlyph</c>.
/// </param>
/// <param name="glyphTransform">The accumulated transform to apply to the glyph's geometry.</param>
/// <param name="paintTransform">The accumulated transform to apply to the paint.</param>
/// <param name="transformPaint">Whether wrapper transforms should be applied to the paint (true) or to the glyph geometry (false).</param>
/// <param name="compositeMode">Accumulated composite mode.</param>
/// <param name="processor">The glyph variation processor, or null for non-variable fonts.</param>
/// <param name="outLayers">Accumulator for resolved layers.</param>
private void FlattenPaintToLayers(
Paint node,
ushort? currentGlyphId,
Matrix3x2 glyphTransform,
Matrix3x2 paintTransform,
bool transformPaint,
CompositeMode compositeMode,
GlyphVariationProcessor? processor,
List<ResolvedGlyphLayer> outLayers)
{
switch (node)
{
// ---------------------------
// Containers and indirections
// ---------------------------
case PaintColrLayers pcl:
{
// Iterates layer indices and flattens each addressed paint subtree.
// No glyph id is implied here; child subtrees must bind via PaintGlyph/ColrGlyph.
int first = (int)pcl.FirstLayerIndex;
int count = pcl.NumLayers;
ReadOnlySpan<uint> offs = this.GetLayerPaintOffsets(first, count);
for (int i = 0; i < offs.Length; i++)
{
uint off = offs[i];
if (off == 0)
{
continue;
}
if (this.paintCache!.TryGetValue(off, out Paint? child) && child is not null)
{
this.FlattenPaintToLayers(child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
}
}
return;
}
case PaintColrGlyph pcg:
{
// Resolve the referenced glyph's root paint and recurse through its own bindings.
if (this.TryGetRootPaintOffset(pcg.GlyphId, out uint off) && off != 0
&& this.paintCache!.TryGetValue(off, out Paint? colrRoot) && colrRoot is not null)
{
this.FlattenPaintToLayers(colrRoot, null, glyphTransform, Matrix3x2.Identity, false, compositeMode, processor, outLayers);
}
return;
}
case PaintGlyph pg:
{
// Bind geometry to the specified glyph id and recurse into its child paint.
this.FlattenPaintToLayers(pg.Child, pg.GlyphId, glyphTransform, Matrix3x2.Identity, true, compositeMode, processor, outLayers);
return;
}
// ---------------------------
// Wrappers: forward glyph id
// ---------------------------
case PaintTransform pt:
{
Affine2x3 a = pt.Transform;
Matrix3x2 next = new(a.Xx, a.Yx, a.Xy, a.Yy, a.Dx, a.Dy);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(pt.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintVarTransform pvt:
{
VarAffine2x3 a = pvt.Transform;
uint vib = a.VarIndexBase;
float xx = a.Xx + this.ResolveDelta(processor, vib + 0u);
float yx = a.Yx + this.ResolveDelta(processor, vib + 1u);
float xy = a.Xy + this.ResolveDelta(processor, vib + 2u);
float yy = a.Yy + this.ResolveDelta(processor, vib + 3u);
float dx = a.Dx + this.ResolveDelta(processor, vib + 4u);
float dy = a.Dy + this.ResolveDelta(processor, vib + 5u);
Matrix3x2 next = new(xx, yx, xy, yy, dx, dy);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(pvt.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintTranslate t:
{
Matrix3x2 next = Matrix3x2.CreateTranslation(t.Dx, t.Dy);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(t.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintVarTranslate vt:
{
float dx = vt.Dx + this.ResolveDelta(processor, vt.VarIndexBase + 0u);
float dy = vt.Dy + this.ResolveDelta(processor, vt.VarIndexBase + 1u);
Matrix3x2 next = Matrix3x2.CreateTranslation(dx, dy);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(vt.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintScale s:
{
Matrix3x2 next = BuildScale(s.ScaleX, s.ScaleY, s.AroundCenter, s.CenterX, s.CenterY);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(s.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintVarScale vs:
{
uint vib = vs.VarIndexBase;
float sx = vs.ScaleX + this.ResolveDelta(processor, vib + 0u);
float sy = vs.Uniform ? sx : vs.ScaleY + this.ResolveDelta(processor, vib + 1u);
int centerOffset = vs.Uniform ? 1 : 2;
float cx = vs.AroundCenter ? vs.CenterX + this.ResolveDelta(processor, vib + (uint)centerOffset) : 0;
float cy = vs.AroundCenter ? vs.CenterY + this.ResolveDelta(processor, vib + (uint)centerOffset + 1u) : 0;
Matrix3x2 next = BuildScale(sx, sy, vs.AroundCenter, cx, cy);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(vs.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintRotate r:
{
Matrix3x2 next = BuildRotate(r.Angle, r.AroundCenter, r.CenterX, r.CenterY);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(r.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintVarRotate vr:
{
uint vib = vr.VarIndexBase;
float angle = vr.Angle + this.ResolveDelta(processor, vib + 0u);
float cx = vr.AroundCenter ? vr.CenterX + this.ResolveDelta(processor, vib + 1u) : 0;
float cy = vr.AroundCenter ? vr.CenterY + this.ResolveDelta(processor, vib + 2u) : 0;
Matrix3x2 next = BuildRotate(angle, vr.AroundCenter, cx, cy);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(vr.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintSkew k:
{
Matrix3x2 next = BuildSkew(k.XSkew, k.YSkew, k.AroundCenter, k.CenterX, k.CenterY);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(k.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintVarSkew vk:
{
uint vib = vk.VarIndexBase;
float xSkew = vk.XSkew + this.ResolveDelta(processor, vib + 0u);
float ySkew = vk.YSkew + this.ResolveDelta(processor, vib + 1u);
float cx = vk.AroundCenter ? vk.CenterX + this.ResolveDelta(processor, vib + 2u) : 0;
float cy = vk.AroundCenter ? vk.CenterY + this.ResolveDelta(processor, vib + 3u) : 0;
Matrix3x2 next = BuildSkew(xSkew, ySkew, vk.AroundCenter, cx, cy);
if (transformPaint)
{
paintTransform *= next;
}
else
{
glyphTransform *= next;
}
this.FlattenPaintToLayers(vk.Child, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
case PaintComposite comp:
{
compositeMode = MapCompositeMode(comp.CompositeMode);
// Backdrop first, then Source. Both inherit the current glyph id.
this.FlattenPaintToLayers(comp.Backdrop, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
this.FlattenPaintToLayers(comp.Source, currentGlyphId, glyphTransform, paintTransform, transformPaint, compositeMode, processor, outLayers);
return;
}
// ---------------------------
// Leaves: emit only if bound
// ---------------------------
case PaintSolid:
case PaintVarSolid:
case PaintLinearGradient:
case PaintVarLinearGradient:
case PaintRadialGradient:
case PaintVarRadialGradient:
case PaintSweepGradient:
case PaintVarSweepGradient:
{
// Only emit if we have an active glyph id (i.e., we are inside a PaintGlyph/ColrGlyph branch).
if (currentGlyphId.HasValue)
{
_ = this.TryGetClipBox(currentGlyphId.Value, processor, out Bounds? clip);
outLayers.Add(new ResolvedGlyphLayer(currentGlyphId.Value, node, glyphTransform, paintTransform, compositeMode, clip));
}
return;
}
default:
{
// Unknown or unsupported node: do not emit and do not stop traversal.
return;
}
}
}
/// <summary>
/// Builds a scale matrix, optionally around a center.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Matrix3x2 BuildScale(float sx, float sy, bool aroundCenter, float cx, float cy)
{
if (!aroundCenter)
{
return Matrix3x2.CreateScale(sx, sy);
}
return Matrix3x2.CreateScale(sx, sy, new Vector2(cx, cy));
}
/// <summary>
/// Builds a rotation matrix, optionally around a center.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Matrix3x2 BuildRotate(float angleColrUnits, bool aroundCenter, float cx, float cy)
{
// COLR: 1.0 == 180° => radians = angle * π
float radians = angleColrUnits * MathF.PI;
if (!aroundCenter)
{
return Matrix3x2.CreateRotation(radians);
}
return Matrix3x2.CreateRotation(radians, new Vector2(cx, cy));
}
/// <summary>
/// Builds a skew matrix, optionally around a center.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Matrix3x2 BuildSkew(float xSkew, float ySkew, bool aroundCenter, float cx, float cy)
{
// COLR: 1.0 == 180° => radians = angle * π
float rx = xSkew * MathF.PI;
float ry = ySkew * MathF.PI;
if (!aroundCenter)
{
return Matrix3x2.CreateSkew(rx, ry);
}
return Matrix3x2.CreateSkew(rx, ry, new Vector2(cx, cy));
}
/// <summary>
/// Maps a COLR composite mode to the internal <see cref="CompositeMode"/>.
/// <para>
/// Returns <see cref="CompositeMode.SrcOver"/> when <paramref name="mode"/> is null
/// or when the value is not recognized.
/// </para>
/// </summary>
/// <param name="mode">The optional COLR composite mode.</param>
/// <returns>The mapped <see cref="CompositeMode"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static CompositeMode MapCompositeMode(ColrCompositeMode? mode)
=> mode switch
{
// PorterDuff
ColrCompositeMode.Clear => CompositeMode.Clear,
ColrCompositeMode.Src => CompositeMode.Src,
ColrCompositeMode.Dst => CompositeMode.Dest,
ColrCompositeMode.SrcOver => CompositeMode.SrcOver,
ColrCompositeMode.DstOver => CompositeMode.DestOver,
ColrCompositeMode.SrcIn => CompositeMode.SrcIn,
ColrCompositeMode.DstIn => CompositeMode.DestIn,
ColrCompositeMode.SrcOut => CompositeMode.SrcOut,
ColrCompositeMode.DstOut => CompositeMode.DestOut,
ColrCompositeMode.SrcAtop => CompositeMode.SrcAtop,
ColrCompositeMode.DstAtop => CompositeMode.DestAtop,
ColrCompositeMode.Xor => CompositeMode.Xor,
ColrCompositeMode.Plus => CompositeMode.Plus,
// Blend modes
ColrCompositeMode.Screen => CompositeMode.Screen,
ColrCompositeMode.Overlay => CompositeMode.Overlay,
ColrCompositeMode.Darken => CompositeMode.Darken,
ColrCompositeMode.Lighten => CompositeMode.Lighten,
ColrCompositeMode.ColorDodge => CompositeMode.ColorDodge,
ColrCompositeMode.ColorBurn => CompositeMode.ColorBurn,
ColrCompositeMode.HardLight => CompositeMode.HardLight,
ColrCompositeMode.SoftLight => CompositeMode.SoftLight,
ColrCompositeMode.Difference => CompositeMode.Difference,
ColrCompositeMode.Exclusion => CompositeMode.Exclusion,
ColrCompositeMode.Multiply => CompositeMode.Multiply,
ColrCompositeMode.Hue => CompositeMode.Hue,
ColrCompositeMode.Saturation => CompositeMode.Saturation,
ColrCompositeMode.Color => CompositeMode.Color,
ColrCompositeMode.Luminosity => CompositeMode.Luminosity,
_ => CompositeMode.SrcOver,
};
/// <summary>
/// Attempts to retrieve the paint table offset associated with the specified glyph ID.
/// </summary>
/// <param name="glyphId">The glyph ID for which to look up the paint table offset.</param>
/// <param name="paintOffset">
/// When this method returns, contains the paint table offset for the specified glyph ID, if found; otherwise, zero.
/// This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the paint table offset was found for the specified glyph ID; otherwise, <see langword="false"/>.
/// </returns>
private bool TryGetRootPaintOffset(ushort glyphId, out uint paintOffset)
{
if (this.baseGlyphList is null)
{
paintOffset = 0;
return false;
}
ReadOnlySpan<BaseGlyphPaintRecord> recs = this.baseGlyphList.Records;
int lo = 0, hi = recs.Length - 1;
while (lo <= hi)
{
int mid = (lo + hi) >> 1;
ushort gid = recs[mid].GlyphId;
if (glyphId == gid)
{
paintOffset = recs[mid].PaintOffset;
return true;
}
if (glyphId < gid)
{
hi = mid - 1;
}
else
{
lo = mid + 1;
}
}
paintOffset = 0;
return false;
}
/// <summary>
/// Gets a span of paint offsets from the layer list starting at the specified index.
/// </summary>
/// <param name="first">The index of the first paint offset.</param>
/// <param name="count">The number of paint offsets to retrieve.</param>
/// <returns>A read-only span of paint offsets, or an empty span if the layer list is null or the range is invalid.</returns>
private ReadOnlySpan<uint> GetLayerPaintOffsets(int first, int count)
{
if (this.layerList is null || count <= 0)
{
return [];
}
Span<uint> offsets = this.layerList.PaintOffsets.AsSpan();
if ((uint)first >= (uint)offsets.Length)
{
return [];
}
int len = Math.Min(count, offsets.Length - first);
return offsets.Slice(first, len);
}
/// <summary>
/// Attempts to retrieve the clip box bounds for the specified glyph ID.
/// </summary>
/// <param name="glyphId">The glyph ID.</param>
/// <param name="processor">The glyph variation processor, or <see langword="null"/> for non-variable fonts.</param>
/// <param name="bounds">When this method returns, contains the clip bounds if found; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if a clip box was found; otherwise, <see langword="false"/>.</returns>
private bool TryGetClipBox(ushort glyphId, GlyphVariationProcessor? processor, out Bounds? bounds)
{
if (this.clipList is null)
{
bounds = default;
return false;
}
return this.clipList.TryGetClipBox(glyphId, this, processor, out bounds);
}
/// <summary>
/// Loads the COLR table from the specified binary reader.
/// </summary>
/// <param name="reader">The big-endian binary reader positioned at the start of the COLR table.</param>
/// <returns>The loaded <see cref="ColrTable"/>.</returns>
public static ColrTable Load(BigEndianBinaryReader reader)
{
// HEADER
// Type | Name | Description
// ----------|------------------------|----------------------------------------------------------------------------------------------------
// uint16 | version | Table version number(starts at 0).
// uint16 | numBaseGlyphRecords | Number of Base Glyph Records.
// Offset32 | baseGlyphRecordsOffset | Offset(from beginning of COLR table) to Base Glyph records.
// Offset32 | layerRecordsOffset | Offset(from beginning of COLR table) to Layer Records.
// uint16 | numLayerRecords | Number of Layer Records.
ushort version = reader.ReadUInt16();
ushort numBaseGlyphRecords = reader.ReadUInt16();
uint baseGlyphRecordsOffset = reader.ReadOffset32();
uint layerRecordsOffset = reader.ReadOffset32();
ushort numLayerRecords = reader.ReadUInt16();
uint baseGlyphListOffset = 0;
uint layerListOffset = 0;
uint clipListOffset = 0;
uint varIndexMapOffset = 0;
uint itemVariationStoreOffset = 0;
if (version == 1)
{
// | Type | Name | Description |
// |----------|--------------------------|-------------------------------------------------------------------------------|
// | uint16 | version | Table version number—set to 1. |
// | uint16 | numBaseGlyphRecords | Number of BaseGlyph records; may be 0 in a version 1 table. |
// | Offset32 | baseGlyphRecordsOffset | Offset to baseGlyphRecords array, from beginning of COLR table (may be NULL). |
// | Offset32 | layerRecordsOffset | Offset to layerRecords array, from beginning of COLR table (may be NULL). |
// | uint16 | numLayerRecords | Number of Layer records; may be 0 in a version 1 table. |
// | Offset32 | baseGlyphListOffset | Offset to BaseGlyphList table, from beginning of COLR table. |
// | Offset32 | layerListOffset | Offset to LayerList table, from beginning of COLR table (may be NULL). |
// | Offset32 | clipListOffset | Offset to ClipList table, from beginning of COLR table (may be NULL). |
// | Offset32 | varIndexMapOffset | Offset to DeltaSetIndexMap table, from beginning of COLR table (may be NULL). |
// | Offset32 | itemVariationStoreOffset | Offset to ItemVariationStore, from beginning of COLR table (may be NULL). |
baseGlyphListOffset = reader.ReadOffset32();
layerListOffset = reader.ReadOffset32();
clipListOffset = reader.ReadOffset32();
varIndexMapOffset = reader.ReadOffset32();
itemVariationStoreOffset = reader.ReadOffset32();
}
// v0: BaseGlyph and Layer records (optional in v1; may be zero)
BaseGlyphRecord[] glyphs = [];
if (numBaseGlyphRecords != 0 && baseGlyphRecordsOffset != 0)
{
glyphs = new BaseGlyphRecord[numBaseGlyphRecords];
reader.Seek(baseGlyphRecordsOffset, SeekOrigin.Begin);
for (int i = 0; i < numBaseGlyphRecords; i++)
{
ushort gi = reader.ReadUInt16();
ushort idx = reader.ReadUInt16();
ushort num = reader.ReadUInt16();
glyphs[i] = new BaseGlyphRecord(gi, idx, num);
}
}
LayerRecord[] layerRecs = [];
if (numLayerRecords != 0 && layerRecordsOffset != 0)
{
layerRecs = new LayerRecord[numLayerRecords];
reader.Seek(layerRecordsOffset, SeekOrigin.Begin);
for (int i = 0; i < numLayerRecords; i++)
{
ushort gi = reader.ReadUInt16();
ushort pi = reader.ReadUInt16();
layerRecs[i] = new LayerRecord(gi, pi);
}
}
// v1: BaseGlyphList, LayerList, ClipList (nullable if not present)
BaseGlyphList? baseGlyphList = null;
LayerList? layerList = null;
ClipList? clipList = null;
Dictionary<uint, Paint>? paintCache = null;
if (version == 1)
{
baseGlyphList = BaseGlyphList.Load(reader, baseGlyphListOffset);
layerList = LayerList.Load(reader, layerListOffset);
clipList = ClipList.Load(reader, clipListOffset);
paintCache = LoadPaintRoots(reader, baseGlyphList, layerList);
}
ItemVariationStore? itemVariationStore = itemVariationStoreOffset != 0
? ItemVariationStore.Load(reader, itemVariationStoreOffset)
: null;
DeltaSetIndexMap[]? deltaSetIndexMap = varIndexMapOffset != 0
? DeltaSetIndexMap.Load(reader, varIndexMapOffset)
: null;
return new ColrTable(glyphs, layerRecs, baseGlyphList, layerList, clipList, itemVariationStore, deltaSetIndexMap, paintCache, 1);
}
/// <summary>
/// Eagerly loads and caches all paint objects referenced by the BaseGlyphList and LayerList.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="baseGlyphList">The base glyph list, or <see langword="null"/>.</param>
/// <param name="layerList">The layer list, or <see langword="null"/>.</param>
/// <returns>A dictionary mapping paint offsets to their resolved paint objects.</returns>
private static Dictionary<uint, Paint> LoadPaintRoots(
BigEndianBinaryReader reader,
BaseGlyphList? baseGlyphList,
LayerList? layerList)
{
PaintCaches caches = new();
// 1) Root paints from BaseGlyphList
if (baseGlyphList is not null)
{
foreach (BaseGlyphPaintRecord rec in baseGlyphList.Records)
{
if (rec.PaintOffset != 0)
{
_ = LoadPaintAt(reader, rec.PaintOffset, layerList, caches);
}
}
}
// 2) All paints referenced by LayerList (PaintColrLayers points into these)
if (layerList is not null)
{
foreach (uint offset in layerList.PaintOffsets)
{
if (offset != 0)
{
_ = LoadPaintAt(reader, offset, layerList, caches);
}
}
}
return caches.PaintCache;
}
/// <summary>
/// Loads a paint object from the specified offset, using the cache to avoid redundant reads.
/// Recursively loads child paints as needed.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="paintOffset">The COLR-relative offset of the paint table.</param>
/// <param name="layerList">The layer list for resolving PaintColrLayers references, or <see langword="null"/>.</param>
/// <param name="caches">The shared caches for deduplicating loaded objects.</param>
/// <returns>The loaded paint object.</returns>
private static Paint LoadPaintAt(
BigEndianBinaryReader reader,
uint paintOffset,
LayerList? layerList,
PaintCaches caches)
{
if (caches.PaintCache.TryGetValue(paintOffset, out Paint? p))
{
return p;
}
long restore = reader.BaseStream.Position;
reader.Seek(paintOffset, SeekOrigin.Begin);
byte format = reader.ReadByte();
Paint result;
switch (format)
{
// 1: PaintColrLayers
case 1:
{
byte numLayers = reader.ReadByte();
uint firstLayerIndex = reader.ReadUInt32();
result = new PaintColrLayers
{
Format = format,
NumLayers = numLayers,
FirstLayerIndex = firstLayerIndex
};
// Walk children immediately:
if (layerList is not null)
{
for (uint i = 0; i < numLayers; i++)
{
int idx = (int)(firstLayerIndex + i);
uint layerPaintOff = layerList.PaintOffsets[idx];
if (layerPaintOff != 0)
{
_ = LoadPaintAt(reader, layerPaintOff, layerList, caches);
}
}
}
break;
}
// 2/3: PaintSolid / PaintVarSolid
case 2:
{
ushort paletteIndex = reader.ReadUInt16();
float alpha = reader.ReadF2Dot14();
result = new PaintSolid { Format = format, PaletteIndex = paletteIndex, Alpha = alpha };
break;
}
case 3:
{
ushort paletteIndex = reader.ReadUInt16();
float alpha = reader.ReadF2Dot14();
uint varBase = reader.ReadUInt32();
result = new PaintVarSolid { Format = format, PaletteIndex = paletteIndex, Alpha = alpha, VarIndexBase = varBase };
break;
}
// 4/5: PaintLinearGradient / PaintVarLinearGradient
case 4:
{
uint colorLineOff = reader.ReadOffset24();
ColorLine line = LoadColorLineAt(reader, paintOffset + colorLineOff, caches);
short x0 = reader.ReadFWORD();
short y0 = reader.ReadFWORD();
short x1 = reader.ReadFWORD();
short y1 = reader.ReadFWORD();
short x2 = reader.ReadFWORD();
short y2 = reader.ReadFWORD();
result = new PaintLinearGradient { Format = format, ColorLine = line, X0 = x0, Y0 = y0, X1 = x1, Y1 = y1, X2 = x2, Y2 = y2 };
break;
}
case 5:
{
uint colorLineOff = reader.ReadOffset24();
VarColorLine line = LoadVarColorLineAt(reader, paintOffset + colorLineOff, caches);
short x0 = reader.ReadFWORD();
short y0 = reader.ReadFWORD();
short x1 = reader.ReadFWORD();
short y1 = reader.ReadFWORD();
short x2 = reader.ReadFWORD();
short y2 = reader.ReadFWORD();
uint varBase = reader.ReadUInt32();
result = new PaintVarLinearGradient
{
Format = format,
ColorLine = line,
X0 = x0,
Y0 = y0,
X1 = x1,
Y1 = y1,
X2 = x2,
Y2 = y2,
VarIndexBase = varBase
};
break;
}
// 6/7: PaintRadialGradient / PaintVarRadialGradient
case 6:
{
uint colorLineOff = reader.ReadOffset24();
ColorLine line = LoadColorLineAt(reader, paintOffset + colorLineOff, caches);
short x0 = reader.ReadFWORD();
short y0 = reader.ReadFWORD();
ushort r0 = reader.ReadUFWORD();
short x1 = reader.ReadFWORD();
short y1 = reader.ReadFWORD();
ushort r1 = reader.ReadUFWORD();
result = new PaintRadialGradient
{
Format = format,
ColorLine = line,
X0 = x0,
Y0 = y0,
Radius0 = r0,
X1 = x1,
Y1 = y1,
Radius1 = r1
};
break;
}
case 7:
{
uint colorLineOff = reader.ReadOffset24();
VarColorLine line = LoadVarColorLineAt(reader, paintOffset + colorLineOff, caches);
short x0 = reader.ReadFWORD();
short y0 = reader.ReadFWORD();
ushort r0 = reader.ReadUFWORD();
short x1 = reader.ReadFWORD();
short y1 = reader.ReadFWORD();
ushort r1 = reader.ReadUFWORD();
uint varBase = reader.ReadUInt32();
result = new PaintVarRadialGradient
{
Format = format,
ColorLine = line,
X0 = x0,
Y0 = y0,
Radius0 = r0,
X1 = x1,
Y1 = y1,
Radius1 = r1,
VarIndexBase = varBase
};
break;
}
// 8/9: PaintSweepGradient / PaintVarSweepGradient
case 8:
{
uint colorLineOff = reader.ReadOffset24();
ColorLine line = LoadColorLineAt(reader, paintOffset + colorLineOff, caches);
short cx = reader.ReadFWORD();
short cy = reader.ReadFWORD();
float start = reader.ReadF2Dot14();
float end = reader.ReadF2Dot14();
result = new PaintSweepGradient
{
Format = format,
ColorLine = line,
CenterX = cx,
CenterY = cy,
StartAngle = start,
EndAngle = end
};
break;
}
case 9:
{
uint colorLineOff = reader.ReadOffset24();
VarColorLine line = LoadVarColorLineAt(reader, paintOffset + colorLineOff, caches);
short cx = reader.ReadFWORD();
short cy = reader.ReadFWORD();
float start = reader.ReadF2Dot14();
float end = reader.ReadF2Dot14();
uint varBase = reader.ReadUInt32();
result = new PaintVarSweepGradient
{
Format = format,
ColorLine = line,
CenterX = cx,
CenterY = cy,
StartAngle = start,
EndAngle = end,
VarIndexBase = varBase
};
break;
}
// 10: PaintGlyph
case 10:
{
uint childOff = reader.ReadOffset24();
ushort gid = reader.ReadUInt16();
Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches);
result = new PaintGlyph { Format = format, Child = child, GlyphId = gid };
break;
}
// 11: PaintColrGlyph
case 11:
{
ushort gid = reader.ReadUInt16();
result = new PaintColrGlyph { Format = format, GlyphId = gid };
// Note: resolution of gid->root paint happens elsewhere when you interpret.
break;
}
// 12/13: PaintTransform / PaintVarTransform
case 12:
{
uint childOff = reader.ReadOffset24();
uint transformOff = reader.ReadOffset24();
Affine2x3 m = ReadAffine2x3At(reader, paintOffset + transformOff, caches);
Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches);
result = new PaintTransform { Format = format, Child = child, Transform = m };
break;
}
case 13:
{
uint childOff = reader.ReadOffset24();
uint transformOff = reader.ReadOffset24();
VarAffine2x3 vm = ReadVarAffine2x3At(reader, paintOffset + transformOff, caches);
Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches);
result = new PaintVarTransform { Format = format, Child = child, Transform = vm };
break;
}
// 14/15: PaintTranslate / PaintVarTranslate
case 14:
{
uint childOff = reader.ReadOffset24();
short dx = reader.ReadFWORD();
short dy = reader.ReadFWORD();
Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches);
result = new PaintTranslate { Format = format, Child = child, Dx = dx, Dy = dy };
break;
}
case 15:
{
uint childOff = reader.ReadOffset24();
short dx = reader.ReadFWORD();
short dy = reader.ReadFWORD();
uint varBase = reader.ReadUInt32();
Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches);
result = new PaintVarTranslate { Format = format, Child = child, Dx = dx, Dy = dy, VarIndexBase = varBase };
break;
}
// 16/17/18/19/20/21/22/23: Scale variants
case 16: // PaintScale
case 17: // PaintVarScale
case 18: // PaintScaleAroundCenter
case 19: // PaintVarScaleAroundCenter
case 20: // PaintScaleUniform
case 21: // PaintVarScaleUniform
case 22: // PaintScaleUniformAroundCenter
case 23: // PaintVarScaleUniformAroundCenter
{
bool aroundCenter = format is 18 or 19 or 22 or 23;
bool uniform = format is 20 or 21 or 22 or 23;
bool isVar = (format % 2) == 1;
uint childOff = reader.ReadOffset24();
float sx = reader.ReadF2Dot14();
float sy = uniform ? sx : reader.ReadF2Dot14();
short cx = 0, cy = 0;
if (aroundCenter)
{
cx = reader.ReadFWORD();
cy = reader.ReadFWORD();
}
uint varBase = isVar ? reader.ReadUInt32() : 0;
Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches);
if (isVar)
{
result = new PaintVarScale
{
Format = format,
Child = child,
ScaleX = sx,
ScaleY = sy,
CenterX = cx,
CenterY = cy,
AroundCenter = aroundCenter,
Uniform = uniform,
VarIndexBase = varBase
};
}
else
{
result = new PaintScale
{
Format = format,
Child = child,
ScaleX = sx,
ScaleY = sy,
CenterX = cx,
CenterY = cy,
AroundCenter = aroundCenter,
Uniform = uniform
};
}
break;
}
// 24/25/26/27: Rotate variants
case 24: // PaintRotate
case 25: // PaintVarRotate
case 26: // PaintRotateAroundCenter
case 27: // PaintVarRotateAroundCenter
{
bool aroundCenter = format is 26 or 27;
bool isVar = (format % 2) == 1;
uint childOff = reader.ReadOffset24();
float angle = reader.ReadF2Dot14();
short cx = 0, cy = 0;
if (aroundCenter)
{
cx = reader.ReadFWORD();
cy = reader.ReadFWORD();
}
uint varBase = isVar ? reader.ReadUInt32() : 0;
Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches);
if (isVar)
{
result = new PaintVarRotate
{
Format = format,
Child = child,
Angle = angle,
CenterX = cx,
CenterY = cy,
AroundCenter = aroundCenter,
VarIndexBase = varBase
};
}
else
{
result = new PaintRotate
{
Format = format,
Child = child,
Angle = angle,
CenterX = cx,
CenterY = cy,
AroundCenter = aroundCenter
};
}
break;
}
// 28/29/30/31: Skew variants
case 28: // PaintSkew
case 29: // PaintVarSkew
case 30: // PaintSkewAroundCenter
case 31: // PaintVarSkewAroundCenter
{
bool aroundCenter = format is 30 or 31;
bool isVar = (format % 2) == 1;
uint childOff = reader.ReadOffset24();
float xskew = reader.ReadF2Dot14();
float yskew = reader.ReadF2Dot14();
short cx = 0, cy = 0;
if (aroundCenter)
{
cx = reader.ReadFWORD();
cy = reader.ReadFWORD();
}
uint varBase = isVar ? reader.ReadUInt32() : 0;
Paint child = LoadPaintAt(reader, paintOffset + childOff, layerList, caches);
if (isVar)
{
result = new PaintVarSkew
{
Format = format,
Child = child,
XSkew = xskew,
YSkew = yskew,
CenterX = cx,
CenterY = cy,
AroundCenter = aroundCenter,
VarIndexBase = varBase
};
}
else
{
result = new PaintSkew
{
Format = format,
Child = child,
XSkew = xskew,
YSkew = yskew,
CenterX = cx,
CenterY = cy,
AroundCenter = aroundCenter
};
}
break;
}
// 32: Composite
case 32:
{
uint srcOff = reader.ReadOffset24();
ColrCompositeMode mode = reader.ReadByte<ColrCompositeMode>();
uint backOff = reader.ReadOffset24();
Paint src = LoadPaintAt(reader, paintOffset + srcOff, layerList, caches);
Paint back = LoadPaintAt(reader, paintOffset + backOff, layerList, caches);
result = new PaintComposite { Format = format, CompositeMode = mode, Source = src, Backdrop = back };
break;
}
default:
// Unknown format -> treat as no-op solid (or throw). We'll store a stub.
result = new PaintSolid { Format = format, PaletteIndex = 0, Alpha = 0 };
break;
}
caches.PaintCache[paintOffset] = result;
reader.BaseStream.Position = restore;
return result;
}
/// <summary>
/// Loads a <see cref="ColorLine"/> from the specified offset, using the cache to avoid redundant reads.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="offset">The COLR-relative offset of the color line.</param>
/// <param name="caches">The shared caches.</param>
/// <returns>The loaded color line.</returns>
private static ColorLine LoadColorLineAt(BigEndianBinaryReader reader, uint offset, PaintCaches caches)
{
if (caches.ColorLineCache.TryGetValue(offset, out ColorLine? line))
{
return line;
}
long restore = reader.BaseStream.Position;
reader.Seek(offset, SeekOrigin.Begin);
line = ColorLine.Load(reader);
caches.ColorLineCache[offset] = line;
reader.BaseStream.Position = restore;
return line;
}
/// <summary>
/// Loads a <see cref="VarColorLine"/> from the specified offset, using the cache to avoid redundant reads.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="offset">The COLR-relative offset of the variable color line.</param>
/// <param name="caches">The shared caches.</param>
/// <returns>The loaded variable color line.</returns>
private static VarColorLine LoadVarColorLineAt(BigEndianBinaryReader reader, uint offset, PaintCaches caches)
{
if (caches.VarColorLineCache.TryGetValue(offset, out VarColorLine? line))
{
return line;
}
long restore = reader.BaseStream.Position;
reader.Seek(offset, SeekOrigin.Begin);
line = VarColorLine.Load(reader);
caches.VarColorLineCache[offset] = line;
reader.BaseStream.Position = restore;
return line;
}
/// <summary>
/// Reads an <see cref="Affine2x3"/> matrix from the specified offset, using the cache to avoid redundant reads.
/// Matrix values are stored as Fixed 16.16 numbers.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="offset">The COLR-relative offset of the affine matrix.</param>
/// <param name="caches">The shared caches.</param>
/// <returns>The loaded affine matrix.</returns>
private static Affine2x3 ReadAffine2x3At(BigEndianBinaryReader reader, uint offset, PaintCaches caches)
{
if (caches.AffineCache.TryGetValue(offset, out Affine2x3 m))
{
return m;
}
long restore = reader.BaseStream.Position;
reader.Seek(offset, SeekOrigin.Begin);
float xx = reader.ReadFixed();
float yx = reader.ReadFixed();
float xy = reader.ReadFixed();
float yy = reader.ReadFixed();
float dx = reader.ReadFixed();
float dy = reader.ReadFixed();
m = new Affine2x3(xx, yx, xy, yy, dx, dy);
caches.AffineCache[offset] = m;
reader.BaseStream.Position = restore;
return m;
}
/// <summary>
/// Reads a <see cref="VarAffine2x3"/> matrix from the specified offset, using the cache to avoid redundant reads.
/// Matrix values are stored as Fixed 16.16 numbers with an appended variation index base.
/// </summary>
/// <param name="reader">The binary reader.</param>
/// <param name="offset">The COLR-relative offset of the variable affine matrix.</param>
/// <param name="caches">The shared caches.</param>
/// <returns>The loaded variable affine matrix.</returns>
private static VarAffine2x3 ReadVarAffine2x3At(BigEndianBinaryReader reader, uint offset, PaintCaches caches)
{
if (caches.VarAffineCache.TryGetValue(offset, out VarAffine2x3 m))
{
return m;
}
long restore = reader.BaseStream.Position;
reader.Seek(offset, SeekOrigin.Begin);
float xx = reader.ReadFixed();
float yx = reader.ReadFixed();
float xy = reader.ReadFixed();
float yy = reader.ReadFixed();
float dx = reader.ReadFixed();
float dy = reader.ReadFixed();
uint varBase = reader.ReadUInt32();
m = new VarAffine2x3(xx, yx, xy, yy, dx, dy, varBase);
caches.VarAffineCache[offset] = m;
reader.BaseStream.Position = restore;
return m;
}
}
/// <summary>
/// Holds per-load caches used during COLR table parsing to deduplicate paint objects,
/// color lines, and affine matrices that may be referenced from multiple offsets.
/// </summary>
internal sealed class PaintCaches
{
/// <summary>
/// Gets the cache of paint objects keyed by their COLR-relative offset.
/// </summary>
public Dictionary<uint, Paint> PaintCache { get; } = [];
/// <summary>
/// Gets the cache of color lines keyed by their COLR-relative offset.
/// </summary>
public Dictionary<uint, ColorLine> ColorLineCache { get; } = [];
/// <summary>
/// Gets the cache of variable color lines keyed by their COLR-relative offset.
/// </summary>
public Dictionary<uint, VarColorLine> VarColorLineCache { get; } = [];
/// <summary>
/// Gets the cache of affine matrices keyed by their COLR-relative offset.
/// </summary>
public Dictionary<uint, Affine2x3> AffineCache { get; } = [];
/// <summary>
/// Gets the cache of variable affine matrices keyed by their COLR-relative offset.
/// </summary>
public Dictionary<uint, VarAffine2x3> VarAffineCache { get; } = [];
}
/// <summary>
/// Represents a resolved COLR v1 glyph layer produced by flattening the paint DAG.
/// Associates a glyph ID with its paint node, geometry transform, paint transform, composite mode, and optional clip box.
/// </summary>
#pragma warning disable SA1201 // Elements should appear in the correct order
[DebuggerDisplay("Id: {GlyphId}")]
internal readonly struct ResolvedGlyphLayer
#pragma warning restore SA1201 // Elements should appear in the correct order
{
/// <summary>
/// Initializes a new instance of the <see cref="ResolvedGlyphLayer"/> struct.
/// </summary>
/// <param name="id">The glyph ID whose outline this layer paints.</param>
/// <param name="paint">The leaf paint node for this layer.</param>
/// <param name="glyphTransform">The accumulated affine transform applied to glyph geometry.</param>
/// <param name="paintTransform">The accumulated affine transform applied to the leaf paint.</param>
/// <param name="mode">The composite mode to apply.</param>
/// <param name="clipBox">The optional clip box bounds, or <see langword="null"/>.</param>
public ResolvedGlyphLayer(ushort id, Paint paint, Matrix3x2 glyphTransform, Matrix3x2 paintTransform, CompositeMode mode, Bounds? clipBox)
{
this.GlyphId = id;
this.Paint = paint;
this.GlyphTransform = glyphTransform;
this.PaintTransform = paintTransform;
this.CompositeMode = mode;
this.ClipBox = clipBox;
}
/// <summary>
/// Gets the glyph ID whose outline this layer paints.
/// </summary>
public ushort GlyphId { get; }
/// <summary>
/// Gets the leaf paint node for this layer.
/// </summary>
public Paint Paint { get; }
/// <summary>
/// Gets the accumulated affine transform applied to glyph geometry.
/// </summary>
public Matrix3x2 GlyphTransform { get; }
/// <summary>
/// Gets the accumulated affine transform applied to the leaf paint.
/// </summary>
public Matrix3x2 PaintTransform { get; }
/// <summary>
/// Gets the composite mode to apply when rendering this layer.
/// </summary>
public CompositeMode CompositeMode { get; }
/// <summary>
/// Gets the optional clip box bounds for this layer, or <see langword="null"/> if no clip applies.
/// </summary>
public Bounds? ClipBox { get; }
}
}