Skip to content

Commit adce707

Browse files
committed
normalize brush use
1 parent 6e61579 commit adce707

2 files changed

Lines changed: 121 additions & 95 deletions

File tree

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

Lines changed: 92 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Numerics;
78
using SixLabors.Fonts;
89
using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing;
@@ -57,98 +58,86 @@ protected override void AfterImageApply()
5758
/// <inheritdoc/>
5859
protected override void OnFrameApply(ImageFrame<TPixel> source)
5960
{
60-
Draw(this.textRenderer.FillOperations, this.definition.Brush);
61-
Draw(this.textRenderer.OutlineOperations, this.definition.Pen?.StrokeFill);
61+
if (this.textRenderer.DrawingOperations.Count > 0)
62+
{
63+
Draw(this.textRenderer.DrawingOperations.OrderBy(x => x.RenderPass));
64+
}
6265

63-
void Draw(List<DrawingOperation> operations, IBrush brush)
66+
void Draw(IEnumerable<DrawingOperation> operations)
6467
{
65-
if (operations?.Count > 0)
68+
var brushes = new Dictionary<IBrush, BrushApplicator<TPixel>>();
69+
foreach (DrawingOperation operation in operations)
6670
{
67-
var brushes = new Dictionary<Color, BrushApplicator<TPixel>>();
68-
foreach (DrawingOperation operation in operations)
71+
if (operation.Brush != null)
6972
{
70-
if (operation.Color.HasValue)
73+
if (!brushes.TryGetValue(operation.Brush, out _))
7174
{
72-
if (!brushes.TryGetValue(operation.Color.Value, out _))
73-
{
74-
brushes[operation.Color.Value] = new SolidBrush(operation.Color.Value).CreateApplicator(
75-
this.Configuration,
76-
this.textRenderer.Options.GraphicsOptions,
77-
source,
78-
this.SourceRectangle);
79-
}
75+
brushes[operation.Brush] = operation.Brush.CreateApplicator(this.Configuration, this.textRenderer.Options.GraphicsOptions, source, this.SourceRectangle);
8076
}
8177
}
78+
}
8279

83-
using (BrushApplicator<TPixel> app = brush.CreateApplicator(this.Configuration, this.textRenderer.Options.GraphicsOptions, source, this.SourceRectangle))
84-
{
85-
foreach (DrawingOperation operation in operations)
86-
{
87-
BrushApplicator<TPixel> currentApp = app;
88-
if (operation.Color != null)
89-
{
90-
brushes.TryGetValue(operation.Color.Value, out currentApp);
91-
}
92-
93-
Buffer2D<float> buffer = operation.Map;
94-
int startY = operation.Location.Y;
95-
int startX = operation.Location.X;
96-
int offsetSpan = 0;
97-
98-
if (startX + buffer.Height < 0)
99-
{
100-
continue;
101-
}
80+
foreach (DrawingOperation operation in operations)
81+
{
82+
var app = brushes[operation.Brush];
10283

103-
if (startX + buffer.Width < 0)
104-
{
105-
continue;
106-
}
84+
Buffer2D<float> buffer = operation.Map;
85+
int startY = operation.Location.Y;
86+
int startX = operation.Location.X;
87+
int offsetSpan = 0;
10788

108-
if (startX < 0)
109-
{
110-
offsetSpan = -startX;
111-
startX = 0;
112-
}
89+
if (startX + buffer.Height < 0)
90+
{
91+
continue;
92+
}
11393

114-
if (startX >= source.Width)
115-
{
116-
continue;
117-
}
94+
if (startX + buffer.Width < 0)
95+
{
96+
continue;
97+
}
11898

119-
int firstRow = 0;
120-
if (startY < 0)
121-
{
122-
firstRow = -startY;
123-
}
99+
if (startX < 0)
100+
{
101+
offsetSpan = -startX;
102+
startX = 0;
103+
}
124104

125-
int maxHeight = source.Height - startY;
126-
int end = Math.Min(operation.Map.Height, maxHeight);
105+
if (startX >= source.Width)
106+
{
107+
continue;
108+
}
127109

128-
for (int row = firstRow; row < end; row++)
129-
{
130-
int y = startY + row;
131-
Span<float> span = buffer.DangerousGetRowSpan(row).Slice(offsetSpan);
132-
currentApp.Apply(span, startX, y);
133-
}
134-
}
110+
int firstRow = 0;
111+
if (startY < 0)
112+
{
113+
firstRow = -startY;
135114
}
136115

137-
foreach (BrushApplicator<TPixel> app in brushes.Values)
116+
int maxHeight = source.Height - startY;
117+
int end = Math.Min(operation.Map.Height, maxHeight);
118+
119+
for (int row = firstRow; row < end; row++)
138120
{
139-
app.Dispose();
121+
int y = startY + row;
122+
Span<float> span = buffer.DangerousGetRowSpan(row).Slice(offsetSpan);
123+
app.Apply(span, startX, y);
140124
}
141125
}
126+
127+
foreach (BrushApplicator<TPixel> app in brushes.Values)
128+
{
129+
app.Dispose();
130+
}
142131
}
143132
}
144133

145134
private struct DrawingOperation
146135
{
147136
public Buffer2D<float> Map { get; set; }
148137

149-
public Point Location { get; set; }
138+
public byte RenderPass { get; set; }
150139

151-
public Color? Color { get; set; }
140+
public Point Location { get; set; }
152141

153142
public IBrush Brush { get; internal set; }
154143
}
@@ -163,14 +152,15 @@ private class CachingGlyphRenderer : IColorGlyphRenderer, IDisposable
163152
private const float AccuracyMultiple = 8;
164153
private readonly Matrix3x2 transform;
165154
private readonly PathBuilder builder;
155+
private readonly Dictionary<Color, IBrush> brushLookup = new();
166156

167157
private Point currentRenderPosition;
168158
private (GlyphRendererParameters Glyph, PointF SubPixelOffset) currentGlyphRenderParams;
169159
private readonly int offset;
170160
private PointF currentPoint;
171161
private Color? currentColor;
172-
private IBrush? currentBrush;
173-
private IPen? currentPen;
162+
private IBrush currentBrush;
163+
private IPen currentPen;
174164

175165
private readonly Dictionary<(GlyphRendererParameters Glyph, PointF SubPixelOffset), GlyphRenderData> glyphData = new();
176166

@@ -183,15 +173,12 @@ public CachingGlyphRenderer(MemoryAllocator memoryAllocator, int size, TextDrawi
183173
this.Pen = pen;
184174
this.Brush = brush;
185175
this.offset = (int)textOptions.Font.Size;
186-
this.FillOperations = new List<DrawingOperation>(size);
187-
this.OutlineOperations = new List<DrawingOperation>(size);
176+
this.DrawingOperations = new List<DrawingOperation>(size);
188177
this.transform = transform;
189178
this.builder = new PathBuilder();
190179
}
191180

192-
public List<DrawingOperation> FillOperations { get; }
193-
194-
public List<DrawingOperation> OutlineOperations { get; }
181+
public List<DrawingOperation> DrawingOperations { get; }
195182

196183
public MemoryAllocator MemoryAllocator { get; internal set; }
197184

@@ -260,8 +247,7 @@ public bool BeginGlyph(FontRectangle bounds, GlyphRendererParameters parameters)
260247
public void BeginText(FontRectangle bounds)
261248
{
262249
// Not concerned about this one
263-
this.OutlineOperations?.Clear();
264-
this.FillOperations?.Clear();
250+
this.DrawingOperations.Clear();
265251
}
266252

267253
public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point)
@@ -296,29 +282,48 @@ public void EndGlyph()
296282
return;
297283
}
298284

