Skip to content
Merged
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
7 changes: 4 additions & 3 deletions src/SixLabors.Fonts/Rendering/Paint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ public abstract class Paint
public float Opacity { get; init; } = 1f;

/// <summary>
/// Gets or sets an optional transform to apply to the paint.
/// Used to pre-apply gradientTransform in SVG or equivalent.
/// Gets the transform from the paint's own coordinate space to the space the glyph is
/// rendered in. Gradient geometry is defined in the paint's space, so a renderer maps each
/// sample through the inverse of this transform before evaluating the gradient.
/// </summary>
internal Matrix3x2 Transform { get; set; }
public Matrix3x2 Transform { get; init; } = Matrix3x2.Identity;

/// <summary>
/// Gets the composite mode to use when applying this paint over existing content.
Expand Down
143 changes: 50 additions & 93 deletions src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -398,20 +398,21 @@ private static void StreamCompositeCommands(
}

/// <summary>
/// Converts a <see cref="Paint"/> 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.
/// Resolves a <see cref="Paint"/> for the target layer. Geometry path commands have already
/// been transformed elsewhere; this method only resolves the paint geometry so the renderer
/// can construct brushes directly.
/// <para>
/// Rules:
/// <list type="bullet">
/// <item><description><b>UserSpaceOnUse</b>: Apply <see cref="Paint.Transform"/> in user space, then apply
/// <paramref name="layerXform"/> to obtain device-space positions. Emit device-space values.</description></item>
/// <item><description><b>ObjectBoundingBox</b>: Apply <see cref="Paint.Transform"/> in normalized [0..1] box space,
/// then denormalize to device space using <paramref name="layerBounds"/>. Emit device-space values.</description></item>
/// <item><description>Linear gradients: the points are transformed into device space, applying
/// <see cref="Paint.Transform"/> in user space or in normalized [0..1] box space first, then
/// <paramref name="layerXform"/>, with <paramref name="layerBounds"/> denormalizing box space.
/// The returned paint has an identity <see cref="Paint.Transform"/>.</description></item>
/// <item><description>Radial and sweep gradients: the centers, radii and angles stay in the
/// paint's own space, flipped to y-down, and the returned <see cref="Paint.Transform"/> maps
/// that space to device space. A skew or a non-uniform scale in the paint's transform
/// therefore reaches the renderer instead of being reduced to a circle.</description></item>
/// <item><description>Color stops (ratios) remain normalized in [0..1] and are passed through unchanged.</description></item>
/// <item><description>All returned paints have identity <see cref="Paint.Transform"/> and are suitable for direct
/// consumption by Drawing brushes (e.g. <c>LinearGradientBrush</c> expects device-space points).</description></item>
/// </list>
/// </para>
/// </summary>
Expand All @@ -423,7 +424,7 @@ private static void StreamCompositeCommands(
/// using <paramref name="layerBounds"/> instead.
/// </param>
/// <returns>
/// A paint expressed in <b>device-space</b> with identity transform, or <see langword="null"/>
/// A paint with <see cref="GradientUnits.UserSpaceOnUse"/> geometry, or <see langword="null"/>
/// if the input was <see langword="null"/>.
/// </returns>
private static Paint? TransformPaint(
Expand Down Expand Up @@ -485,107 +486,36 @@ private static void StreamCompositeCommands(

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,
Center0 = FlipY(rg.Center0),
Radius0 = rg.Radius0,
Center1 = FlipY(rg.Center1),
Radius1 = rg.Radius1,
Spread = rg.Spread,
Stops = rg.Stops,
Opacity = rg.Opacity,
CompositeMode = rg.CompositeMode,
Transform = Matrix3x2.Identity
Transform = GetPaintTransform(rg.Transform, rg.Units, in layerBounds, layerXform)
};
}

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

// The paint space is y-down like the renderer's, so the counter-clockwise angles
// keep their direction and the transform carries any rotation or reflection.
return new SweepGradientPaint
{
Units = GradientUnits.UserSpaceOnUse,
Center = center,
StartAngle = start,
EndAngle = end,
Center = FlipY(sg.Center),
StartAngle = sg.StartAngle,
EndAngle = sg.EndAngle,
Spread = sg.Spread,
Stops = sg.Stops,
Opacity = sg.Opacity,
CompositeMode = sg.CompositeMode,
Transform = Matrix3x2.Identity
Transform = GetPaintTransform(sg.Transform, sg.Units, in layerBounds, layerXform)
};
}

