// 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 { /// /// Represents the OpenType COLR table, which defines color glyph data for both v0 (layer-based) /// and v1 (paint-based) color fonts. /// /// internal class ColrTable : Table { /// /// The table tag name "COLR". /// internal const string TableName = "COLR"; /// /// The COLR v0 base glyph records mapping glyph IDs to layer ranges. /// private readonly BaseGlyphRecord[] glyphRecords; /// /// The COLR v0 layer records defining color layers. /// private readonly LayerRecord[] layers; /// /// The COLR v1 BaseGlyphList, or if not present. /// private readonly BaseGlyphList? baseGlyphList; /// /// The COLR v1 LayerList, or if not present. /// private readonly LayerList? layerList; /// /// The COLR v1 ClipList, or if not present. /// private readonly ClipList? clipList; /// /// The ItemVariationStore for variable font data, or if not present. /// private readonly ItemVariationStore? itemVariationStore; /// /// The DeltaSetIndexMap array for mapping variation indices, or if not present. /// private readonly DeltaSetIndexMap[]? deltaSetIndexMap; /// /// Cache of resolved paint objects keyed by their COLR-relative offset. /// private readonly Dictionary? paintCache; /// /// Initializes a new instance of the class for COLR v0 data only. /// /// The base glyph records. /// The layer records. public ColrTable( BaseGlyphRecord[] glyphRecords, LayerRecord[] layers) : this(glyphRecords, layers, null, null, null, null, null, null, 0) { } /// /// Initializes a new instance of the class with both v0 and optional v1 data. /// /// The COLR v0 base glyph records. /// The COLR v0 layer records. /// The COLR v1 base glyph list, or . /// The COLR v1 layer list, or . /// The COLR v1 clip list, or . /// The ItemVariationStore for variable font data, or . /// The DeltaSetIndexMap array, or . /// The pre-loaded paint cache, or . /// The COLR table version. public ColrTable( BaseGlyphRecord[] glyphRecords, LayerRecord[] layers, BaseGlyphList? baseGlyphList, LayerList? layerList, ClipList? clipList, ItemVariationStore? itemVariationStore, DeltaSetIndexMap[]? deltaSetIndexMap, Dictionary? 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; } /// /// Gets the COLR table version (0 or 1). /// public int Version { get; } /// /// Resolves a variation delta for a given variable index using the COLR table's /// own ItemVariationStore and optional DeltaSetIndexMap. /// /// The glyph variation processor (null for non-variable fonts). /// The variable index (VarIndexBase + field offset). /// The delta value, or 0 if no variation data is available. 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); } /// /// Loads the COLR table from the specified font reader. /// /// The font reader. /// The loaded , or if the table is not present. public static ColrTable? Load(FontReader fontReader) { if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) { return null; } using (binaryReader) { return Load(binaryReader); } } /// /// Gets the COLR v0 layer records for the specified glyph. /// /// The glyph ID. /// A span of layer records for the glyph, or an empty span if not found. internal Span GetLayers(ushort glyph) { foreach (BaseGlyphRecord g in this.glyphRecords) { if (g.GlyphId == glyph) { return this.layers.AsSpan().Slice(g.FirstLayerIndex, g.LayerCount); } } return []; } /// /// Determines whether the specified glyph has an associated COLR v0 color glyph definition. /// /// The identifier of the glyph to check for a COLR v0 color glyph definition. /// /// if the specified glyph has a COLR v0 color glyph definition; otherwise, . /// 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; } /// /// Determines whether the specified glyph has an associated COLR v1 color glyph definition. /// /// The identifier of the glyph to check for a COLR v1 color glyph definition. /// /// if the specified glyph has a COLR v1 color glyph definition; otherwise, . /// 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 _); } /// /// Attempts to retrieve the set of color layer records associated with the specified glyph. /// /// The glyph ID for which to retrieve color layer records. /// /// When this method returns, contains a span of structures /// representing the color layers for the specified glyph, if found; otherwise, an empty span. /// /// /// if color layer records are found for the specified glyph; otherwise, /// . /// internal bool TryGetColrV0Layers(ushort glyph, out Span 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; } /// /// Attempts to resolve and retrieve the list of color glyph layers for the specified glyph ID. /// /// The identifier of the glyph for which to resolve color layers. /// The glyph variation processor, or null for non-variable fonts. /// /// When this method returns, contains a list of resolved glyph layers if the operation succeeds; otherwise, /// . This parameter is passed uninitialized. /// /// if the color glyph layers were successfully resolved; otherwise, . /// internal bool TryGetColrV1Layers( ushort glyphId, GlyphVariationProcessor? processor, [NotNullWhen(true)] out List? 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 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; } /// /// Recursively flattens a COLR v1 paint subtree into s. /// A layer is emitted only when a leaf paint is reached under an active glyph-binding node: /// /// PaintGlyph sets the current glyph id to its GlyphId and recurses into its child paint. /// PaintColrGlyph resolves that glyph's root paint, sets the current glyph id, and recurses. /// Wrapper nodes (transform/translate/scale/rotate/skew, var forms) forward the current glyph id unchanged. /// PaintComposite flattens both branches independently, forwarding the current glyph id to each. /// Leaf paints (solid/linear/radial/sweep, var forms) emit a layer only if has a value. /// /// /// The paint node to flatten. /// /// The glyph id whose outline will receive the paint. Set by PaintGlyph/PaintColrGlyph. /// /// The accumulated transform to apply to the glyph's geometry. /// The accumulated transform to apply to the paint. /// Whether wrapper transforms should be applied to the paint (true) or to the glyph geometry (false). /// Accumulated composite mode. /// The glyph variation processor, or null for non-variable fonts. /// Accumulator for resolved layers. private void FlattenPaintToLayers( Paint node, ushort? currentGlyphId, Matrix3x2 glyphTransform, Matrix3x2 paintTransform, bool transformPaint, CompositeMode compositeMode, GlyphVariationProcessor? processor, List 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 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; } } } /// /// Builds a scale matrix, optionally around a center. /// [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)); } /// /// Builds a rotation matrix, optionally around a center. /// [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)); } /// /// Builds a skew matrix, optionally around a center. /// [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)); } /// /// Maps a COLR composite mode to the internal . /// /// Returns when is null /// or when the value is not recognized. /// /// /// The optional COLR composite mode. /// The mapped . [MethodImpl(MethodImplOptions.AggressiveInlining)] private static CompositeMode MapCompositeMode(ColrCompositeMode? mode) => mode switch { // Porter–Duff 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, }; /// /// Attempts to retrieve the paint table offset associated with the specified glyph ID. /// /// The glyph ID for which to look up the paint table offset. /// /// When this method returns, contains the paint table offset for the specified glyph ID, if found; otherwise, zero. /// This parameter is passed uninitialized. /// /// /// if the paint table offset was found for the specified glyph ID; otherwise, . /// private bool TryGetRootPaintOffset(ushort glyphId, out uint paintOffset) { if (this.baseGlyphList is null) { paintOffset = 0; return false; } ReadOnlySpan 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; } /// /// Gets a span of paint offsets from the layer list starting at the specified index. /// /// The index of the first paint offset. /// The number of paint offsets to retrieve. /// A read-only span of paint offsets, or an empty span if the layer list is null or the range is invalid. private ReadOnlySpan GetLayerPaintOffsets(int first, int count) { if (this.layerList is null || count <= 0) { return []; } Span 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); } /// /// Attempts to retrieve the clip box bounds for the specified glyph ID. /// /// The glyph ID. /// The glyph variation processor, or for non-variable fonts. /// When this method returns, contains the clip bounds if found; otherwise, . /// if a clip box was found; otherwise, . 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); } /// /// Loads the COLR table from the specified binary reader. /// /// The big-endian binary reader positioned at the start of the COLR table. /// The loaded . 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? 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); } /// /// Eagerly loads and caches all paint objects referenced by the BaseGlyphList and LayerList. /// /// The binary reader. /// The base glyph list, or . /// The layer list, or . /// A dictionary mapping paint offsets to their resolved paint objects. private static Dictionary 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; } /// /// Loads a paint object from the specified offset, using the cache to avoid redundant reads. /// Recursively loads child paints as needed. /// /// The binary reader. /// The COLR-relative offset of the paint table. /// The layer list for resolving PaintColrLayers references, or . /// The shared caches for deduplicating loaded objects. /// The loaded paint object. 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(); 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; } /// /// Loads a from the specified offset, using the cache to avoid redundant reads. /// /// The binary reader. /// The COLR-relative offset of the color line. /// The shared caches. /// The loaded color line. 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; } /// /// Loads a from the specified offset, using the cache to avoid redundant reads. /// /// The binary reader. /// The COLR-relative offset of the variable color line. /// The shared caches. /// The loaded variable color line. 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; } /// /// Reads an matrix from the specified offset, using the cache to avoid redundant reads. /// Matrix values are stored as Fixed 16.16 numbers. /// /// The binary reader. /// The COLR-relative offset of the affine matrix. /// The shared caches. /// The loaded affine matrix. 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; } /// /// Reads a 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. /// /// The binary reader. /// The COLR-relative offset of the variable affine matrix. /// The shared caches. /// The loaded variable affine matrix. 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; } } /// /// 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. /// internal sealed class PaintCaches { /// /// Gets the cache of paint objects keyed by their COLR-relative offset. /// public Dictionary PaintCache { get; } = []; /// /// Gets the cache of color lines keyed by their COLR-relative offset. /// public Dictionary ColorLineCache { get; } = []; /// /// Gets the cache of variable color lines keyed by their COLR-relative offset. /// public Dictionary VarColorLineCache { get; } = []; /// /// Gets the cache of affine matrices keyed by their COLR-relative offset. /// public Dictionary AffineCache { get; } = []; /// /// Gets the cache of variable affine matrices keyed by their COLR-relative offset. /// public Dictionary VarAffineCache { get; } = []; } /// /// 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. /// #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 { /// /// Initializes a new instance of the struct. /// /// The glyph ID whose outline this layer paints. /// The leaf paint node for this layer. /// The accumulated affine transform applied to glyph geometry. /// The accumulated affine transform applied to the leaf paint. /// The composite mode to apply. /// The optional clip box bounds, or . 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; } /// /// Gets the glyph ID whose outline this layer paints. /// public ushort GlyphId { get; } /// /// Gets the leaf paint node for this layer. /// public Paint Paint { get; } /// /// Gets the accumulated affine transform applied to glyph geometry. /// public Matrix3x2 GlyphTransform { get; } /// /// Gets the accumulated affine transform applied to the leaf paint. /// public Matrix3x2 PaintTransform { get; } /// /// Gets the composite mode to apply when rendering this layer. /// public CompositeMode CompositeMode { get; } /// /// Gets the optional clip box bounds for this layer, or if no clip applies. /// public Bounds? ClipBox { get; } } }