Skip to content

Commit f6dbef1

Browse files
Fix multiline rendering
1 parent 5e91132 commit f6dbef1

50 files changed

Lines changed: 149 additions & 142 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ void Draw(IEnumerable<DrawingOperation> operations)
6666
this.SourceRectangle);
6767

6868
Buffer2D<float> buffer = operation.Map;
69-
int startY = operation.Location.Y;
70-
int startX = operation.Location.X;
69+
int startY = operation.RenderLocation.Y;
70+
int startX = operation.RenderLocation.X;
7171
int offsetSpan = 0;
7272

7373
if (startX + buffer.Height < 0)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal struct DrawingOperation
1313

1414
public byte RenderPass { get; set; }
1515

16-
public Point Location { get; set; }
16+
public Point RenderLocation { get; set; }
1717

1818
public Brush Brush { get; internal set; }
1919
}

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

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,18 @@ internal sealed class RichTextGlyphRenderer : BaseGlyphBuilder, IColorGlyphRende
4848
// - Provide a good accuracy (smaller than 0.2% image difference compared to the non-caching variant)
4949
// - Cache hit ratio above 60%
5050
private const float AccuracyMultiple = 8;
51-
private readonly Dictionary<(GlyphRendererParameters Glyph, PointF SubPixelOffset), GlyphRenderData> glyphData = new();
51+
private readonly Dictionary<(GlyphRendererParameters Glyph, RectangleF Bounds), GlyphRenderData> glyphData = new();
5252
private bool rasterizationRequired;
5353
private readonly bool noCache;
54-
private (GlyphRendererParameters Glyph, PointF SubPixelOffset) currentCacheKey;
54+
private (GlyphRendererParameters Glyph, RectangleF Bounds) currentCacheKey;
5555

