// 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; } }