Skip to content

Commit 4a88695

Browse files
Merge path-text decorations into continuous bands
Refactors text-along-path decoration rendering to accumulate contiguous same-styled glyph cells into a single segment rather than emitting one rectangle per glyph. Path-following decorations are now emitted as sampled curve bands instead of per-glyph rotated rectangles, eliminating double anti-aliased edges at cell boundaries. - Moves path-transform logic from `RichTextGlyphRenderer`/`PathGlyphBuilder` into `BaseGlyphBuilder` via a new `TextPath` property and `ApplyPathTransform` method - Deletes `PathGlyphBuilder` (replaced by a new `GlyphBuilder(IPath)` constructor) - Adds `PendingSegment` accumulator to `DecorationLane` with contiguity merging - Adds `EmitPathDecorationBand` for curve-sampled decoration geometry - Extracts `RegisterDecorationPath` to share path registration between straight and curved emitters - Adds `DrawTextDecorated` benchmark and `RichTextGlyphRendererTests` unit tests - Updates reference image for path-text triangle test
1 parent 3239307 commit 4a88695

17 files changed

Lines changed: 481 additions & 157 deletions

src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,6 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa
5151
/// </summary>
5252
private readonly Brush? defaultBrush;
5353

54-
/// <summary>
55-
/// When the text is laid out along a path, this holds the path
56-
/// for point-along-path queries. <see langword="null"/> for normal (linear) text.
57-
/// </summary>
58-
private readonly IPath? path;
59-
6054
/// <summary>
6155
/// Set once <see cref="Dispose()"/> has run; guards against double disposal.
6256
/// </summary>
@@ -193,7 +187,7 @@ public RichTextGlyphRenderer(
193187
// so cache hits are vanishingly rare; disable caching entirely.
194188
this.rasterizationRequired = true;
195189
this.noCache = true;
196-
this.path = path;
190+
this.TextPath = path;
197191
}
198192
else if (!MatrixUtilities.IsAffine(drawingOptions.Transform))
199193
{
@@ -299,9 +293,6 @@ protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererPara
299293
}
300294
}
301295

302-
// Transform the glyph vectors using the original bounds
303-
// The default transform will automatically be applied.
304-
this.TransformGlyph(in bounds);
305296
this.rasterizationRequired = true;
306297
return true;
307298
}
@@ -730,50 +721,6 @@ private void UpdateCache(GlyphRenderData renderData)
730721
[MethodImpl(MethodImplOptions.AggressiveInlining)]
731722
private static Point ClampToPixel(PointF point) => Point.Truncate(point);
732723

733-
/// <summary>
734-
/// Applies the path-based transform to the <see cref="BaseGlyphBuilder.Builder"/>
735-
/// for the current glyph, positioning it along the text path (if any) or
736-
/// leaving the identity transform for linear text.
737-
/// </summary>
738-
/// <param name="bounds">The font-metric bounding rectangle of the glyph.</param>
739-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
740-
private void TransformGlyph(in FontRectangle bounds)
741-
=> this.Builder.SetTransform(this.ComputeTransform(in bounds));
742-
743-
/// <summary>
744-
/// Computes the combined translation + rotation matrix that places a glyph
745-
/// along the text path. For linear text (no path), returns <see cref="Matrix4x4.Identity"/>.
746-
/// </summary>
747-
/// <param name="bounds">The font-metric bounding rectangle of the glyph.</param>
748-
/// <returns>
749-
/// The glyph placement matrix.
750-
/// </returns>
751-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
752-
private Matrix4x4 ComputeTransform(in FontRectangle bounds)
753-
{
754-
if (this.path is null)
755-
{
756-
return Matrix4x4.Identity;
757-
}
758-
759-
// Find the point of this intersection along the given path.
760-
// We want to find the point on the path that is closest to the center-bottom side of the glyph.
761-
// Aligned text can overflow the path on either side, so overflowing glyphs extrapolate
762-
// along the boundary tangents.
763-
Vector2 half = new(bounds.Width * .5F, 0);
764-
if (!this.path.TryGetPathPointAtDistanceUnbounded(bounds.Left + half.X, out PathPoint pathPoint))
765-
{
766-
return Matrix4x4.Identity;
767-
}
768-
769-
float angle = GeometryUtilities.DegreeToRadian(pathPoint.Angle);
770-
771-
// Now offset to our target point since we're aligning the top-left location of our glyph against the path.
772-
Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top);
773-
return Matrix4x4.CreateTranslation(translation.X, translation.Y, 0)
774-
* new Matrix4x4(Matrix3x2.CreateRotation(angle, (Vector2)pathPoint.Point));
775-
}
776-
777724
/// <summary>
778725
/// Releases managed resources owned by this renderer.
779726
/// </summary>

0 commit comments

Comments
 (0)