Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/SixLabors.Fonts/Rendering/PaintedCanvasMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal readonly struct PaintedCanvasMetadata
/// </summary>
/// <param name="viewBox">The viewBox rectangle (minX, minY, width, height).</param>
/// <param name="isYDown">True if the source coordinate system is y-down; false if y-up.</param>
/// <param name="rootTransform">An optional root transform in document-space.</param>
/// <param name="rootTransform">An optional root transform, applied after the viewBox mapping and before any Y-flip.</param>
public PaintedCanvasMetadata(FontRectangle viewBox, bool isYDown, Matrix3x2 rootTransform)
{
this.HasViewBox = viewBox != FontRectangle.Empty;
Expand All @@ -40,7 +40,9 @@ public PaintedCanvasMetadata(FontRectangle viewBox, bool isYDown, Matrix3x2 root
public bool IsYDown { get; }

/// <summary>
/// Gets the root transform in document-space.
/// Gets the root transform. It applies in the space the viewBox maps into (font units, in the
/// source's Y direction), after the viewBox mapping and before any Y-flip. Layer transforms
/// do not include it.
/// </summary>
public Matrix3x2 RootTransform { get; }
}
34 changes: 24 additions & 10 deletions src/SixLabors.Fonts/Rendering/PaintedGlyph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public PaintedGlyph(List<PaintedLayer> layers, List<PaintedCompositeCommand>? co
this.Layers = layers;
this.CompositeCommands = compositeCommands;
this.ClipBounds = clipBounds;
this.Bounds = CalculateBounds(layers);
this.Bounds = CalculateBounds(layers, Matrix3x2.Identity);
}

/// <summary>
Expand Down Expand Up @@ -68,11 +68,22 @@ public PaintedGlyph(List<PaintedLayer> layers, List<PaintedCompositeCommand>? co
public static PaintedGlyph Empty => new([]);

/// <summary>
/// Calculates the bounds of all painted geometry after applying each layer transform.
/// Calculates the bounds of all painted geometry after applying each layer transform followed by
/// <paramref name="transform"/>. Unlike transforming <see cref="Bounds"/>, this stays tight when
/// <paramref name="transform"/> rotates or skews, at the cost of a pass over all geometry.
/// </summary>
/// <param name="transform">The transform applied after each layer transform.</param>
/// <returns>The calculated bounds.</returns>
public Bounds GetBounds(Matrix3x2 transform) => CalculateBounds(this.Layers, transform);

/// <summary>
/// Calculates the bounds of all painted geometry after applying each layer transform followed by
/// <paramref name="transform"/>.
/// </summary>
/// <param name="layers">The painted layers.</param>
/// <param name="transform">The transform applied after each layer transform.</param>
/// <returns>The calculated bounds.</returns>
private static Bounds CalculateBounds(List<PaintedLayer> layers)
private static Bounds CalculateBounds(IReadOnlyList<PaintedLayer> layers, Matrix3x2 transform)
{
Vector2 min = new(float.MaxValue);
Vector2 max = new(float.MinValue);
Expand All @@ -81,6 +92,9 @@ private static Bounds CalculateBounds(List<PaintedLayer> layers)
for (int i = 0; i < layers.Count; i++)
{
PaintedLayer layer = layers[i];

// Matrix3x2 transforms row vectors, so the layer transform applies first.
Matrix3x2 layerTransform = layer.Transform * transform;
IReadOnlyList<PathCommand> path = layer.Path;
Vector2 current = default;

Expand All @@ -91,19 +105,19 @@ private static Bounds CalculateBounds(List<PaintedLayer> layers)
{
case PathVerb.MoveTo:
case PathVerb.LineTo:
Include(command.EndPoint, layer.Transform, ref min, ref max, ref hasPoint);
Include(command.EndPoint, layerTransform, ref min, ref max, ref hasPoint);
break;
case PathVerb.QuadraticTo:
Include(command.ControlPoint1, layer.Transform, ref min, ref max, ref hasPoint);
Include(command.EndPoint, layer.Transform, ref min, ref max, ref hasPoint);
Include(command.ControlPoint1, layerTransform, ref min, ref max, ref hasPoint);
Include(command.EndPoint, layerTransform, ref min, ref max, ref hasPoint);
break;
case PathVerb.CubicTo:
Include(command.ControlPoint1, layer.Transform, ref min, ref max, ref hasPoint);
Include(command.ControlPoint2, layer.Transform, ref min, ref max, ref hasPoint);
Include(command.EndPoint, layer.Transform, ref min, ref max, ref hasPoint);
Include(command.ControlPoint1, layerTransform, ref min, ref max, ref hasPoint);
Include(command.ControlPoint2, layerTransform, ref min, ref max, ref hasPoint);
Include(command.EndPoint, layerTransform, ref min, ref max, ref hasPoint);
break;
case PathVerb.ArcTo:
IncludeArc(current, command, layer.Transform, ref min, ref max, ref hasPoint);
IncludeArc(current, command, layerTransform, ref min, ref max, ref hasPoint);
break;
}

Expand Down
63 changes: 53 additions & 10 deletions src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.

using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.Fonts.Unicode;

namespace SixLabors.Fonts.Rendering;
Expand All @@ -15,6 +16,12 @@ public sealed class PaintedGlyphMetrics : FontGlyphMetrics
{
private readonly IPaintedGlyphSource source;

/// <summary>
/// The design bounds measured from the glyph's geometry when the source-to-UPEM mapping
/// rotates or skews, or <see langword="null"/> until first requested.
/// </summary>
private StrongBox<Bounds>? rotatedDesignBounds;

/// <summary>
/// Initializes a new instance of the <see cref="PaintedGlyphMetrics"/> class.
/// </summary>
Expand Down Expand Up @@ -79,7 +86,40 @@ internal override Bounds GetDesignBounds()
}

Matrix3x2 sourceToUpem = ComputeSourceToUpem(canvas, this.UnitsPerEm);
return Bounds.Transform(glyph.Bounds, sourceToUpem);

// Transforming the cached box is exact while the mapping keeps the axes aligned (scale,
// translation, Y-flip, or a quarter-turn that swaps the axes), because the image of an
// axis-aligned box is then itself a box. A rotation or skew, which only a root transform
// introduces, would inflate the box around the rotated corners, so the geometry is
// measured under the full mapping instead.
if (IsAxisAligned(in sourceToUpem))
{
return Bounds.Transform(glyph.Bounds, sourceToUpem);
}

// The glyph and its canvas are fixed for this instance, so the geometry pass runs once.
// A reference write is atomic, so concurrent callers at worst compute the same value twice.
StrongBox<Bounds>? cached = this.rotatedDesignBounds;
if (cached is null)
{
cached = new StrongBox<Bounds>(glyph.GetBounds(sourceToUpem));
this.rotatedDesignBounds = cached;
}

return cached.Value;
}

/// <summary>
/// Returns whether a transform maps axis-aligned boxes to axis-aligned boxes, allowing for the
/// rounding noise that trigonometric construction leaves in the zero terms.
/// </summary>
/// <param name="m">The transform to test.</param>
/// <returns><see langword="true"/> if the transform keeps or swaps the axes.</returns>
private static bool IsAxisAligned(in Matrix3x2 m)
{
float eps = 1e-5F * (MathF.Abs(m.M11) + MathF.Abs(m.M12) + MathF.Abs(m.M21) + MathF.Abs(m.M22));
return (MathF.Abs(m.M12) <= eps && MathF.Abs(m.M21) <= eps)
|| (MathF.Abs(m.M11) <= eps && MathF.Abs(m.M22) <= eps);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -160,25 +200,20 @@ private Matrix3x2 ComputeTotalTransform(
layout *= Matrix3x2.CreateScale(1F, -1F);
layout.Translation += glyphOrigin;

// Source-to-UPEM: viewBox mapping (uniform "meet"), optional y-flip, optional root transform.
// Source-to-UPEM: viewBox mapping (uniform "meet"), optional root transform, optional y-flip.
return ComputeSourceToUpem(canvas, this.UnitsPerEm) * layout;
}

/// <summary>
/// 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 normalizes
/// Y-down document coordinates to the Y-up glyph-metrics coordinate system.
/// Enforces a uniform 'meet' scale from the root viewBox (if present), then applies the
/// root transform in the space the viewBox maps into, and finally normalizes Y-down
/// document coordinates to the Y-up glyph-metrics coordinate system.
/// </summary>
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)
{
Expand All @@ -193,6 +228,14 @@ private static Matrix3x2 ComputeSourceToUpem(in PaintedCanvasMetadata canvas, us
m = m * t * sUni;
}

// Root transform. SVG 2 applies a transform on an <svg> element as if it were set on a
// parent element, so it acts in the parent (UPEM) space after the viewBox mapping.
// Matrix3x2 transforms row vectors, so appending it here makes it apply after the viewBox.
if (!canvas.RootTransform.IsIdentity)
{
m *= canvas.RootTransform;
}

// Normalize every painted source to the Y-up font-space contract used by glyph metrics.
if (canvas.IsYDown)
{
Expand Down
Loading
Loading