5656
public RichTextGlyphRenderer(
5757
RichTextOptions textOptions,
5858
DrawingOptions drawingOptions,
5959
MemoryAllocator memoryAllocator,
6060
Pen pen,
6161
Brush brush)
62+
: base(drawingOptions.Transform)
6263
{
6364
this.textOptions = textOptions;
6465
this.drawingOptions = drawingOptions;
@@ -67,9 +68,6 @@ public RichTextGlyphRenderer(
6768
this.defaultBrush = brush;
6869
this.DrawingOperations = new List<DrawingOperation>();
6970

70-
// Set the default transform.
71-
this.Builder.SetTransform(drawingOptions.Transform);
72-
7371
IPath path = textOptions.Path;
7472
if (path is not null)
7573
{
@@ -138,15 +136,17 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara
138136
if (!this.noCache)
139137
{
140138
// Create a cache entry for the glyph.
139+
// We need to apply the default transform to the bounds to get the correct size
140+
// for comparison with future glyphs. We can use this cached glyph anywhere in the text block.
141141
var currentBounds = RectangleF.Transform(
142-
new RectangleF(bounds.Location, new(bounds.Width, bounds.Height)),
143-
this.drawingOptions.Transform);
142+
new RectangleF(bounds.Location, new(bounds.Width, bounds.Height)),
143+
this.drawingOptions.Transform);
144144

145-
PointF subPixelOffset = currentBounds.Location - ClampToPixel(currentBounds.Location);
146-
subPixelOffset.X = MathF.Round(subPixelOffset.X * AccuracyMultiple) / AccuracyMultiple;
147-
subPixelOffset.Y = MathF.Round(subPixelOffset.Y * AccuracyMultiple) / AccuracyMultiple;
145+
SizeF subPixelSize = new(
146+
MathF.Round(currentBounds.Width * AccuracyMultiple) / AccuracyMultiple,
147+
MathF.Round(currentBounds.Height * AccuracyMultiple) / AccuracyMultiple);
148148

149-
this.currentCacheKey = (parameters, subPixelOffset);
149+
this.currentCacheKey = (parameters, new RectangleF(new(0, 0), subPixelSize));
150150
if (this.glyphData.ContainsKey(this.currentCacheKey))
151151
{
152152
// We have already drawn the glyph vectors.
@@ -155,6 +155,8 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara
155155
}
156156
}
157157

158+
// Transform the glyph vectors using the original bounds
159+
// The default transform will automatically be applied.
158160
this.TransformGlyph(in bounds);
159161
this.rasterizationRequired = true;
160162
}
@@ -273,7 +275,7 @@ protected override void EndGlyph()
273275
GlyphRenderData renderData = default;
274276

275277
// Fix up the text runs colors.
276-
// Only if both brush and pen is null do we fallback to the defualt value
278+
// Only if both brush and pen is null do we fallback to the default value.
277279
if (this.currentBrush == null && this.currentPen == null)
278280
{
279281
this.currentBrush = this.defaultBrush;
@@ -284,8 +286,8 @@ protected override void EndGlyph()
284286
bool renderOutline = false;
285287

286288
// If we are using the fonts color layers we ignore the request to draw an outline only
287-
// cause that wont really work and instead force drawing with fill with the requested color
288-
// if color fonts disabled then this.currentColor will always be null
289+
// because that won't really work. Instead we force drawing using fill with the requested color.
290+
// If color fonts are disabled then this.currentColor will always be null.
289291
if (this.currentBrush != null || this.currentColor != null)
290292
{
291293
renderFill = true;
@@ -310,16 +312,14 @@ protected override void EndGlyph()
310312

311313
// Path has already been added to the collection via the base class.
312314
IPath path = this.Paths.Last();
315+
Point renderLocation = ClampToPixel(path.Bounds.Location);
313316
if (this.noCache || this.rasterizationRequired)
314317
{
315318
if (path.Bounds.Equals(RectangleF.Empty))
316319
{
317320
return;
318321
}
319322

320-
// If we are using the fonts color layers we ignore the request to draw an outline only
321-
// cause that wont really work and instead force drawing with fill with the requested color
322-
// if color fonts disabled then this.currentColor will always be null
323323
if (renderFill)
324324
{
325325
renderData.FillMap = this.Render(path);
@@ -331,6 +331,10 @@ protected override void EndGlyph()
331331
renderData.OutlineMap = this.Render(path);
332332
}
333333

334+
// Capture the delta between the location and the truncated render location.
335+
// We can use this to offset the render location on the next instance of this glyph.
336+
renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation);
337+
334338
if (!this.noCache)
335339
{
336340
this.glyphData[this.currentCacheKey] = renderData;
@@ -339,13 +343,16 @@ protected override void EndGlyph()
339343
else
340344
{
341345
renderData = this.glyphData[this.currentCacheKey];
346+
347+
// Offset the render location by the delta from the cached glyph and this one.
348+
renderLocation = (Point)(path.Bounds.Location - (PointF)renderData.LocationDelta);
342349
}
343350

344351
if (renderData.FillMap != null)
345352
{
346353
this.DrawingOperations.Add(new DrawingOperation
347354
{
348-
Location = ClampToPixel(path.Bounds.Location),
355+
RenderLocation = renderLocation,
349356
Map = renderData.FillMap,
350357
Brush = this.currentBrush,
351358
RenderPass = RenderOrderFill
@@ -356,7 +363,7 @@ protected override void EndGlyph()
356363
{
357364
this.DrawingOperations.Add(new DrawingOperation
358365
{
359-
Location = ClampToPixel(path.Bounds.Location),
366+
RenderLocation = renderLocation,
360367
Map = renderData.OutlineMap,
361368
Brush = this.currentPen?.StrokeFill ?? this.currentBrush,
362369
RenderPass = RenderOrderOutline
@@ -375,9 +382,9 @@ protected override void EndText()
375382
public void Dispose() => this.Dispose(true);
376383

377384
[MethodImpl(MethodImplOptions.AggressiveInlining)]
378-
private static Point ClampToPixel(PointF point)
379-
=> Point.Truncate(point);
385+
private static Point ClampToPixel(PointF point) => Point.Truncate(point);
380386

387+
// Point.Truncate(point);
381388
private void FinalizeDecoration(ref TextDecorationDetails? decoration)
382389
{
383390
if (decoration != null)
@@ -396,11 +403,11 @@ private void FinalizeDecoration(ref TextDecorationDetails? decoration)
396403

397404
if (outline.Bounds.Width != 0 && outline.Bounds.Height != 0)
398405
{
399-
// Render the Path here
406+
// Render the path here. Decorations are uncached.
400407
this.DrawingOperations.Add(new DrawingOperation
401408
{
402409
Brush = decoration.Value.Pen.StrokeFill,
403-
Location = ClampToPixel(outline.Bounds.Location),
410+
RenderLocation = ClampToPixel(outline.Bounds.Location),
404411
Map = this.Render(outline),
405412
RenderPass = RenderOrderDecoration
406413
});
@@ -444,14 +451,7 @@ private void AppendDecoration(ref TextDecorationDetails? decoration, Vector2 sta
444451

445452
[MethodImpl(MethodImplOptions.AggressiveInlining)]
446453
private void TransformGlyph(in FontRectangle bounds)
447-
{
448-
if (this.path is null)
449-
{
450-
return;
451-
}
452-
453-
this.Builder.SetTransform(this.ComputeTransform(in bounds));
454-
}
454+
=> this.Builder.SetTransform(this.ComputeTransform(in bounds));
455455

456456
[MethodImpl(MethodImplOptions.AggressiveInlining)]
457457
private Matrix3x2 ComputeTransform(in FontRectangle bounds)
@@ -482,15 +482,19 @@ private Matrix3x2 ComputeTransform(in FontRectangle bounds)
482482

483483
private Buffer2D<float> Render(IPath path)
484484
{
485-
// We need to offset the path now against 0,0 for rasterization.
486-
IPath offsetPath = path.Translate(-path.Bounds.Location);
485+
// We need to offset the path now against the equivalent pixel position of [0,0] for rasterization.
486+
IPath offsetPath = path.Translate(-ClampToPixel(path.Bounds.Location));
487487
Size size = Rectangle.Ceiling(offsetPath.Bounds).Size;
488+
489+
// Pad to prevent edge clipping.
490+
size += new Size(2, 2);
491+
488492
int subpixelCount = FillPathProcessor.MinimumSubpixelCount;
489493
float xOffset = .5F;
490494
GraphicsOptions graphicsOptions = this.drawingOptions.GraphicsOptions;
491495
if (graphicsOptions.Antialias)
492496
{
493-
xOffset = 0F; // We are antialiasing skip offsetting as real antialiasing should take care of offset.
497+
xOffset = 0F; // We are antialiasing. Skip offsetting as real antialiasing should take care of offset.
494498
subpixelCount = Math.Max(subpixelCount, graphicsOptions.AntialiasSubpixelDepth);
495499
}
496500

@@ -543,7 +547,7 @@ private void Dispose(bool disposing)
543547
{
544548
if (disposing)
545549
{
546-
foreach (KeyValuePair<(GlyphRendererParameters Glyph, PointF SubPixelOffset), GlyphRenderData> kv in this.glyphData)
550+
foreach (KeyValuePair<(GlyphRendererParameters Glyph, RectangleF Bounds), GlyphRenderData> kv in this.glyphData)
547551
{
548552
kv.Value.Dispose();
549553
}
@@ -564,7 +568,8 @@ private void Dispose(bool disposing)
564568

565569
private struct GlyphRenderData : IDisposable
566570
{
567-
// public Color? Color;
571+
public Vector2 LocationDelta;
572+
568573
public Buffer2D<float> FillMap;
569574

570575
public Buffer2D<float> OutlineMap;

src/ImageSharp.Drawing/Shapes/PathBuilder.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public PathBuilder SetOrigin(PointF origin)
6565
}
6666

6767
/// <summary>
68-
/// Resets the translation to the default.
68+
/// Resets the transform to the default.
6969
/// </summary>
7070
/// <returns>The <see cref="PathBuilder"/>.</returns>
7171
public PathBuilder ResetTransform()
@@ -451,8 +451,6 @@ public void Clear()
451451
this.currentFigure = new Figure();
452452
this.figures.Clear();
453453
this.figures.Add(this.currentFigure);
454-
455-
// TODO: Should we reset currentPoint here instead?
456454
}
457455

458456
private class Figure

src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ internal ref struct PolygonScanner
1414
private readonly int minY;
1515
private readonly int maxY;
1616
private readonly IntersectionRule intersectionRule;
17-
private ScanEdgeCollection edgeCollection;
18-
private Span<ScanEdge> edges;
17+
private readonly ScanEdgeCollection edgeCollection;
18+
private readonly Span<ScanEdge> edges;
1919

2020
// Common contiguous buffer for sorted0, sorted1, intersections, activeEdges [,intersectionTypes]
21-
private IMemoryOwner<int> dataBuffer;
21+
private readonly IMemoryOwner<int> dataBuffer;
2222

2323
// | <- edgeCnt -> | <- edgeCnt -> | <- edgeCnt -> | <- maxIntersectionCount -> | <- maxIntersectionCount -> |
2424
// |---------------|---------------|---------------|----------------------------|----------------------------|
2525
// | sorted0 | sorted1 | activeEdges | intersections | intersectionTypes |
2626
// |---------------|---------------|---------------|----------------------------|----------------------------|
27-
private Span<int> sorted0;
28-
private Span<int> sorted1;
27+
private readonly Span<int> sorted0;
28+
private readonly Span<int> sorted1;
2929
private ActiveEdgeList activeEdges;
30-
private Span<float> intersections;
31-
private Span<NonZeroIntersectionType> intersectionTypes;
30+
private readonly Span<float> intersections;
31+
private readonly Span<NonZeroIntersectionType> intersectionTypes;
3232

3333
private int idx0;
3434
private int idx1;
@@ -192,11 +192,9 @@ public bool MoveToNextSubpixelScanLine()
192192
}
193193

194194
public ReadOnlySpan<float> ScanCurrentLine()
195-
{
196-
return this.intersectionRule == IntersectionRule.OddEven
197-
? this.activeEdges.ScanOddEven(this.SubPixelY, this.edges, this.intersections)
198-
: this.activeEdges.ScanNonZero(this.SubPixelY, this.edges, this.intersections, this.intersectionTypes);
199-
}
195+
=> this.intersectionRule == IntersectionRule.OddEven
196+
? this.activeEdges.ScanOddEven(this.SubPixelY, this.edges, this.intersections)
197+
: this.activeEdges.ScanNonZero(this.SubPixelY, this.edges, this.intersections, this.intersectionTypes);
200198

201199
public void Dispose()
202200
{

src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ internal class BaseGlyphBuilder : IGlyphRenderer
2121
/// </summary>
2222
public BaseGlyphBuilder() => this.Builder = new PathBuilder();
2323

24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="BaseGlyphBuilder"/> class.
26+
/// </summary>
27+
/// <param name="transform">The default transform.</param>
28+
public BaseGlyphBuilder(Matrix3x2 transform) => this.Builder = new PathBuilder(transform);
29+
2430
/// <summary>
2531
/// Gets the paths that have been rendered by the current instance.
2632
/// </summary>
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading

0 commit comments

Comments
 (0)