285+
// fix up the text runs colors
286+
// only if both brush and pen is null do we fallback to the defualt value
287+
if (this.currentBrush == null && this.currentPen == null)
288+
{
289+
this.currentBrush = this.Brush;
290+
this.currentPen = this.Pen;
291+
}
292+
299293
// If we are using the fonts color layers we ignore the request to draw an outline only
300294
// cause that wont really work and instead force drawing with fill with the requested color
301295
// if color fonts disabled then this.currentColor will always be null
302-
var brush = this.Brush ?? this.currentBrush;
303-
if (brush != null || this.currentColor != null)
296+
if (this.currentBrush != null || this.currentColor != null)
304297
{
305298
renderData.FillMap = this.Render(path);
306-
renderData.Color = this.currentColor;
299+
300+
if (this.currentColor.HasValue)
301+
{
302+
if (this.brushLookup.TryGetValue(this.currentColor.Value, out var brush))
303+
{
304+
this.currentBrush = brush;
305+
}
306+
else
307+
{
308+
this.currentBrush = new SolidBrush(this.currentColor.Value);
309+
this.brushLookup[this.currentColor.Value] = this.currentBrush;
310+
}
311+
}
307312
}
308313

309-
var pen = this.currentPen ?? this.Pen;
310-
if (pen != null && this.currentColor == null)
314+
if (this.currentPen != null && this.currentColor == null)
311315
{
312-
if (pen.StrokePattern.Length == 0)
316+
if (this.currentPen.StrokePattern.Length == 0)
313317
{
314-
path = path.GenerateOutline(pen.StrokeWidth);
318+
path = path.GenerateOutline(this.currentPen.StrokeWidth);
315319
}
316320
else
317321
{
318-
path = path.GenerateOutline(pen.StrokeWidth, pen.StrokePattern, pen.JointStyle, pen.EndCapStyle);
322+
path = path.GenerateOutline(this.currentPen.StrokeWidth, this.currentPen.StrokePattern, this.currentPen.JointStyle, this.currentPen.EndCapStyle);
319323
}
320324

321325
renderData.OutlineMap = this.Render(path);
326+
this.currentBrush = this.currentPen.StrokeFill;
322327
}
323328

