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