// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.Fonts.Rendering;
using SixLabors.Fonts.Tables.AdvancedTypographic.Variations;
using SixLabors.Fonts.Tables.TrueType.Glyphs;
namespace SixLabors.Fonts.Tables.General.Colr {
///
/// A base class for COLR glyph sources.
///
internal abstract class ColrGlyphSourceBase : IPaintedGlyphSource
{
///
/// Initializes a new instance of the class.
///
/// The COLR table.
/// The CPAL table, or null if not present.
/// Delegate that loads a glyph outline for the given glyph id.
public ColrGlyphSourceBase(ColrTable colr, CpalTable? cpal, Func glyphLoader)
{
this.Colr = colr;
this.Cpal = cpal;
this.GlyphLoader = glyphLoader;
}
///
/// Gets the COLR table.
///
protected ColrTable Colr { get; }
///
/// Gets the CPAL table, or null if not present.
///
protected CpalTable? Cpal { get; }
///
/// Gets the glyph loader delegate.
///
protected Func GlyphLoader { get; }
///
public abstract bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas);
///
/// Recursively flattens a COLR paint graph:
/// - Wrapper nodes pre-multiply their matrix into and recurse to the child.
/// - Composite emits backdrop subtree first (inherits ),
/// then source subtree with currentCompositeMode = node.CompositeMode.
/// - Leaf nodes emit concrete Rendering.Paint with Transform = accum and CompositeMode = currentBlend ?? default.
/// Colors and stop offsets are passed through; no Y-flip applied here.
///
/// The COLR paint node.
/// The affine matrix in document space.
/// The active composite mode to apply to leaf paints, or null for default.
/// Optional CPAL palette for color resolution.
/// The COLR table for variation delta resolution.
/// The glyph variation processor, or null for non-variable fonts.
/// Collector for emitted leaf paints.
protected static void FlattenPaint(
Paint node,
Matrix3x2 transform,
CompositeMode mode,
CpalTable? cpal,
ColrTable colr,
GlyphVariationProcessor? processor,
List outLeaves)
{
// The input node will only be a paintable leaf here, as upstream resolution
// should have eliminated glyph/colr-glyph nodes and flattened composites.
switch (node)
{
case PaintSolid ps:
{
if (ps.PaletteIndex == 0xFFFF)
{
// "Use foreground" => represent as a SolidPaint with fully transparent color;
// renderer can substitute foreground if needed.
outLeaves.Add(new SolidPaint
{
Color = new GlyphColor(0, 0, 0, 0),
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
GlyphColor color = ResolveColor(cpal, ps.PaletteIndex, ps.Alpha);
outLeaves.Add(new SolidPaint
{
Color = color,
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
case PaintVarSolid pvs:
{
float alpha = pvs.Alpha + colr.ResolveDelta(processor, pvs.VarIndexBase + 0u);
if (pvs.PaletteIndex == 0xFFFF)
{
outLeaves.Add(new SolidPaint
{
Color = new GlyphColor(0, 0, 0, 0),
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
GlyphColor color = ResolveColor(cpal, pvs.PaletteIndex, alpha);
outLeaves.Add(new SolidPaint
{
Color = color,
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
case PaintLinearGradient pl:
{
GradientStop[] stops = ResolveStops(pl.ColorLine, cpal);
outLeaves.Add(new LinearGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
P0 = new Vector2(pl.X0, pl.Y0),
P1 = new Vector2(pl.X1, pl.Y1),
P2 = new Vector2(pl.X2, pl.Y2),
Spread = MapSpread(pl.ColorLine.Extend),
Stops = stops,
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
case PaintVarLinearGradient vpl:
{
uint vib = vpl.VarIndexBase;
GradientStop[] stops = ResolveStops(vpl.ColorLine, cpal, colr, processor);
outLeaves.Add(new LinearGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
P0 = new Vector2(vpl.X0 + colr.ResolveDelta(processor, vib + 0u), vpl.Y0 + colr.ResolveDelta(processor, vib + 1u)),
P1 = new Vector2(vpl.X1 + colr.ResolveDelta(processor, vib + 2u), vpl.Y1 + colr.ResolveDelta(processor, vib + 3u)),
P2 = new Vector2(vpl.X2 + colr.ResolveDelta(processor, vib + 4u), vpl.Y2 + colr.ResolveDelta(processor, vib + 5u)),
Spread = MapSpread(vpl.ColorLine.Extend),
Stops = stops,
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
case PaintRadialGradient pr:
{
GradientStop[] stops = ResolveStops(pr.ColorLine, cpal);
outLeaves.Add(new RadialGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
Center0 = new Vector2(pr.X0, pr.Y0),
Radius0 = pr.Radius0,
Center1 = new Vector2(pr.X1, pr.Y1),
Radius1 = pr.Radius1,
Spread = MapSpread(pr.ColorLine.Extend),
Stops = stops,
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
case PaintVarRadialGradient vpr:
{
uint vib = vpr.VarIndexBase;
GradientStop[] stops = ResolveStops(vpr.ColorLine, cpal, colr, processor);
outLeaves.Add(new RadialGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
Center0 = new Vector2(vpr.X0 + colr.ResolveDelta(processor, vib + 0u), vpr.Y0 + colr.ResolveDelta(processor, vib + 1u)),
Radius0 = (ushort)(vpr.Radius0 + colr.ResolveDelta(processor, vib + 2u)),
Center1 = new Vector2(vpr.X1 + colr.ResolveDelta(processor, vib + 3u), vpr.Y1 + colr.ResolveDelta(processor, vib + 4u)),
Radius1 = (ushort)(vpr.Radius1 + colr.ResolveDelta(processor, vib + 5u)),
Spread = MapSpread(vpr.ColorLine.Extend),
Stops = stops,
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
case PaintSweepGradient sw:
{
GradientStop[] stops = ResolveStops(sw.ColorLine, cpal);
outLeaves.Add(new SweepGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
Center = new Vector2(sw.CenterX, sw.CenterY),
// Spec says: add 1.0 and multiply by 180 to retrieve counter-clockwise degrees.
StartAngle = (sw.StartAngle + 1F) * 180F,
EndAngle = (sw.EndAngle + 1F) * 180F,
Spread = MapSpread(sw.ColorLine.Extend),
Stops = stops,
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
case PaintVarSweepGradient vsw:
{
uint vib = vsw.VarIndexBase;
GradientStop[] stops = ResolveStops(vsw.ColorLine, cpal, colr, processor);
float startAngle = vsw.StartAngle + colr.ResolveDelta(processor, vib + 2u);
float endAngle = vsw.EndAngle + colr.ResolveDelta(processor, vib + 3u);
outLeaves.Add(new SweepGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
Center = new Vector2(vsw.CenterX + colr.ResolveDelta(processor, vib + 0u), vsw.CenterY + colr.ResolveDelta(processor, vib + 1u)),
StartAngle = (startAngle + 1F) * 180F,
EndAngle = (endAngle + 1F) * 180F,
Spread = MapSpread(vsw.ColorLine.Extend),
Stops = stops,
Opacity = 1F,
Transform = transform,
CompositeMode = mode
});
return;
}
default:
return;
}
}
///
/// Converts a glyph vector into a sequence of path commands.
///
/// The glyph vector.
protected static List BuildPath(GlyphVector gv)
{
IList points = gv.ControlPoints;
IReadOnlyList ends = gv.EndPoints;
List cmds = new(points.Count + ends.Count);
int endOfContour = -1;
for (int ci = 0; ci < ends.Count; ci++)
{
int startOfContour = endOfContour + 1;
endOfContour = ends[ci];
if (endOfContour < startOfContour)
{
continue;
}
int length = endOfContour - startOfContour + 1;
if (length == 0)
{
continue;
}
// Choose initial MoveTo: last on-curve, else first on-curve, else midpoint(last, first).
ControlPoint first = points[startOfContour];
ControlPoint last = points[endOfContour];
Vector2 moveTo = last.OnCurve ? last.Point
: first.OnCurve ? first.Point
: Mid(last.Point, first.Point);
cmds.Add(PathCommand.MoveTo(moveTo));
// Ring traversal over input points.
Vector2 curr = last.Point;
Vector2 next = first.Point;
for (int p = 0; p < length; p++)
{
Vector2 prev = curr;
curr = next;
int currentIndex = startOfContour + p;
int nextIndex = startOfContour + ((p + 1) % length);
int prevIndex = startOfContour + ((length + p - 1) % length);
next = points[nextIndex].Point;
bool currOn = points[currentIndex].OnCurve;
bool prevOn = points[prevIndex].OnCurve;
bool nextOn = points[nextIndex].OnCurve;
if (currOn)
{
// Emit line to the current on-curve point unconditionally.
cmds.Add(PathCommand.LineTo(curr));
continue;
}
// Off-curve: insert implicit on-curve midpoints.
Vector2 prev2 = prevOn ? prev : Mid(curr, prev);
Vector2 next2 = nextOn ? next : Mid(curr, next);
if (!prevOn)
{
// Conditional line when previous input point was off-curve.
cmds.Add(PathCommand.LineTo(prev2));
}
// Metrics emits a LineTo(prev2) immediately before the quadratic as well.
cmds.Add(PathCommand.LineTo(prev2));
// Quadratic segment with control at current off-curve and endpoint at next2.
cmds.Add(PathCommand.QuadraticTo(curr, next2));
}
cmds.Add(PathCommand.Close());
}
return cmds;
}
///
/// Maps COLR Extend to renderer SpreadMethod.
///
/// The COLR extend mode.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static SpreadMethod MapSpread(Extend extend)
=> extend switch
{
Extend.Pad => SpreadMethod.Pad,
Extend.Repeat => SpreadMethod.Repeat,
Extend.Reflect => SpreadMethod.Reflect,
_ => SpreadMethod.Pad
};
///
/// Resolves a color line into concrete gradient stops. Offsets are clamped to [0,1].
/// 0xFFFF palette indices are treated as transparent here (foreground color handled by text color elsewhere).
///
/// The color line.
/// The CPAL table, or null if not present.
/// The resolved gradient stops.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static GradientStop[] ResolveStops(ColorLine line, CpalTable? cpal)
{
ColorStop[] src = line.Stops;
GradientStop[] stops = new GradientStop[src.Length];
for (int i = 0; i < src.Length; i++)
{
ref readonly ColorStop s = ref src[i];
GlyphColor c = s.PaletteIndex == 0xFFFF
? new GlyphColor(0, 0, 0, 0) // transparent placeholder; renderer can blend with foreground
: ResolveColor(cpal, s.PaletteIndex, s.Alpha);
float offset = Math.Clamp(s.StopOffset, 0F, 1F);
stops[i] = new GradientStop(offset, c);
}
return stops;
}
///
/// Resolves a variable color line into concrete gradient stops with variation deltas applied.
/// Offsets are clamped to [0,1].
/// 0xFFFF palette indices are treated as transparent here (foreground color handled by text color elsewhere).
///
/// The variable color line.
/// The CPAL table, or null if not present.
/// The COLR table for delta resolution.
/// The glyph variation processor, or null.
/// The resolved gradient stops.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static GradientStop[] ResolveStops(VarColorLine line, CpalTable? cpal, ColrTable colr, GlyphVariationProcessor? processor)
{
VarColorStop[] src = line.Stops;
GradientStop[] stops = new GradientStop[src.Length];
for (int i = 0; i < src.Length; i++)
{
ref readonly VarColorStop s = ref src[i];
// Per spec: VarColorStop has varIndexBase with offsets +0 = stopOffset, +1 = alpha.
float stopOffset = s.StopOffset + colr.ResolveDelta(processor, s.VarIndexBase + 0u);
float alpha = s.Alpha + colr.ResolveDelta(processor, s.VarIndexBase + 1u);
GlyphColor c = s.PaletteIndex == 0xFFFF
? new GlyphColor(0, 0, 0, 0) // transparent placeholder; renderer can blend with foreground
: ResolveColor(cpal, s.PaletteIndex, alpha);
float offset = Math.Clamp(stopOffset, 0F, 1F);
stops[i] = new GradientStop(offset, c);
}
return stops;
}
///
/// Resolves a CPAL palette entry with an alpha multiplier.
///
/// The CPAL table, or null if not present.
/// The palette entry index.
/// The alpha multiplier.
/// The resolved color.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static GlyphColor ResolveColor(CpalTable? cpal, int paletteEntryIndex, float alphaMul)
{
// Palette index 0 selection. If you later expose palette selection, thread it here.
GlyphColor baseColor = cpal is null ? new GlyphColor(0, 0, 0, 0) : cpal.GetGlyphColor(0, paletteEntryIndex);
byte a = (byte)Math.Clamp((int)MathF.Round(baseColor.A * alphaMul), 0, 255);
return new GlyphColor(baseColor.R, baseColor.G, baseColor.B, a);
}
///
/// Calculates the midpoint between two vectors.
///
/// The first vector to use in the midpoint calculation.
/// The second vector to use in the midpoint calculation.
/// A representing the point exactly halfway between the two input vectors.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector2 Mid(Vector2 a, Vector2 b)
=> (a + b) * .5F;
}
}