1515
1616namespace SixLabors . ImageSharp . Drawing . Processing . Processors . Text
1717{
18- // TODO: Fix path rendering and add caching.
19- internal sealed class RichTextGlyphRenderer : GlyphBuilder , IColorGlyphRenderer , IDisposable
18+ // TODO: Add caching.
19+ internal sealed class RichTextGlyphRenderer : BaseGlyphBuilder , IColorGlyphRenderer , IDisposable
2020 {
21+ private const byte RenderOrderFill = 0 ;
22+ private const byte RenderOrderOutline = 1 ;
23+ private const byte RenderOrderDecoration = 2 ;
24+
2125 private readonly TextDrawingOptions textOptions ;
2226 private readonly DrawingOptions drawingOptions ;
2327 private readonly MemoryAllocator memoryAllocator ;
2428 private readonly Pen defaultPen ;
2529 private readonly Brush defaultBrush ;
2630 private readonly IPathInternals path ;
27- private Vector2 textOffset ;
31+ private Vector2 textPathOffset ;
2832 private bool isDisposed ;
2933
3034 private readonly Dictionary < Color , Brush > brushLookup = new ( ) ;
@@ -35,25 +39,24 @@ internal sealed class RichTextGlyphRenderer : GlyphBuilder, IColorGlyphRenderer,
3539 private TextDecorationDetails ? currentUnderline ;
3640 private TextDecorationDetails ? currentStrikout ;
3741 private TextDecorationDetails ? currentOverline ;
38- private Point currentRenderPosition ;
39- private Matrix3x2 currentTransform ;
4042
4143 public RichTextGlyphRenderer (
4244 TextDrawingOptions textOptions ,
4345 DrawingOptions drawingOptions ,
4446 MemoryAllocator memoryAllocator ,
4547 Pen pen ,
4648 Brush brush )
47- : base ( textOptions . Origin )
4849 {
4950 this . textOptions = textOptions ;
5051 this . drawingOptions = drawingOptions ;
51- this . currentTransform = this . drawingOptions . Transform ;
5252 this . memoryAllocator = memoryAllocator ;
5353 this . defaultPen = pen ;
5454 this . defaultBrush = brush ;
5555 this . DrawingOperations = new List < DrawingOperation > ( ) ;
5656
57+ // Set the default transform.
58+ this . Builder . SetTransform ( drawingOptions . Transform ) ;
59+
5760 IPath path = textOptions . Path ;
5861 if ( path is not null )
5962 {
@@ -95,7 +98,7 @@ protected override void BeginText(in FontRectangle bounds)
9598 HorizontalAlignment . Left => bounds . Left ,
9699 _ => bounds . Left ,
97100 } ;
98- this . textOffset = new ( xOffset , yOffset ) ;
101+ this . textPathOffset = new ( xOffset , yOffset ) ;
99102 }
100103
101104 /// <inheritdoc/>
@@ -272,21 +275,21 @@ protected override void EndGlyph()
272275 {
273276 this . DrawingOperations . Add ( new DrawingOperation
274277 {
275- Location = this . currentRenderPosition ,
278+ Location = Point . Truncate ( path . Bounds . Location ) ,
276279 Map = renderData . FillMap ,
277280 Brush = this . currentBrush ,
278- RenderPass = 1
281+ RenderPass = RenderOrderFill
279282 } ) ;
280283 }
281284
282285 if ( renderData . OutlineMap != null )
283286 {
284287 this . DrawingOperations . Add ( new DrawingOperation
285288 {
286- Location = this . currentRenderPosition ,
289+ Location = Point . Truncate ( path . Bounds . Location ) ,
287290 Map = renderData . OutlineMap ,
288291 Brush = this . currentPen ? . StrokeFill ?? this . currentBrush ,
289- RenderPass = 2 // Render outlines 2nd to ensure they are always on top of fills
292+ RenderPass = RenderOrderOutline
290293 } ) ;
291294 }
292295 }
@@ -305,13 +308,18 @@ private void FinalizeDecoration(ref TextDecorationDetails? decoration)
305308 {
306309 if ( decoration != null )
307310 {
308- // TODO: If the path is curved ths does not work well.
311+ // TODO: If the path is curved a line segment does not work well.
309312 // What would be great would be if we could take a slice of a path given an offset and length.
310313 IPath path = new Path ( new LinearLineSegment ( decoration . Value . Start , decoration . Value . End ) ) ;
311- path = path . Transform ( this . currentTransform ) ;
312-
313314 IPath outline = decoration . Value . Pen . GeneratePath ( path , decoration . Value . Thickness ) ;
314315
316+ // Calculate the transform for this path.
317+ // We cannot use the pathbuilder transform as this path is rendered independently.
318+ FontRectangle rectangle = new ( outline . Bounds . Location , new ( outline . Bounds . Width , outline . Bounds . Height ) ) ;
319+ Matrix3x2 pathTransform = this . ComputeTransform ( in rectangle ) ;
320+ Matrix3x2 defaultTransform = this . drawingOptions . Transform ;
321+ outline = outline . Transform ( pathTransform * defaultTransform ) ;
322+
315323 if ( outline . Bounds . Width != 0 && outline . Bounds . Height != 0 )
316324 {
317325 // Render the Path here
@@ -320,7 +328,7 @@ private void FinalizeDecoration(ref TextDecorationDetails? decoration)
320328 Brush = decoration . Value . Pen . StrokeFill ,
321329 Location = Point . Truncate ( outline . Bounds . Location ) ,
322330 Map = this . Render ( outline ) ,
323- RenderPass = 3 // after outlines !!
331+ RenderPass = RenderOrderDecoration
324332 } ) ;
325333 }
326334
@@ -336,15 +344,15 @@ private void AppendDecoration(ref TextDecorationDetails? decoration, Vector2 sta
336344 if ( this . path is not null )
337345 {
338346 // Let's try and expand it first.
339- if ( thickness == decoration . Value . Thickness &&
340- decoration . Value . End . Y == start . Y &&
341- ( decoration . Value . End . X + 1 ) >= start . X &&
342- decoration . Value . Pen . Equals ( pen ) )
347+ if ( thickness == decoration . Value . Thickness
348+ && decoration . Value . End . Y == start . Y
349+ && ( decoration . Value . End . X + 1 ) >= start . X
350+ && decoration . Value . Pen . Equals ( pen ) )
343351 {
344352 // Expand the line
345353 start = decoration . Value . Start ;
346354
347- // If this is null finalize does nothing we then set it again before we leave
355+ // If this is null finalize does nothing.
348356 decoration = null ;
349357 }
350358 }
@@ -363,14 +371,22 @@ private void AppendDecoration(ref TextDecorationDetails? decoration, Vector2 sta
363371 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
364372 private void TransformGlyph ( in FontRectangle bounds )
365373 {
366- // We don't have access to the pathbuilder transform here so we recreate it from the origin.
367- var origin = Matrix3x2 . CreateTranslation ( this . textOptions . Origin ) ;
368374 if ( this . path is null )
369375 {
370- this . currentRenderPosition = Point . Truncate ( PointF . Transform ( bounds . Location , origin ) ) ;
371376 return ;
372377 }
373378
379+ this . Builder . SetTransform ( this . ComputeTransform ( in bounds ) ) ;
380+ }
381+
382+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
383+ private Matrix3x2 ComputeTransform ( in FontRectangle bounds )
384+ {
385+ if ( this . path is null )
386+ {
387+ return Matrix3x2 . Identity ;
388+ }
389+
374390 // Find the intersection point.
375391 // This should be offset to ensure we rotate at the bottom-center of the glyph.
376392 float halfWidth = bounds . Width * .5F ;
@@ -383,18 +399,11 @@ private void TransformGlyph(in FontRectangle bounds)
383399 // characters in multiline text scales with the angle and vertical offset.
384400 // This is expected and consistant with other libraries.
385401 // Multiple line text should be rendered using multiple paths to avoid this behavior.
386- Vector2 targetPoint = ( Vector2 ) pathPoint . Point + new Vector2 ( - halfWidth , bounds . Top ) - bounds . Location - this . textOffset ;
402+ Vector2 targetPoint = ( Vector2 ) pathPoint . Point + new Vector2 ( - halfWidth , bounds . Top ) - bounds . Location - this . textPathOffset ;
387403
388404 // Due to how matrix combining works you have to combine this in the reverse order of operation.
389- this . currentTransform = Matrix3x2 . CreateTranslation ( targetPoint )
390- * Matrix3x2 . CreateRotation ( pathPoint . Angle - MathF . PI , pathPoint . Point )
391- * this . drawingOptions . Transform ;
392-
393- // TODO: This is incorrect. It looks like width/height depends on the rotation.
394- this . currentRenderPosition = Point . Truncate (
395- PointF . Transform ( new ( bounds . Left + halfWidth , bounds . Bottom ) , this . currentTransform * origin ) ) ;
396-
397- this . Builder . SetTransform ( this . currentTransform ) ;
405+ return Matrix3x2 . CreateTranslation ( targetPoint )
406+ * Matrix3x2 . CreateRotation ( pathPoint . Angle - MathF . PI , pathPoint . Point ) ;
398407 }
399408
400409 private Buffer2D < float > Render ( IPath path )
0 commit comments