Expand All @@ -597,6 +527,33 @@ private static void StreamCompositeCommands(

static Vector2 DenormalizePoint(Vector2 p, in FontRectangle bounds)
=> new(bounds.X + (p.X * bounds.Width), bounds.Y + (p.Y * bounds.Height));

static Vector2 FlipY(Vector2 p) => new(p.X, -p.Y);
}

/// <summary>
/// Builds the transform from a gradient paint's y-down space to device space.
/// </summary>
/// <param name="paintTransform">The paint's own transform in its y-up space.</param>
/// <param name="units">The coordinate system of the paint geometry.</param>
/// <param name="layerBounds">The layer bounds that normalized geometry maps through.</param>
/// <param name="layerXform">The transform from the layer to device space.</param>
/// <returns>The transform from the paint's y-down space to device space.</returns>
private static Matrix3x2 GetPaintTransform(
Matrix3x2 paintTransform,
GradientUnits units,
in FontRectangle layerBounds,
Matrix3x2 layerXform)
{
// The flip is its own inverse: it takes the y-down paint geometry back to the y-up
// space the paint's own transform is defined in.
Matrix3x2 transform = Matrix3x2.CreateScale(1F, -1F) * paintTransform;
if (units == GradientUnits.ObjectBoundingBox)
{
transform *= Matrix3x2.CreateScale(layerBounds.Width, layerBounds.Height) * Matrix3x2.CreateTranslation(layerBounds.X, layerBounds.Y);
}

return transform * layerXform;
}

