// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Numerics;
using SixLabors.Fonts.Unicode;
namespace SixLabors.Fonts.Rendering {
///
/// Provides painted (layered) glyph rendering for color formats such as COLR v1 and OT-SVG.
/// Geometry and paints are supplied in document-space by an interpreter; all layout transforms
/// (UPEM mapping, DPI/point-size scaling, rotation, final placement) are applied here.
///
public sealed class PaintedGlyphMetrics : FontGlyphMetrics
{
private readonly IPaintedGlyphSource source;
///
/// Initializes a new instance of the class.
///
/// The font metrics.
/// The glyph identifier.
/// The code point.
/// The painted glyph source.
/// The design-space bounds for the glyph.
/// The advance width.
/// The advance height.
/// The left side bearing.
/// The top side bearing.
/// Units per EM.
/// Text attributes.
/// Text decorations.
internal PaintedGlyphMetrics(
StreamFontMetrics font,
ushort glyphId,
CodePoint codePoint,
IPaintedGlyphSource source,
Bounds bounds,
ushort advanceWidth,
ushort advanceHeight,
short leftSideBearing,
short topSideBearing,
ushort unitsPerEM,
TextAttributes textAttributes,
TextDecorations textDecorations)
: base(
font,
glyphId,
codePoint,
bounds,
advanceWidth,
advanceHeight,
leftSideBearing,
topSideBearing,
unitsPerEM,
textAttributes,
textDecorations,
GlyphType.Painted)
=> this.source = source;
///
/// Initializes a new instance of the class for rendering with overrides.
///
internal PaintedGlyphMetrics(
StreamFontMetrics font,
ushort glyphId,
CodePoint codePoint,
IPaintedGlyphSource source,
Bounds bounds,
ushort advanceWidth,
ushort advanceHeight,
short leftSideBearing,
short topSideBearing,
ushort unitsPerEM,
Vector2 offset,
Vector2 scaleFactor,
TextRun textRun)
: base(
font,
glyphId,
codePoint,
bounds,
advanceWidth,
advanceHeight,
leftSideBearing,
topSideBearing,
unitsPerEM,
offset,
scaleFactor,
textRun,
GlyphType.Painted)
=> this.source = source;
///
internal override FontGlyphMetrics CloneForRendering(TextRun textRun)
=> new PaintedGlyphMetrics(
this.FontMetrics,
this.GlyphId,
this.CodePoint,
this.source,
this.Bounds,
this.AdvanceWidth,
this.AdvanceHeight,
this.LeftSideBearing,
this.TopSideBearing,
this.UnitsPerEm,
this.Offset,
this.ScaleFactor,
textRun);
///
internal override void RenderTo(
IGlyphRenderer renderer,
int graphemeIndex,
Vector2 glyphOrigin,
Vector2 decorationOrigin,
GlyphLayoutMode mode,
TextOptions options)
{
if (ShouldSkipGlyphRendering(this.CodePoint))
{
return;
}
float pointSize = this.TextRun.Font?.Size ?? options.Font.Size;
float dpi = options.Dpi;
// Device-space placement.
glyphOrigin *= dpi;
decorationOrigin *= dpi;
float scaledPpem = this.GetScaledSize(pointSize, dpi);
Vector2 scale = new Vector2(scaledPpem) / this.ScaleFactor; // uniform
Matrix3x2 rotation = GetRotationMatrix(mode);
// Layout similarity: uniform scale then rotation; translation added below.
Matrix3x2 layout = Matrix3x2.CreateScale(scale);
layout *= rotation;
layout.Translation = (this.Offset * scale) + glyphOrigin;
// Bounds in device space for BeginGlyph.
FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPpem);
GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex);
if (renderer.BeginGlyph(in box, in parameters))
{
if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint)
&& this.source.TryGetPaintedGlyph(this.GlyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas))
{
// Source-to-UPEM: viewBox mapping (uniform "meet"), optional y-flip, optional root transform.
Matrix3x2 s2u = ComputeSourceToUpem(canvas, this.UnitsPerEm);
// Full transform from source doc-space to device space.
Matrix3x2 total = s2u * layout;
// Stream layers and commands with correct transforms.
StreamPaintedGlyph(glyph, in box, renderer, total);
}
renderer.EndGlyph();
this.RenderDecorationsTo(renderer, decorationOrigin, mode, rotation, scaledPpem, options);
}
}
///
/// Computes the mapping from the interpreter's document-space to UPEM font space.
/// Enforces a uniform 'meet' scale from the root viewBox (if present) and flips Y
/// only if the source is y-up.
///
private static Matrix3x2 ComputeSourceToUpem(in PaintedCanvasMetadata canvas, ushort upem)
{
Matrix3x2 m = Matrix3x2.Identity;
// Root transform (doc-space). Apply first if provided.
if (!canvas.RootTransform.IsIdentity)
{
m *= canvas.RootTransform;
}
// Translate viewBox min to origin, then uniform scale to UPEM using "meet".
if (canvas.HasViewBox)
{
Matrix3x2 t = Matrix3x2.CreateTranslation(-canvas.ViewBox.X, -canvas.ViewBox.Y);
float sx = upem / Math.Max(canvas.ViewBox.Width, 1e-6f);
float sy = upem / Math.Max(canvas.ViewBox.Height, 1e-6f);
float s = MathF.Min(sx, sy);
Matrix3x2 sUni = Matrix3x2.CreateScale(s);
m = m * t * sUni;
}
// Coordinate system orientation.
if (!canvas.IsYDown)
{
// Flip Y around the origin; placement happens in layout.
m *= Matrix3x2.CreateScale(1f, -1f);
}
return m;
}
///
/// Streams the painted glyph to the renderer, transforming geometry and userSpaceOnUse paints.
///
/// The painted glyph.
/// The device-space bounds of the glyph.
/// The glyph renderer.
/// The full device-space transform to apply.
private static void StreamPaintedGlyph(
in PaintedGlyph glyph,
in FontRectangle bounds,
IGlyphRenderer renderer,
Matrix3x2 xform)
{
IReadOnlyList layers = glyph.Layers;
for (int i = 0; i < layers.Count; i++)
{
PaintedLayer layer = layers[i];
// pre-applied transforms (element/group)
Matrix3x2 layerXform = layer.Transform * xform;
// Clip bounds in device space (if any).
ClipQuad? clipBounds = layer.ClipBounds.HasValue
? ClipQuad.FromBounds(layer.ClipBounds.Value, layerXform)
: null;
// Similarity decomposition for arc radii/angle/sweep adjustment (from layer).
Similarity sim = Similarity.FromMatrix(layerXform);
// Transform userSpaceOnUse paints into device space; keep ObjectBoundingBox normalized.
Paint? paint = TransformPaint(layer.Paint, in bounds, layerXform);
renderer.BeginLayer(paint, layer.FillRule, clipBounds);
bool open = false;
IReadOnlyList cmds = layer.Path;
for (int j = 0; j < cmds.Count; j++)
{
PathCommand c = cmds[j];
switch (c.Verb)
{
case PathVerb.MoveTo:
{
if (!open)
{
renderer.BeginFigure();
open = true;
}
renderer.MoveTo(Vector2.Transform(c.EndPoint, layerXform));
break;
}
case PathVerb.LineTo:
{
renderer.LineTo(Vector2.Transform(c.EndPoint, layerXform));
break;
}
case PathVerb.QuadraticTo:
{
renderer.QuadraticBezierTo(
Vector2.Transform(c.ControlPoint1, layerXform),
Vector2.Transform(c.EndPoint, layerXform));
break;
}
case PathVerb.CubicTo:
{
renderer.CubicBezierTo(
Vector2.Transform(c.ControlPoint1, layerXform),
Vector2.Transform(c.ControlPoint2, layerXform),
Vector2.Transform(c.EndPoint, layerXform));
break;
}
case PathVerb.ArcTo:
{
// Adjust radii by the scale component of the transform;
// angle/sweep by the similarity component;
// endpoint is fully transformed.
float rx = c.RadiusX * layerXform.M11;
float ry = c.RadiusY * layerXform.M12;
float ang = c.RotationDegrees + sim.RotationDegrees;
bool sweep = sim.Reflection ? !c.Sweep : c.Sweep;
renderer.ArcTo(rx, ry, ang, c.LargeArc, sweep, Vector2.Transform(c.EndPoint, layerXform));
break;
}
case PathVerb.ClosePath:
{
if (open)
{
renderer.EndFigure();
open = false;
}
break;
}
}
}
if (open)
{
renderer.EndFigure();
}
renderer.EndLayer();
}
}
///
/// Converts a into device-space geometry for the target layer,
/// removing (baking in) any paint-local transforms. Geometry path commands have already
/// been transformed elsewhere; this method only resolves paint geometry (start/end points,
/// centers, radii, angles) into device space so the renderer can construct brushes directly.
///
/// Rules:
///
/// - UserSpaceOnUse: Apply in user space, then apply
/// to obtain device-space positions. Emit device-space values.
/// - ObjectBoundingBox: Apply in normalized [0..1] box space,
/// then denormalize to device space using . Emit device-space values.
/// - Color stops (ratios) remain normalized in [0..1] and are passed through unchanged.
/// - All returned paints have identity and are suitable for direct
/// consumption by Drawing brushes (e.g. LinearGradientBrush expects device-space points).
///
///
///
/// The source paint, or .
/// The device-space axis-aligned bounding box of the current layer’s geometry.
///
/// The full device-space transform applied to this layer’s geometry (e.g., layer * s2u * layout).
/// Used to push UserSpaceOnUse paints into device space. ObjectBoundingBox paints are denormalized
/// using instead.
///
///
/// A paint expressed in device-space with identity transform, or
/// if the input was .
///
private static Paint? TransformPaint(
Paint? paint,
in FontRectangle layerBounds,
Matrix3x2 layerXform)
{
if (paint is null)
{
return null;
}
switch (paint)
{
case SolidPaint s:
{
return s;
}
case LinearGradientPaint lg:
{
Vector2 p0;
Vector2 p1;
Vector2? p2;
if (lg.Units == GradientUnits.UserSpaceOnUse)
{
// USOU: transform directly to device space.
Matrix3x2 paintXForm = lg.Transform * layerXform;
p0 = Vector2.Transform(lg.P0, paintXForm);
p1 = Vector2.Transform(lg.P1, paintXForm);
p2 = lg.P2.HasValue ? Vector2.Transform(lg.P2.Value, paintXForm) : null;
}
else
{
// OBB: transform in normalized [0..1] space, then denormalize to device via layer bounds.
Vector2 n0 = Vector2.Transform(lg.P0, lg.Transform);
Vector2 n1 = Vector2.Transform(lg.P1, lg.Transform);
Vector2? n2 = lg.P2.HasValue ? Vector2.Transform(lg.P2.Value, lg.Transform) : null;
p0 = Vector2.Transform(DenormalizePoint(n0, layerBounds), layerXform);
p1 = Vector2.Transform(DenormalizePoint(n1, layerBounds), layerXform);
p2 = n2.HasValue ? Vector2.Transform(DenormalizePoint(n2.Value, layerBounds), layerXform) : null;
}
return new LinearGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
P0 = p0,
P1 = p1,
P2 = p2,
Spread = lg.Spread,
Stops = lg.Stops,
Opacity = lg.Opacity,
Transform = Matrix3x2.Identity
};
}
case RadialGradientPaint rg:
{
Vector2 c0;
Vector2 c1;
float r0;
float r1;
if (rg.Units == GradientUnits.UserSpaceOnUse)
{
// USOU: transform directly to device space.
Matrix3x2 paintXForm = rg.Transform * layerXform;
// Centers get full layer transform.
c0 = Vector2.Transform(rg.Center0, paintXForm);
c1 = Vector2.Transform(rg.Center1, paintXForm);
// Radii scale by uniform similarity only.
Similarity compSim = Similarity.FromMatrix(paintXForm);
r0 = rg.Radius0 * compSim.Scale;
r1 = rg.Radius1 * compSim.Scale;
}
else
{
// OBB: transform in normalized [0..1] space, then denormalize to device via layer bounds.
Vector2 nc0 = Vector2.Transform(rg.Center0, rg.Transform);
Vector2 nc1 = Vector2.Transform(rg.Center1, rg.Transform);
c0 = Vector2.Transform(DenormalizePoint(nc0, layerBounds), layerXform);
c1 = Vector2.Transform(DenormalizePoint(nc1, layerBounds), layerXform);
// Radii scale by total similarity (paint * layer).
Matrix3x2 paintXForm = rg.Transform * layerXform;
Similarity compSim = Similarity.FromMatrix(paintXForm);
r0 = rg.Radius0 * compSim.Scale;
r1 = rg.Radius1 * compSim.Scale;
}
return new RadialGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
Center0 = c0,
Radius0 = r0,
Center1 = c1,
Radius1 = r1,
Spread = rg.Spread,
Stops = rg.Stops,
Opacity = rg.Opacity,
Transform = Matrix3x2.Identity
};
}
case SweepGradientPaint sg:
{
Vector2 center;
float start = sg.StartAngle;
float end = sg.EndAngle;
if (sg.Units == GradientUnits.UserSpaceOnUse)
{
// USOU: transform directly to device space.
Matrix3x2 paintXForm = sg.Transform * layerXform;
// Center gets full layer transform.
center = Vector2.Transform(sg.Center, paintXForm);
// Angles adjust by similarity rotation and reflection only.
Similarity compSim = Similarity.FromMatrix(paintXForm);
start += compSim.RotationDegrees;
end += compSim.RotationDegrees;
if (compSim.Reflection)
{
(start, end) = (end, start);
}
}
else
{
// OBB: transform in normalized [0..1] space, then denormalize to device via layer bounds.
Vector2 nc = Vector2.Transform(sg.Center, sg.Transform);
center = Vector2.Transform(DenormalizePoint(nc, layerBounds), layerXform);
// Angles adjust by total similarity (paint * layer).
Matrix3x2 paintXForm = sg.Transform * layerXform;
Similarity compSim = Similarity.FromMatrix(paintXForm);
start += compSim.RotationDegrees;
end += compSim.RotationDegrees;
if (compSim.Reflection)
{
(start, end) = (end, start);
}
}
return new SweepGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
Center = center,
StartAngle = start,
EndAngle = end,
Spread = sg.Spread,
Stops = sg.Stops,
Opacity = sg.Opacity,
Transform = Matrix3x2.Identity
};
}
default:
{
return paint;
}
}
static Vector2 DenormalizePoint(Vector2 p, in FontRectangle bounds)
=> new(bounds.X + (p.X * bounds.Width), bounds.Y + (p.Y * bounds.Height));
}
///
/// Represents the similarity component of a 2D affine transformation.
///
///
/// A similarity transformation is an affine transform that preserves an object's shape and angles,
/// allowing only uniform scaling, rotation, and optional reflection. This structure isolates those
/// properties from a general so that dependent operations such as arc or
/// gradient adjustment can apply proportional transformations correctly.
///
private readonly struct Similarity
{
private Similarity(float scale, float rotationDeg, bool reflection, bool isSimilarity)
{
this.Scale = scale;
this.RotationDegrees = rotationDeg;
this.Reflection = reflection;
this.IsSimilarity = isSimilarity;
}
///
/// Gets the length of the first column.
///
public float Scale { get; }
///
/// Gets the rotation in degrees.
///
public float RotationDegrees { get; }
///
/// Gets a value indicating whether this matrix includes a reflection.
public bool Reflection { get; }
///
/// Gets a value indicating whether this matrix is a similarity transform.
/// True if columns are orthogonal and equal length within tolerance.
///
public bool IsSimilarity { get; }
public static Similarity FromMatrix(in Matrix3x2 m)
{
float a = m.M11, b = m.M12, c = m.M21, d = m.M22;
// scale = |X column|
float sx = MathF.Sqrt((a * a) + (b * b));
// rotation from X column
float rotDeg = MathF.Atan2(b, a) * (180f / MathF.PI);
// reflection from determinant
bool refl = ((a * d) - (b * c)) < 0f;
// similarity test: columns orthogonal and same length
float dot = (a * c) + (b * d);
float sy = MathF.Sqrt((c * c) + (d * d));
const float eps = 1e-4f;
bool ortho = MathF.Abs(dot) <= eps;
bool equal = MathF.Abs(sx - sy) <= eps;
return new Similarity(sx, rotDeg, refl, ortho && equal && sx > 0f);
}
}
}
}