448 lines
19 KiB
C#
448 lines
19 KiB
C#
// 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 {
|
|
/// <summary>
|
|
/// A base class for COLR glyph sources.
|
|
/// </summary>
|
|
internal abstract class ColrGlyphSourceBase : IPaintedGlyphSource
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ColrGlyphSourceBase"/> class.
|
|
/// </summary>
|
|
/// <param name="colr">The COLR table.</param>
|
|
/// <param name="cpal">The CPAL table, or null if not present.</param>
|
|
/// <param name="glyphLoader">Delegate that loads a glyph outline for the given glyph id.</param>
|
|
public ColrGlyphSourceBase(ColrTable colr, CpalTable? cpal, Func<ushort, GlyphVector?> glyphLoader)
|
|
{
|
|
this.Colr = colr;
|
|
this.Cpal = cpal;
|
|
this.GlyphLoader = glyphLoader;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the COLR table.
|
|
/// </summary>
|
|
protected ColrTable Colr { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the CPAL table, or null if not present.
|
|
/// </summary>
|
|
protected CpalTable? Cpal { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the glyph loader delegate.
|
|
/// </summary>
|
|
protected Func<ushort, GlyphVector?> GlyphLoader { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public abstract bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas);
|
|
|
|
/// <summary>
|
|
/// Recursively flattens a COLR paint graph:
|
|
/// - Wrapper nodes pre-multiply their matrix into <paramref name="transform"/> and recurse to the child.
|
|
/// - Composite emits backdrop subtree first (inherits <paramref name="mode"/>),
|
|
/// then source subtree with <c>currentCompositeMode = node.CompositeMode</c>.
|
|
/// - Leaf nodes emit concrete Rendering.Paint with <c>Transform = accum</c> and <c>CompositeMode = currentBlend ?? default</c>.
|
|
/// Colors and stop offsets are passed through; no Y-flip applied here.
|
|
/// </summary>
|
|
/// <param name="node">The COLR paint node.</param>
|
|
/// <param name="transform">The affine matrix in document space.</param>
|
|
/// <param name="mode">The active composite mode to apply to leaf paints, or null for default.</param>
|
|
/// <param name="cpal">Optional CPAL palette for color resolution.</param>
|
|
/// <param name="colr">The COLR table for variation delta resolution.</param>
|
|
/// <param name="processor">The glyph variation processor, or null for non-variable fonts.</param>
|
|
/// <param name="outLeaves">Collector for emitted leaf paints.</param>
|
|
protected static void FlattenPaint(
|
|
Paint node,
|
|
Matrix3x2 transform,
|
|
CompositeMode mode,
|
|
CpalTable? cpal,
|
|
ColrTable colr,
|
|
GlyphVariationProcessor? processor,
|
|
List<Rendering.Paint> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a glyph vector into a sequence of path commands.
|
|
/// </summary>
|
|
/// <param name="gv">The glyph vector.</param>
|
|
protected static List<PathCommand> BuildPath(GlyphVector gv)
|
|
{
|
|
IList<ControlPoint> points = gv.ControlPoints;
|
|
IReadOnlyList<ushort> ends = gv.EndPoints;
|
|
|
|
List<PathCommand> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps COLR Extend to renderer SpreadMethod.
|
|
/// </summary>
|
|
/// <param name="extend">The COLR extend mode.</param>
|
|
[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
|
|
};
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
/// <param name="line">The color line.</param>
|
|
/// <param name="cpal">The CPAL table, or null if not present.</param>
|
|
/// <returns>The resolved gradient stops.</returns>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
/// <param name="line">The variable color line.</param>
|
|
/// <param name="cpal">The CPAL table, or null if not present.</param>
|
|
/// <param name="colr">The COLR table for delta resolution.</param>
|
|
/// <param name="processor">The glyph variation processor, or null.</param>
|
|
/// <returns>The resolved gradient stops.</returns>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a CPAL palette entry with an alpha multiplier.
|
|
/// </summary>
|
|
/// <param name="cpal">The CPAL table, or null if not present.</param>
|
|
/// <param name="paletteEntryIndex">The palette entry index.</param>
|
|
/// <param name="alphaMul">The alpha multiplier.</param>
|
|
/// <returns>The resolved color.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the midpoint between two vectors.
|
|
/// </summary>
|
|
/// <param name="a">The first vector to use in the midpoint calculation.</param>
|
|
/// <param name="b">The second vector to use in the midpoint calculation.</param>
|
|
/// <returns>A <see cref="Vector2"/> representing the point exactly halfway between the two input vectors.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static Vector2 Mid(Vector2 a, Vector2 b)
|
|
=> (a + b) * .5F;
|
|
}
|
|
}
|