/// <summary>
Expand Down
10 changes: 9 additions & 1 deletion src/SixLabors.Fonts/ShapingBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2468,11 +2468,19 @@ private void CopyGlyphsMoved(int count)
/// Moves the pass position so that the given number of records sit on the
/// output side. Advancing streams records forward; rewinding returns produced
/// records to the input side ahead of the read cursor, exactly reversing the
/// stream.
/// stream. Outside a pass the sides are one array, so both cursors go
/// straight to the position.
/// </summary>
/// <param name="outputPosition">The output-side record count to move to.</param>
public void MoveTo(int outputPosition)
{
if (!this.IsPassActive)
{
this.ReadIndex = outputPosition;
this.PassOutputCount = outputPosition;
return;
}

if (this.PassOutputCount < outputPosition && this.ReadIndex < this.Count)
{
int count = Math.Min(outputPosition - this.PassOutputCount, this.Count - this.ReadIndex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ private static int FixupMatchPositions(Span<int> matchPositions, int matchCount,
/// <param name="matchPositions">The buffer positions the input sequence matched at.</param>
/// <param name="matchCount">The number of matched positions.</param>
/// <param name="count">The number of glyphs in the input sequence.</param>
/// <param name="matchEnd">The position one past the final matched input record.</param>
/// <returns><see langword="true"/> if the lookups were applied.</returns>
public static bool ApplyLookupList(
FontMetrics fontMetrics,
Expand All @@ -286,7 +287,8 @@ public static bool ApplyLookupList(
ShapingBuffer buffer,
ReadOnlySpan<int> matchPositions,
int matchCount,
int count)
int count,
int matchEnd)
{
if (buffer.NestingLimitReached)
{
Expand All @@ -296,6 +298,7 @@ public static bool ApplyLookupList(
// Positioning never changes the buffer's length, so the matched
// positions stand for the whole record list.
int startIndex = matchPositions[0];
int end = buffer.PassBacktrackLength + (matchEnd - buffer.ReadIndex);
buffer.PushNestedApplication();

foreach (SequenceLookupRecord lookupRecord in records)
Expand All @@ -312,6 +315,9 @@ public static bool ApplyLookupList(
}

buffer.PopNestedApplication();

// Everything the rule matched is now behind the cursor.
buffer.MoveTo(end);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,22 @@ internal sealed class LookupType2Format1SubTable : LookupSubTable
{
private readonly CoverageTable coverageTable;
private readonly PairSetTable[] pairSets;
private readonly ValueFormat valueFormat2;

/// <summary>
/// Initializes a new instance of the <see cref="LookupType2Format1SubTable"/> class.
/// </summary>
/// <param name="coverageTable">The coverage table.</param>
/// <param name="pairSets">The array of pair set tables.</param>
/// <param name="valueFormat2">The value format of the second glyph's record.</param>
/// <param name="lookupFlags">The lookup qualifiers.</param>
/// <param name="markFilteringSet">The mark filtering set index.</param>
public LookupType2Format1SubTable(CoverageTable coverageTable, PairSetTable[] pairSets, LookupFlags lookupFlags, ushort markFilteringSet)
public LookupType2Format1SubTable(CoverageTable coverageTable, PairSetTable[] pairSets, ValueFormat valueFormat2, LookupFlags lookupFlags, ushort markFilteringSet)
: base(lookupFlags, markFilteringSet)
{
this.coverageTable = coverageTable;
this.pairSets = pairSets;
this.valueFormat2 = valueFormat2;
}

/// <summary>
Expand Down Expand Up @@ -110,7 +113,7 @@ public static LookupType2Format1SubTable Load(BigEndianBinaryReader reader, long

CoverageTable coverageTable = CoverageTable.Load(reader, offset + coverageOffset);

return new LookupType2Format1SubTable(coverageTable, pairSets, lookupFlags, markFilteringSet);
return new LookupType2Format1SubTable(coverageTable, pairSets, valueFormat2, lookupFlags, markFilteringSet);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -165,6 +168,10 @@ public override bool TryUpdatePosition(
ValueRecord record2 = pairValueRecord.ValueRecord2;
AdvancedTypographicUtils.ApplyPosition(fontMetrics, buffer, secondIndex, record2, feature);

// With no second value record the second glyph is the next
// glyph a lookup is performed for; with one, the pair is
// closed and the walk resumes after it.
buffer.MoveTo(this.valueFormat2 == 0 ? secondIndex : secondIndex + 1);
return true;
}
}
Expand Down Expand Up @@ -262,6 +269,7 @@ internal sealed class LookupType2Format2SubTable : LookupSubTable
private readonly Class1Record[] class1Records;
private readonly ClassDefinitionTable classDefinitionTable1;
private readonly ClassDefinitionTable classDefinitionTable2;
private readonly ValueFormat valueFormat2;

/// <summary>
/// Initializes a new instance of the <see cref="LookupType2Format2SubTable"/> class.
Expand All @@ -270,13 +278,15 @@ internal sealed class LookupType2Format2SubTable : LookupSubTable
/// <param name="class1Records">The array of Class1 records.</param>
/// <param name="classDefinitionTable1">The class definition table for the first glyph.</param>
/// <param name="classDefinitionTable2">The class definition table for the second glyph.</param>
/// <param name="valueFormat2">The value format of the second glyph's record.</param>
/// <param name="lookupFlags">The lookup qualifiers.</param>
/// <param name="markFilteringSet">The mark filtering set index.</param>
public LookupType2Format2SubTable(
CoverageTable coverageTable,
Class1Record[] class1Records,
ClassDefinitionTable classDefinitionTable1,
ClassDefinitionTable classDefinitionTable2,
ValueFormat valueFormat2,
LookupFlags lookupFlags,
ushort markFilteringSet)
: base(lookupFlags, markFilteringSet)
Expand All @@ -285,6 +295,7 @@ public LookupType2Format2SubTable(
this.class1Records = class1Records;
this.classDefinitionTable1 = classDefinitionTable1;
this.classDefinitionTable2 = classDefinitionTable2;
this.valueFormat2 = valueFormat2;
}

/// <summary>
Expand Down Expand Up @@ -347,7 +358,7 @@ public static LookupType2Format2SubTable Load(BigEndianBinaryReader reader, long
ClassDefinitionTable classDefTable1 = ClassDefinitionTable.Load(reader, offset + classDef1Offset);
ClassDefinitionTable classDefTable2 = ClassDefinitionTable.Load(reader, offset + classDef2Offset);

return new LookupType2Format2SubTable(coverageTable, class1Records, classDefTable1, classDefTable2, lookupFlags, markFilteringSet);
return new LookupType2Format2SubTable(coverageTable, class1Records, classDefTable1, classDefTable2, valueFormat2, lookupFlags, markFilteringSet);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -405,6 +416,10 @@ public override bool TryUpdatePosition(
ValueRecord record2 = class2Record.ValueRecord2;
AdvancedTypographicUtils.ApplyPosition(fontMetrics, buffer, secondIndex, record2, feature);

// With no second value record the second glyph is the next glyph
// a lookup is performed for; with one, the pair is closed and the
// walk resumes after it.
buffer.MoveTo(this.valueFormat2 == 0 ? secondIndex : secondIndex + 1);
return true;
}

Expand Down
Loading
Loading