Skip to content

Commit ab96c2e

Browse files
RichTextGlyphRenderer works!
1 parent deeef2e commit ab96c2e

6 files changed

Lines changed: 74 additions & 76 deletions

File tree

src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public DrawTextProcessor(DrawingOptions drawingOptions, TextDrawingOptions textO
4646
public DrawingOptions DrawingOptions { get; }
4747

4848
/// <summary>
49-
/// Gets the <see cref="Processing.TextDrawingOptions"/> defining text-specific drawing settings.
49+
/// Gets the <see cref="TextDrawingOptions"/> defining text-specific drawing settings.
5050
/// </summary>
5151
public TextDrawingOptions TextOptions { get; }
5252

src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Numerics;
78
using SixLabors.Fonts;
8-
using SixLabors.Fonts.Unicode;
9-
using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing;
109
using SixLabors.ImageSharp.Memory;
1110
using SixLabors.ImageSharp.PixelFormats;
1211
using SixLabors.ImageSharp.Processing.Processors;
@@ -32,43 +31,24 @@ protected override void BeforeImageApply()
3231
{
3332
base.BeforeImageApply();
3433

35-
// Do everything at the image level as we are delegating the processing down to other processors
36-
//this.textRenderer = new CachingGlyphRenderer(
37-
// this.Configuration.MemoryAllocator,
38-
// this.definition.Text.GetGraphemeCount(),
39-
// this.definition.TextOptions,
40-
// this.definition.Pen,
41-
// this.definition.Brush,
42-
// this.definition.DrawingOptions.Transform)
43-
//{
44-
// Options = this.definition.DrawingOptions
45-
//};
34+
// Do everything at the image level as we are delegating
35+
// the processing down to other processors
36+
TextDrawingOptions textOptions = ConfigureOptions(this.definition.TextOptions);
4637

4738
this.textRenderer = new RichTextGlyphRenderer(
48-
this.definition.TextOptions,
39+
textOptions,
4940
this.definition.DrawingOptions,
5041
this.Configuration.MemoryAllocator,
5142
this.definition.Pen,
5243
this.definition.Brush);
5344

5445
TextRenderer renderer = new(this.textRenderer);
55-
renderer.RenderText(this.definition.Text, this.definition.TextOptions);
46+
renderer.RenderText(this.definition.Text, textOptions);
5647
}
5748

5849
protected override void AfterImageApply()
5950
{
6051
base.AfterImageApply();
61-
62-
foreach (var path in this.textRenderer.Paths)
63-
{
64-
new FillPathProcessor(
65-
this.definition.DrawingOptions,
66-
new SolidBrush(Color.HotPink.WithAlpha(.5F)),
67-
path).Execute(this.Configuration, this.Source, this.SourceRectangle);
68-
}
69-
70-
71-
7252
this.textRenderer?.Dispose();
7353
this.textRenderer = null;
7454
}
@@ -135,5 +115,20 @@ void Draw(IEnumerable<DrawingOperation> operations)
135115
Draw(this.textRenderer.DrawingOperations.OrderBy(x => x.RenderPass));
136116
}
137117
}
118+
119+
private static TextDrawingOptions ConfigureOptions(TextDrawingOptions options)
120+
{
121+
// When a path is specified we should explicitly follow that path
122+
// and not adjust the origin. Any tranlation should be applied to the path.
123+
if (options.Path is not null && options.Origin != Vector2.Zero)
124+
{
125+
return new(options)
126+
{
127+
Origin = Vector2.Zero
128+
};
129+
}
130+
131+
return options;
132+
}
138133
}
139134
}

src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@
1515

1616
namespace 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)

src/ImageSharp.Drawing/Processing/TextDrawingOptions.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ public TextDrawingOptions(Font font)
2727
/// <param name="options">The options whose properties are copied into this instance.</param>
2828
public TextDrawingOptions(TextDrawingOptions options)
2929
: base(options)
30-
{
31-
}
30+
=> this.Path = options.Path;
3231

3332
/// <summary>
3433
/// Gets or sets an optional collection of text runs to apply to the body of text.

src/ImageSharp.Drawing/Shapes/PathBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ public PathBuilder(Matrix3x2 defaultTransform)
4141
/// <summary>
4242
/// Sets the translation to be applied to all items to follow being applied to the <see cref="PathBuilder"/>.
4343
/// </summary>
44-
/// <param name="translation">The translation.</param>
44+
/// <param name="transform">The transform.</param>
4545
/// <returns>The <see cref="PathBuilder"/>.</returns>
46-
public PathBuilder SetTransform(Matrix3x2 translation)
46+
public PathBuilder SetTransform(Matrix3x2 transform)
4747
{
48-
this.setTransform = translation;
48+
this.setTransform = transform;
4949
this.currentTransform = this.setTransform * this.defaultTransform;
5050
return this;
5151
}
@@ -57,7 +57,7 @@ public PathBuilder SetTransform(Matrix3x2 translation)
5757
/// <returns>The <see cref="PathBuilder"/>.</returns>
5858
public PathBuilder SetOrigin(PointF origin)
5959
{
60-
// the new origin should be transformed based on the default transform
60+
// The new origin should be transformed based on the default transform
6161
this.setTransform.Translation = origin;
6262
this.currentTransform = this.setTransform * this.defaultTransform;
6363

tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -587,15 +587,15 @@ public void DrawRichTextArabic<TPixel>(
587587
}
588588

589589
[Theory]
590-
[WithSolidFilledImages(1000, 1000, nameof(Color.Black), PixelTypes.Rgba32, 32)]
590+
[WithSolidFilledImages(500, 200, nameof(Color.Black), PixelTypes.Rgba32, 32)]
591591
[WithSolidFilledImages(500, 300, nameof(Color.Black), PixelTypes.Rgba32, 40)]
592592
public void DrawRichTextRainbow<TPixel>(
593593
TestImageProvider<TPixel> provider,
594594
int fontSize)
595595
where TPixel : unmanaged, IPixel<TPixel>
596596
{
597597
Font font = CreateFont(TestFonts.OpenSans, fontSize);
598-
const string text = "abcdefg";//"The quick brown fox jumps over the lazy dog";
598+
const string text = "The quick brown fox jumps over the lazy dog";
599599

600600
SolidPen[] colors = new[]
601601
{
@@ -616,20 +616,15 @@ public void DrawRichTextRainbow<TPixel>(
616616
{
617617
Start = i,
618618
End = i + 1,
619-
StrikeoutPen = pen
619+
UnderlinePen = pen
620620
});
621621
}
622622

623-
string svgPath = "M275 175 A100 100 0 1 1 275 174";
624-
bool parsed = Path.TryParseSvgPath(svgPath, out IPath path);
625-
Assert.True(parsed);
626-
627623
TextDrawingOptions textOptions = new(font)
628624
{
629-
//Origin = new Vector2(-100),
625+
Origin = new Vector2(15),
630626
WrappingLength = 400,
631627
TextRuns = runs,
632-
Path = path
633628
};
634629

635630
provider.RunValidatingProcessorTest(

0 commit comments

Comments
 (0)