324329
this.glyphData[this.currentGlyphRenderParams] = renderData;
@@ -330,22 +335,23 @@ public void EndGlyph()
330335

331336
if (renderData.FillMap != null)
332337
{
333-
this.FillOperations.Add(new DrawingOperation
338+
this.DrawingOperations.Add(new DrawingOperation
334339
{
335340
Location = this.currentRenderPosition,
336341
Map = renderData.FillMap,
337-
Color = this.currentColor,
338342
Brush = this.currentBrush,
343+
RenderPass = 1
339344
});
340345
}
341346

342347
if (renderData.OutlineMap != null)
343348
{
344-
this.OutlineOperations.Add(new DrawingOperation
349+
this.DrawingOperations.Add(new DrawingOperation
345350
{
346351
Location = this.currentRenderPosition,
347352
Map = renderData.OutlineMap,
348-
Brush = this.currentPen?.StrokeFill,
353+
Brush = this.currentBrush,
354+
RenderPass = 2 // render outlines 2nd to ensure they are always ontop of fills
349355
});
350356
}
351357
}

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

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -494,31 +494,51 @@ public void CanRotateOutlineFont_Issue175<TPixel>(
494494
}
495495

496496
[Theory]
497-
[WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, 32)]
498-
[WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, 40)]
497+
[WithSolidFilledImages(500, 200, nameof(Color.White), PixelTypes.Rgba32, 32)]
498+
[WithSolidFilledImages(500, 200, nameof(Color.White), PixelTypes.Rgba32, 40)]
499499
public void DrawRichTextWithMixOfPensAndBrushes<TPixel>(
500500
TestImageProvider<TPixel> provider,
501501
int fontSize)
502502
where TPixel : unmanaged, IPixel<TPixel>
503503
{
504504
Font font = CreateFont(TestFonts.OpenSans, fontSize);
505-
const string text = "QuickTYZ";
505+
Font font2 = CreateFont(TestFonts.OpenSans, fontSize * 1.5f);
506+
const string text = "The quick brown fox jumps over the lazy log.";
506507

507508
TextDrawingOptions textOptions = new(font)
508509
{
510+
WrappingLength = 400,
509511
TextRuns = new[]
510512
{
511513
new TextDrawingRun
512514
{
513-
Start = 2,
514-
End = 2,
515+
Start = 4,
516+
End = 10,
517+
TextAttributes = TextAttribute.Strikethrough,
515518
Brush = Brushes.Solid(Color.Red),
516519
},
520+
517521
new TextDrawingRun
518522
{
519-
Start = 4,
520-
End = 5,
521-
Pen = Pens.Dot(Color.Red, 0.2f),
523+
Start = 10,
524+
End = 13,
525+
Font = font2,
526+
TextAttributes = TextAttribute.Strikethrough,
527+
},
528+
529+
new TextDrawingRun
530+
{
531+
Start = 19,
532+
End = 23,
533+
TextAttributes = TextAttribute.Underline,
534+
Brush = Brushes.Solid(Color.Blue),
535+
},
536+
537+
new TextDrawingRun
538+
{
539+
Start = 23,
540+
End = 26,
541+
TextAttributes = TextAttribute.Underline
522542
}
523543
}
524544
};
@@ -538,7 +558,7 @@ private static string ToTestOutputDisplayText(string text)
538558
return fnDisplayText.Substring(0, Math.Min(fnDisplayText.Length, 4));
539559
}
540560

541-
private static Font CreateFont(string fontName, int size)
561+
private static Font CreateFont(string fontName, float size)
542562
=> TestFontUtilities.GetFont(fontName, size);
543563
}
544564
}

0 commit comments

Comments
 (0)