diff --git a/Directory.Build.targets b/Directory.Build.targets
index 1428a7e3..53e2992d 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -27,7 +27,7 @@
-
+
diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
index c942bbd3..0b8ad49b 100644
--- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
+++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/src/ImageSharp.Drawing/Primitives/Region.cs b/src/ImageSharp.Drawing/Primitives/Region.cs
index c930cbf7..408c8d91 100644
--- a/src/ImageSharp.Drawing/Primitives/Region.cs
+++ b/src/ImageSharp.Drawing/Primitives/Region.cs
@@ -10,27 +10,12 @@ namespace SixLabors.ImageSharp.Drawing
///
public abstract class Region
{
- ///
- /// Gets the maximum number of intersections to could be returned.
- ///
- public abstract int MaxIntersections { get; }
-
///
/// Gets the bounding box that entirely surrounds this region.
///
- ///
- /// This should always contains all possible points returned from .
- ///
public abstract Rectangle Bounds { get; }
- ///
- /// Scans the X axis for intersections at the Y axis position.
- ///
- /// The position along the y axis to find intersections.
- /// The buffer.
- /// A instance in the context of the caller.
- /// How intersections are handled.
- /// The number of intersections found.
- public abstract int Scan(float y, Span buffer, Configuration configuration, IntersectionRule intersectionRule);
+ // We should consider removing Region, so keeping this internal for now.
+ internal abstract IPath Shape { get; }
}
}
diff --git a/src/ImageSharp.Drawing/Primitives/ShapeRegion.cs b/src/ImageSharp.Drawing/Primitives/ShapeRegion.cs
index 4b6edef9..2b39567d 100644
--- a/src/ImageSharp.Drawing/Primitives/ShapeRegion.cs
+++ b/src/ImageSharp.Drawing/Primitives/ShapeRegion.cs
@@ -18,7 +18,6 @@ internal class ShapeRegion : Region
public ShapeRegion(IPath shape)
{
IPath closedPath = shape.AsClosedPath();
- this.MaxIntersections = closedPath.MaxIntersections;
this.Shape = closedPath;
int left = (int)MathF.Floor(shape.Bounds.Left);
int top = (int)MathF.Floor(shape.Bounds.Top);
@@ -31,32 +30,9 @@ public ShapeRegion(IPath shape)
///
/// Gets the fillable shape
///
- public IPath Shape { get; }
-
- ///
- public override int MaxIntersections { get; }
+ internal override IPath Shape { get; }
///
public override Rectangle Bounds { get; }
-
- ///
- public override int Scan(float y, Span buffer, Configuration configuration, IntersectionRule intersectionRule)
- {
- var start = new PointF(this.Bounds.Left - 1, y);
- var end = new PointF(this.Bounds.Right + 1, y);
-
- using (IMemoryOwner tempBuffer = configuration.MemoryAllocator.Allocate(buffer.Length))
- {
- Span innerBuffer = tempBuffer.Memory.Span;
- int count = this.Shape.FindIntersections(start, end, innerBuffer, intersectionRule);
-
- for (int i = 0; i < count; i++)
- {
- buffer[i] = innerBuffer[i].X;
- }
-
- return count;
- }
- }
}
}
diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
index 24fa5a81..81df5b13 100644
--- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
+++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
@@ -139,7 +139,7 @@ public Edge(Path path, Color startColor, Color endColor)
{
this.path = path;
- Vector2[] points = path.LineSegments.SelectMany(s => s.Flatten()).Select(p => (Vector2)p).ToArray();
+ Vector2[] points = path.LineSegments.SelectMany(s => s.Flatten().ToArray()).Select(p => (Vector2)p).ToArray();
this.Start = points[0];
this.StartColor = (Vector4)startColor;
diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs
index f5070896..1792a2d7 100644
--- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs
+++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs
@@ -4,7 +4,7 @@
using System;
using System.Buffers;
using System.Numerics;
-using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Drawing.Utilities;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -155,7 +155,7 @@ public override void Apply(Span scanline, int x, int y)
for (int i = 0; i < scanline.Length; i++)
{
- amountSpan[i] = NumberUtilities.ClampFloat(scanline[i] * this.Options.BlendPercentage, 0, 1F);
+ amountSpan[i] = NumericUtilities.ClampFloat(scanline[i] * this.Options.BlendPercentage, 0, 1F);
int patternX = (x + i) % this.pattern.Columns;
overlaySpan[i] = this.pattern[patternY, patternX];
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs
index ce597a73..c809e344 100644
--- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs
+++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs
@@ -12,6 +12,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing
///
public class FillRegionProcessor : IImageProcessor
{
+ ///
+ /// Minimum subpixel count for rasterization, being applied even if antialiasing is off.
+ ///
+ internal const int MinimumSubpixelCount = 8;
+
///
/// Initializes a new instance of the class.
///
@@ -42,7 +47,6 @@ public FillRegionProcessor(ShapeGraphicsOptions options, IBrush brush, Region re
///
public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle)
- where TPixel : unmanaged, IPixel
- => new FillRegionProcessor(configuration, this, source, sourceRectangle);
+ where TPixel : unmanaged, IPixel => new FillRegionProcessor(configuration, this, source, sourceRectangle);
}
}
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs
index a4442970..99a42517 100644
--- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs
+++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor{TPixel}.cs
@@ -3,7 +3,8 @@
using System;
using System.Buffers;
-using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Drawing.Shapes;
+using SixLabors.ImageSharp.Drawing.Shapes.Rasterization;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors;
@@ -54,8 +55,7 @@ protected override void OnFrameApply(ImageFrame source)
return; // no effect inside image;
}
- int maxIntersections = region.MaxIntersections;
- float subpixelCount = 4;
+ int subpixelCount = FillRegionProcessor.MinimumSubpixelCount;
// we need to offset the pixel grid to account for when we outline a path.
// basically if the line is [1,2] => [3,2] then when outlining at 1 we end up with a region of [0.5,1.5],[1.5, 1.5],[3.5,2.5],[2.5,2.5]
@@ -63,118 +63,77 @@ protected override void OnFrameApply(ImageFrame source)
// region to align with the pixel grid.
if (graphicsOptions.Antialias)
{
- subpixelCount = graphicsOptions.AntialiasSubpixelDepth;
- if (subpixelCount < 4)
- {
- subpixelCount = 4;
- }
+ subpixelCount = Math.Max(subpixelCount, graphicsOptions.AntialiasSubpixelDepth);
}
- using (BrushApplicator applicator = brush.CreateApplicator(configuration, graphicsOptions, source, rect))
+ using BrushApplicator applicator = brush.CreateApplicator(configuration, graphicsOptions, source, rect);
+ int scanlineWidth = maxX - minX;
+ MemoryAllocator allocator = this.Configuration.MemoryAllocator;
+ bool scanlineDirty = true;
+
+ var scanner = PolygonScanner.Create(
+ region.Shape,
+ minY,
+ maxY,
+ subpixelCount,
+ shapeOptions.IntersectionRule,
+ configuration.MemoryAllocator);
+
+ try
{
- int scanlineWidth = maxX - minX;
- MemoryAllocator allocator = this.Configuration.MemoryAllocator;
- using (IMemoryOwner bBuffer = allocator.Allocate(maxIntersections))
- using (IMemoryOwner bScanline = allocator.Allocate(scanlineWidth))
+ using IMemoryOwner bScanline = allocator.Allocate(scanlineWidth);
+ Span scanline = bScanline.Memory.Span;
+
+ while (scanner.MoveToNextPixelLine())
{
- bool scanlineDirty = true;
- float subpixelFraction = 1f / subpixelCount;
- float subpixelFractionPoint = subpixelFraction / subpixelCount;
+ if (scanlineDirty)
+ {
+ scanline.Clear();
+ }
- Span buffer = bBuffer.Memory.Span;
- Span scanline = bScanline.Memory.Span;
+ scanlineDirty = scanner.ScanCurrentPixelLineInto(minX, 0, scanline);
- for (int y = minY; y < maxY; y++)
+ if (scanlineDirty)
{
- if (scanlineDirty)
+ int y = scanner.PixelLineY;
+ if (!graphicsOptions.Antialias)
{
- scanline.Clear();
- scanlineDirty = false;
- }
-
- float yPlusOne = y + 1;
- for (float subPixel = y; subPixel < yPlusOne; subPixel += subpixelFraction)
- {
- int pointsFound = region.Scan(subPixel, buffer, configuration, shapeOptions.IntersectionRule);
- if (pointsFound == 0)
+ bool hasOnes = false;
+ bool hasZeros = false;
+ for (int x = 0; x < scanline.Length; x++)
{
- // nothing on this line, skip
- continue;
- }
-
- for (int point = 0; point < pointsFound && point < buffer.Length - 1; point += 2)
- {
- // points will be paired up
- float scanStart = buffer[point] - minX;
- float scanEnd = buffer[point + 1] - minX;
- int startX = (int)MathF.Floor(scanStart);
- int endX = (int)MathF.Floor(scanEnd);
-
- if (startX >= 0 && startX < scanline.Length)
+ if (scanline[x] >= 0.5)
{
- for (float x = scanStart; x < startX + 1; x += subpixelFraction)
- {
- scanline[startX] += subpixelFractionPoint;
- scanlineDirty = true;
- }
+ scanline[x] = 1;
+ hasOnes = true;
}
-
- if (endX >= 0 && endX < scanline.Length)
- {
- for (float x = endX; x < scanEnd; x += subpixelFraction)
- {
- scanline[endX] += subpixelFractionPoint;
- scanlineDirty = true;
- }
- }
-
- int nextX = startX + 1;
- endX = Math.Min(endX, scanline.Length); // reduce to end to the right edge
- nextX = Math.Max(nextX, 0);
- for (int x = nextX; x < endX; x++)
+ else
{
- scanline[x] += subpixelFraction;
- scanlineDirty = true;
+ scanline[x] = 0;
+ hasZeros = true;
}
}
- }
- if (scanlineDirty)
- {
- if (!graphicsOptions.Antialias)
+ if (isSolidBrushWithoutBlending && hasOnes != hasZeros)
{
- bool hasOnes = false;
- bool hasZeros = false;
- for (int x = 0; x < scanlineWidth; x++)
+ if (hasOnes)
{
- if (scanline[x] >= 0.5)
- {
- scanline[x] = 1;
- hasOnes = true;
- }
- else
- {
- scanline[x] = 0;
- hasZeros = true;
- }
+ source.GetPixelRowSpan(y).Slice(minX, scanlineWidth).Fill(solidBrushColor);
}
- if (isSolidBrushWithoutBlending && hasOnes != hasZeros)
- {
- if (hasOnes)
- {
- source.GetPixelRowSpan(y).Slice(minX, scanlineWidth).Fill(solidBrushColor);
- }
-
- continue;
- }
+ continue;
}
-
- applicator.Apply(scanline, minX, y);
}
+
+ applicator.Apply(scanline, minX, y);
}
}
}
+ finally
+ {
+ // ref structs can't implement interfaces so technically PolygonScanner is not IDisposable
+ scanner.Dispose();
+ }
}
private static bool IsSolidBrushWithoutBlending(GraphicsOptions options, IBrush inputBrush, out SolidBrush solidBrush)
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs
index afa074d8..645ba6b5 100644
--- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs
+++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs
@@ -6,6 +6,9 @@
using System.Collections.Generic;
using System.Numerics;
using SixLabors.Fonts;
+using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing;
+using SixLabors.ImageSharp.Drawing.Shapes.Rasterization;
+using SixLabors.ImageSharp.Drawing.Utilities;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors;
@@ -57,9 +60,15 @@ protected override void BeforeImageApply()
ColorFontSupport = this.definition.Options.TextOptions.RenderColorFonts ? ColorFontSupport.MicrosoftColrFormat : ColorFontSupport.None,
};
- this.textRenderer = new CachingGlyphRenderer(this.Configuration.MemoryAllocator, this.Text.Length, this.Pen, this.Brush != null);
+ this.textRenderer = new CachingGlyphRenderer(
+ this.Configuration.MemoryAllocator,
+ this.Text.Length,
+ this.Pen,
+ this.Brush != null)
+ {
+ Options = this.Options
+ };
- this.textRenderer.Options = this.Options.GraphicsOptions;
var renderer = new TextRenderer(this.textRenderer);
renderer.RenderText(this.Text, style);
}
@@ -89,12 +98,16 @@ void Draw(List operations, IBrush brush)
{
if (!brushes.TryGetValue(operation.Color.Value, out _))
{
- brushes[operation.Color.Value] = new SolidBrush(operation.Color.Value).CreateApplicator(this.Configuration, this.textRenderer.Options, source, this.SourceRectangle);
+ brushes[operation.Color.Value] = new SolidBrush(operation.Color.Value).CreateApplicator(
+ this.Configuration,
+ this.textRenderer.Options.GraphicsOptions,
+ source,
+ this.SourceRectangle);
}
}
}
- using (BrushApplicator app = brush.CreateApplicator(this.Configuration, this.textRenderer.Options, source, this.SourceRectangle))
+ using (BrushApplicator app = brush.CreateApplicator(this.Configuration, this.textRenderer.Options.GraphicsOptions, source, this.SourceRectangle))
{
foreach (DrawingOperation operation in operations)
{
@@ -219,7 +232,7 @@ public CachingGlyphRenderer(MemoryAllocator memoryAllocator, int size, IPen pen,
public IPen Pen { get; internal set; }
- public GraphicsOptions Options { get; internal set; }
+ public TextGraphicsOptions Options { get; internal set; }
protected void SetLayerColor(Color color)
{
@@ -358,110 +371,54 @@ private Buffer2D Render(IPath path)
Size size = Rectangle.Ceiling(path.Bounds).Size;
size = new Size(size.Width + (this.offset * 2), size.Height + (this.offset * 2));
- float subpixelCount = 4;
- float offset = 0.5f;
- if (this.Options.Antialias)
+ int subpixelCount = FillRegionProcessor.MinimumSubpixelCount;
+ float xOffset = 0.5f;
+ GraphicsOptions graphicsOptions = this.Options.GraphicsOptions;
+ if (graphicsOptions.Antialias)
{
- offset = 0f; // we are antialiasing skip offsetting as real antialiasing should take care of offset.
- subpixelCount = this.Options.AntialiasSubpixelDepth;
- if (subpixelCount < 4)
- {
- subpixelCount = 4;
- }
+ xOffset = 0f; // we are antialiasing skip offsetting as real antialiasing should take care of offset.
+ subpixelCount = Math.Max(subpixelCount, graphicsOptions.AntialiasSubpixelDepth);
}
// take the path inside the path builder, scan thing and generate a Buffer2d representing the glyph and cache it.
Buffer2D fullBuffer = this.MemoryAllocator.Allocate2D(size.Width + 1, size.Height + 1, AllocationOptions.Clean);
- using (IMemoryOwner bufferBacking = this.MemoryAllocator.Allocate(path.MaxIntersections))
- using (IMemoryOwner rowIntersectionBuffer = this.MemoryAllocator.Allocate(size.Width))
- {
- float subpixelFraction = 1f / subpixelCount;
- float subpixelFractionPoint = subpixelFraction / subpixelCount;
- Span intersectionSpan = rowIntersectionBuffer.Memory.Span;
- Span buffer = bufferBacking.Memory.Span;
+ var scanner = PolygonScanner.Create(
+ path,
+ 0,
+ size.Height,
+ subpixelCount,
+ IntersectionRule.Nonzero,
+ this.MemoryAllocator);
- for (int y = 0; y <= size.Height; y++)
+ try
+ {
+ while (scanner.MoveToNextPixelLine())
{
- Span scanline = fullBuffer.GetRowSpan(y);
- bool scanlineDirty = false;
- float yPlusOne = y + 1;
+ Span scanline = fullBuffer.GetRowSpan(scanner.PixelLineY);
+ bool scanlineDirty = scanner.ScanCurrentPixelLineInto(0, xOffset, scanline);
- for (float subPixel = y; subPixel < yPlusOne; subPixel += subpixelFraction)
+ if (scanlineDirty && !graphicsOptions.Antialias)
{
- var start = new PointF(path.Bounds.Left - 1, subPixel);
- var end = new PointF(path.Bounds.Right + 1, subPixel);
- int pointsFound = path.FindIntersections(start, end, intersectionSpan, IntersectionRule.Nonzero);
-
- if (pointsFound == 0)
- {
- // nothing on this line skip
- continue;
- }
-
- for (int i = 0; i < pointsFound && i < intersectionSpan.Length; i++)
- {
- buffer[i] = intersectionSpan[i].X;
- }
-
- QuickSort.Sort(buffer.Slice(0, pointsFound));
-
- for (int point = 0; point < pointsFound; point += 2)
+ for (int x = 0; x < size.Width; x++)
{
- // points will be paired up
- float scanStart = buffer[point];
- float scanEnd = buffer[point + 1];
- int startX = (int)MathF.Floor(scanStart + offset);
- int endX = (int)MathF.Floor(scanEnd + offset);
-
- if (startX >= 0 && startX < scanline.Length)
- {
- for (float x = scanStart; x < startX + 1; x += subpixelFraction)
- {
- scanline[startX] += subpixelFractionPoint;
- scanlineDirty = true;
- }
- }
-
- if (endX >= 0 && endX < scanline.Length)
- {
- for (float x = endX; x < scanEnd; x += subpixelFraction)
- {
- scanline[endX] += subpixelFractionPoint;
- scanlineDirty = true;
- }
- }
-
- int nextX = startX + 1;
- endX = Math.Min(endX, scanline.Length); // reduce to end to the right edge
- nextX = Math.Max(nextX, 0);
- for (int x = nextX; x < endX; x++)
+ if (scanline[x] >= 0.5)
{
- scanline[x] += subpixelFraction;
- scanlineDirty = true;
+ scanline[x] = 1;
}
- }
- }
-
- if (scanlineDirty)
- {
- if (!this.Options.Antialias)
- {
- for (int x = 0; x < size.Width; x++)
+ else
{
- if (scanline[x] >= 0.5)
- {
- scanline[x] = 1;
- }
- else
- {
- scanline[x] = 0;
- }
+ scanline[x] = 0;
}
}
}
}
}
+ finally
+ {
+ // ref structs can't implement interfaces so technically PolygonScanner is not IDisposable
+ scanner.Dispose();
+ }
return fullBuffer;
}
diff --git a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs
index cebaed10..8cc91704 100644
--- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs
+++ b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using SixLabors.ImageSharp.Drawing.Shapes;
+
namespace SixLabors.ImageSharp.Drawing.Processing
{
///
diff --git a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs
index a46791e3..1e19f393 100644
--- a/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs
+++ b/src/ImageSharp.Drawing/Processing/TextGraphicsOptions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using SixLabors.ImageSharp.Drawing.Shapes;
+
namespace SixLabors.ImageSharp.Drawing.Processing
{
///
diff --git a/src/ImageSharp.Drawing/Processing/TextOptions.cs b/src/ImageSharp.Drawing/Processing/TextOptions.cs
index 75c9a259..361195f3 100644
--- a/src/ImageSharp.Drawing/Processing/TextOptions.cs
+++ b/src/ImageSharp.Drawing/Processing/TextOptions.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using SixLabors.Fonts;
+using SixLabors.ImageSharp.Drawing.Shapes;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Drawing.Processing
diff --git a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs
index bcd5200e..5b13fdb2 100644
--- a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs
+++ b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs
@@ -6,7 +6,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
-using Orientation = SixLabors.ImageSharp.Drawing.InternalPath.Orientation;
+using SixLabors.ImageSharp.Drawing.Utilities;
namespace SixLabors.ImageSharp.Drawing
{
@@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Drawing
/// Represents a complex polygon made up of one or more shapes overlayed on each other, where overlaps causes holes.
///
///
- public sealed class ComplexPolygon : IPath
+ public sealed class ComplexPolygon : IPath, IInternalPathOwner
{
private readonly IPath[] paths;
private List internalPaths = null;
@@ -206,14 +206,14 @@ public int FindIntersections(PointF start, PointF end, Span buffer, Inte
this.EnsureInternalPathsInitalized();
int totalAdded = 0;
- Orientation[] orientations = ArrayPool.Shared.Rent(buffer.Length); // the largest number of intersections of any sub path of the set is the max size with need for this buffer.
- Span orientationsSpan = orientations;
+ InternalPath.PointOrientation[] orientations = ArrayPool.Shared.Rent(buffer.Length); // the largest number of intersections of any sub path of the set is the max size with need for this buffer.
+ Span orientationsSpan = orientations;
try
{
foreach (var ip in this.internalPaths)
{
Span subBuffer = buffer.Slice(totalAdded);
- Span subOrientationsSpan = orientationsSpan.Slice(totalAdded);
+ Span subOrientationsSpan = orientationsSpan.Slice(totalAdded);
var position = ip.FindIntersectionsWithOrientation(start, end, subBuffer, subOrientationsSpan);
totalAdded += position;
@@ -227,7 +227,7 @@ public int FindIntersections(PointF start, PointF end, Span buffer, Inte
var activeBuffer = buffer.Slice(0, totalAdded);
var activeOrientationsSpan = orientationsSpan.Slice(0, totalAdded);
- QuickSort.Sort(distances, activeBuffer, activeOrientationsSpan);
+ SortUtility.Sort(distances, activeBuffer, activeOrientationsSpan);
if (intersectionRule == IntersectionRule.Nonzero)
{
@@ -236,7 +236,7 @@ public int FindIntersections(PointF start, PointF end, Span buffer, Inte
}
finally
{
- ArrayPool.Shared.Return(orientations);
+ ArrayPool.Shared.Return(orientations);
}
return totalAdded;
@@ -376,5 +376,12 @@ public SegmentInfo PointAlongPath(float distanceAlongPath)
throw new InvalidOperationException("Should not be possible to reach this line");
}
+
+ ///
+ IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath()
+ {
+ this.EnsureInternalPathsInitalized();
+ return this.internalPaths;
+ }
}
}
diff --git a/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs b/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs
index c3a094d4..264508bb 100644
--- a/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs
+++ b/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs
@@ -20,7 +20,7 @@ public sealed class CubicBezierLineSegment : ILineSegment
///
/// The line points.
///
- private readonly List linePoints;
+ private readonly PointF[] linePoints;
private readonly PointF[] controlPoints;
///
@@ -79,7 +79,7 @@ public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF control
///
/// Returns the current as simple linear path.
///
- public IReadOnlyList Flatten()
+ public ReadOnlyMemory Flatten()
{
return this.linePoints;
}
@@ -114,7 +114,7 @@ public CubicBezierLineSegment Transform(Matrix3x2 matrix)
/// A line segment with the matrix applied to it.
ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix);
- private static List GetDrawingPoints(PointF[] controlPoints)
+ private static PointF[] GetDrawingPoints(PointF[] controlPoints)
{
var drawingPoints = new List();
int curveCount = (controlPoints.Length - 1) / 3;
@@ -132,7 +132,7 @@ private static List GetDrawingPoints(PointF[] controlPoints)
drawingPoints.AddRange(bezierCurveDrawingPoints);
}
- return drawingPoints;
+ return drawingPoints.ToArray();
}
private static List FindDrawingPoints(int curveIndex, PointF[] controlPoints)
diff --git a/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs b/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs
index b1f1ec6f..58fe33b0 100644
--- a/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs
+++ b/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs
@@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Drawing
///
/// A shape made up of a single path made up of one of more s
///
- public class EllipsePolygon : IPath, ISimplePath
+ public class EllipsePolygon : IPath, ISimplePath, IInternalPathOwner
{
private readonly InternalPath innerPath;
private readonly CubicBezierLineSegment segment;
@@ -72,7 +72,7 @@ private EllipsePolygon(CubicBezierLineSegment segment)
///
/// Gets the points that make up this simple linear path.
///
- IReadOnlyList ISimplePath.Points => this.innerPath.Points();
+ ReadOnlyMemory ISimplePath.Points => this.innerPath.Points();
///
public RectangleF Bounds => this.innerPath.Bounds;
@@ -229,5 +229,8 @@ private static CubicBezierLineSegment CreateSegment(Vector2 location, SizeF size
return new CubicBezierLineSegment(points);
}
+
+ ///
+ IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() => new[] { this.innerPath };
}
}
diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs b/src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs
new file mode 100644
index 00000000..d814608a
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.Helpers
+{
+ ///
+ /// Implements some basic algorithms on raw data structures.
+ /// Polygons are represented with a span of points,
+ /// where first point should be repeated at the end.
+ ///
+ ///
+ /// Positive orientation means Clockwise in world coordinates (positive direction goes UP on paper).
+ /// Since the Drawing library deals mostly with Screen coordinates where this is opposite,
+ /// we use different terminology here to avoid confusion.
+ ///
+ internal static class TopologyUtilities
+ {
+ ///
+ /// Positive: CCW in world coords (CW on screen)
+ /// Negative: CW in world coords (CCW on screen)
+ ///
+ public static void EnsureOrientation(Span polygon, int expectedOrientation)
+ {
+ if (GetPolygonOrientation(polygon) * expectedOrientation < 0)
+ {
+ polygon.Reverse();
+ }
+ }
+
+ ///
+ /// Zero: area is 0
+ /// Positive: CCW in world coords (CW on screen)
+ /// Negative: CW in world coords (CCW on screen)
+ ///
+ private static int GetPolygonOrientation(ReadOnlySpan polygon)
+ {
+ float sum = 0f;
+ for (var i = 0; i < polygon.Length - 1; ++i)
+ {
+ PointF curr = polygon[i];
+ PointF next = polygon[i + 1];
+ sum += (curr.X * next.Y) - (next.X * curr.Y);
+ }
+
+ // Normally, this should be a tolerant comparison, we don't have a special path for zero-area
+ // (or for self-intersecting, semi-zero-area) polygons in edge scanning.
+ return Math.Sign(sum);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs b/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs
index bbaf24ca..a72114b1 100644
--- a/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs
+++ b/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using System;
using System.Numerics;
namespace SixLabors.ImageSharp.Drawing
@@ -19,7 +20,7 @@ internal static class VectorExtensions
///
/// the Merged arrays
///
- public static bool Equivelent(this PointF source1, PointF source2, float threshold)
+ public static bool Equivalent(this PointF source1, PointF source2, float threshold)
{
var abs = Vector2.Abs(source1 - source2);
@@ -35,7 +36,7 @@ public static bool Equivelent(this PointF source1, PointF source2, float thresho
///
/// the Merged arrays
///
- public static bool Equivelent(this Vector2 source1, Vector2 source2, float threshold)
+ public static bool Equivalent(this Vector2 source1, Vector2 source2, float threshold)
{
var abs = Vector2.Abs(source1 - source2);
diff --git a/src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs b/src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs
new file mode 100644
index 00000000..231279b4
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.Collections.Generic;
+
+namespace SixLabors.ImageSharp.Drawing
+{
+ ///
+ /// An internal interface for shapes which are backed by
+ /// so we can have a fast path tessellating them.
+ ///
+ internal interface IInternalPathOwner
+ {
+ ///
+ /// Returns the rings as a list of -s.
+ ///
+ /// The list
+ IReadOnlyList GetRingsAsInternalPath();
+
+ // TODO: We may want to reconfigure StyleCop rules for internals to avoid unnecessary redundant trivial code comments like in this file.
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/ILineSegment.cs b/src/ImageSharp.Drawing/Shapes/ILineSegment.cs
index 12237bc1..a18f5561 100644
--- a/src/ImageSharp.Drawing/Shapes/ILineSegment.cs
+++ b/src/ImageSharp.Drawing/Shapes/ILineSegment.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using System;
using System.Collections.Generic;
using System.Numerics;
@@ -23,7 +24,7 @@ public interface ILineSegment
/// Converts the into a simple linear path..
///
/// Returns the current as simple linear path.
- IReadOnlyList Flatten();
+ ReadOnlyMemory Flatten();
///
/// Transforms the current LineSegment using specified matrix.
diff --git a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs
index 1df1a000..d8ebdfa4 100644
--- a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs
+++ b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using System;
using System.Collections.Generic;
namespace SixLabors.ImageSharp.Drawing
@@ -18,6 +19,6 @@ public interface ISimplePath
///
/// Gets the points that make this up as a simple linear path.
///
- IReadOnlyList Points { get; }
+ ReadOnlyMemory Points { get; }
}
}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Shapes/InternalPath.cs b/src/ImageSharp.Drawing/Shapes/InternalPath.cs
index 932a78c3..3f8cf4c3 100644
--- a/src/ImageSharp.Drawing/Shapes/InternalPath.cs
+++ b/src/ImageSharp.Drawing/Shapes/InternalPath.cs
@@ -7,6 +7,9 @@
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Drawing.Shapes.Rasterization;
+using SixLabors.ImageSharp.Drawing.Utilities;
+using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Drawing
{
@@ -41,8 +44,9 @@ internal class InternalPath
///
/// The segments.
/// if set to true [is closed path].
- internal InternalPath(IEnumerable segments, bool isClosedPath)
- : this(Simplify(segments, isClosedPath), isClosedPath)
+ /// Whether to remove close and collinear vertices
+ internal InternalPath(IReadOnlyList segments, bool isClosedPath, bool removeCloseAndCollinear = true)
+ : this(Simplify(segments, isClosedPath, removeCloseAndCollinear), isClosedPath)
{
}
@@ -52,7 +56,7 @@ internal InternalPath(IEnumerable segments, bool isClosedPath)
/// The segment.
/// if set to true [is closed path].
internal InternalPath(ILineSegment segment, bool isClosedPath)
- : this(segment?.Flatten() ?? Enumerable.Empty(), isClosedPath)
+ : this(segment?.Flatten() ?? Array.Empty(), isClosedPath)
{
}
@@ -61,8 +65,8 @@ internal InternalPath(ILineSegment segment, bool isClosedPath)
///
/// The points.
/// if set to true [is closed path].
- internal InternalPath(IEnumerable points, bool isClosedPath)
- : this(Simplify(points, isClosedPath), isClosedPath)
+ internal InternalPath(ReadOnlyMemory points, bool isClosedPath)
+ : this(Simplify(points, isClosedPath, true), isClosedPath)
{
}
@@ -96,7 +100,7 @@ private InternalPath(PointData[] points, bool isClosedPath)
///
/// the orrientateion of an point form a line
///
- internal enum Orientation
+ internal enum PointOrientation
{
///
/// Point is colienear
@@ -198,10 +202,10 @@ public int FindIntersections(PointF start, PointF end, Span buffer)
/// number of intersections hit
public int FindIntersections(PointF start, PointF end, Span buffer, IntersectionRule intersectionRule)
{
- Orientation[] orientations = ArrayPool.Shared.Rent(buffer.Length);
+ PointOrientation[] orientations = ArrayPool.Shared.Rent(buffer.Length);
try
{
- Span orientationsSpan = orientations.AsSpan(0, buffer.Length);
+ Span orientationsSpan = orientations.AsSpan(0, buffer.Length);
var position = this.FindIntersectionsWithOrientation(start, end, buffer, orientationsSpan);
var activeBuffer = buffer.Slice(0, position);
@@ -217,7 +221,7 @@ public int FindIntersections(PointF start, PointF end, Span buffer, Inte
}
finally
{
- ArrayPool.Shared.Return(orientations);
+ ArrayPool.Shared.Return(orientations);
}
}
@@ -230,7 +234,7 @@ public int FindIntersections(PointF start, PointF end, Span buffer, Inte
/// The buffer.
/// The buffer for storeing the orientation of each intersection.
/// number of intersections hit
- public int FindIntersectionsWithOrientation(PointF start, PointF end, Span buffer, Span orientationsSpan)
+ public int FindIntersectionsWithOrientation(PointF start, PointF end, Span buffer, Span orientationsSpan)
{
if (this.points.Length < 2)
{
@@ -261,9 +265,9 @@ public int FindIntersectionsWithOrientation(PointF start, PointF end, Span 0; i++)
@@ -271,20 +275,20 @@ public int FindIntersectionsWithOrientation(PointF start, PointF end, Span 0) &&
(IsOnSegment(target, edge.Start) || IsOnSegment(target, edge.End));
// is there any chance the segments will intersection (do their bounding boxes touch)
bool doIntersect = false;
- if (pointOrientation == Orientation.Colinear || pointOrientation != nextOrientation)
+ if (pointOrientation == PointOrientation.Colinear || pointOrientation != nextOrientation)
{
doIntersect = (edge.Min.X - Epsilon) <= target.Max.X &&
(edge.Max.X + Epsilon) >= target.Min.X &&
@@ -333,25 +337,25 @@ public int FindIntersectionsWithOrientation(PointF start, PointF end, Span buffer, Span orientationsSpan)
+ internal static int ApplyNonZeroIntersectionRules(Span buffer, Span orientationsSpan)
{
int newpositions = 0;
int tracker = 0;
@@ -404,13 +408,13 @@ internal static int ApplyNonZeroIntersectionRules(Span buffer, Span
/// The
- internal IReadOnlyList Points() => this.points.Select(x => (PointF)x.Point).ToArray();
+ internal ReadOnlyMemory Points() => this.points.Select(x => x.Point).ToArray();
///
/// Calculates the point a certain distance a path.
@@ -520,6 +524,19 @@ internal SegmentInfo PointAlongPath(float distanceAlongPath)
throw new InvalidOperationException("should alwys reach a point along the path");
}
+ internal IMemoryOwner ExtractVertices(MemoryAllocator allocator)
+ {
+ IMemoryOwner buffer = allocator.Allocate(this.points.Length + 1);
+ Span span = buffer.Memory.Span;
+
+ for (int i = 0; i < this.points.Length; i++)
+ {
+ span[i] = this.points[i].Point;
+ }
+
+ return buffer;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsOnSegment(Vector2 p, Vector2 q, Vector2 r)
{
@@ -579,7 +596,7 @@ private static int WrapArrayIndex(int i, int arrayLength)
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Orientation CalulateOrientation(Vector2 p, Vector2 q, Vector2 r)
+ private static PointOrientation CalulateOrientation(Vector2 p, Vector2 q, Vector2 r)
{
// See http://www.geeksforgeeks.org/orientation-3-ordered-points/
// for details of below formula.
@@ -589,14 +606,14 @@ private static Orientation CalulateOrientation(Vector2 p, Vector2 q, Vector2 r)
if (val > -Epsilon && val < Epsilon)
{
- return Orientation.Colinear; // colinear
+ return PointOrientation.Colinear; // colinear
}
- return (val > 0) ? Orientation.Clockwise : Orientation.Counterclockwise; // clock or counterclock wise
+ return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Orientation CalulateOrientation(Vector2 qp, Vector2 rq)
+ private static PointOrientation CalulateOrientation(Vector2 qp, Vector2 rq)
{
// See http://www.geeksforgeeks.org/orientation-3-ordered-points/
// for details of below formula.
@@ -604,10 +621,10 @@ private static Orientation CalulateOrientation(Vector2 qp, Vector2 rq)
if (val > -Epsilon && val < Epsilon)
{
- return Orientation.Colinear; // colinear
+ return PointOrientation.Colinear; // colinear
}
- return (val > 0) ? Orientation.Clockwise : Orientation.Counterclockwise; // clock or counterclock wise
+ return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise
}
///
@@ -675,23 +692,26 @@ private static Vector2 FindIntersection(in Segment source, in Segment target)
///
/// The segments.
/// Weather the path is closed or open.
+ /// Whether to remove close and collinear vertices
///
/// The .
///
- private static PointData[] Simplify(IEnumerable segments, bool isClosed)
+ private static PointData[] Simplify(IReadOnlyList segments, bool isClosed, bool removeCloseAndCollinear)
{
var simplified = new List();
+
foreach (ILineSegment seg in segments)
{
- simplified.AddRange(seg.Flatten());
+ ReadOnlyMemory points = seg.Flatten();
+ simplified.AddRange(points.ToArray());
}
- return Simplify(simplified, isClosed);
+ return Simplify(simplified.ToArray(), isClosed, removeCloseAndCollinear);
}
- private static PointData[] Simplify(IEnumerable vectors, bool isClosed)
+ private static PointData[] Simplify(ReadOnlyMemory vectors, bool isClosed, bool removeCloseAndCollinear)
{
- PointF[] points = vectors.ToArray();
+ ReadOnlySpan points = vectors.Span;
int polyCorners = points.Length;
if (polyCorners == 0)
@@ -707,7 +727,7 @@ private static PointData[] Simplify(IEnumerable vectors, bool isClosed)
results.Add(new PointData
{
Point = points[0],
- Orientation = Orientation.Colinear,
+ Orientation = PointOrientation.Colinear,
Length = 0
});
}
@@ -725,7 +745,7 @@ private static PointData[] Simplify(IEnumerable vectors, bool isClosed)
new PointData
{
Point = points[0],
- Orientation = Orientation.Colinear,
+ Orientation = PointOrientation.Colinear,
Segment = new Segment(points[0], points[next]),
Length = 0,
TotalLength = 0
@@ -734,7 +754,7 @@ private static PointData[] Simplify(IEnumerable vectors, bool isClosed)
return results.ToArray();
}
}
- while (points[0].Equivelent(points[prev], Epsilon2)); // skip points too close together
+ while (removeCloseAndCollinear && points[0].Equivalent(points[prev], Epsilon2)); // skip points too close together
polyCorners = prev + 1;
lastPoint = points[prev];
@@ -755,8 +775,8 @@ private static PointData[] Simplify(IEnumerable vectors, bool isClosed)
for (int i = 1; i < polyCorners; i++)
{
int next = WrapArrayIndex(i + 1, polyCorners);
- Orientation or = CalulateOrientation(lastPoint, points[i], points[next]);
- if (or == Orientation.Colinear && next != 0)
+ PointOrientation or = CalulateOrientation(lastPoint, points[i], points[next]);
+ if (or == PointOrientation.Colinear && next != 0)
{
continue;
}
@@ -774,10 +794,10 @@ private static PointData[] Simplify(IEnumerable vectors, bool isClosed)
lastPoint = points[i];
}
- if (isClosed)
+ if (isClosed && removeCloseAndCollinear)
{
// walk back removing collinear points
- while (results.Count > 2 && results.Last().Orientation == Orientation.Colinear)
+ while (results.Count > 2 && results.Last().Orientation == PointOrientation.Colinear)
{
results.RemoveAt(results.Count - 1);
}
@@ -837,36 +857,6 @@ private void ClampPoints(ref PointF start, ref PointF end)
}
}
- ///
- /// Returns the length of the path.
- ///
- ///
- /// The .
- ///
- private float CalculateLength()
- {
- float length = 0;
- int polyCorners = this.points.Length;
-
- if (!this.closedPath)
- {
- polyCorners -= 1;
- }
-
- for (int i = 0; i < polyCorners; i++)
- {
- int next = i + 1;
- if (this.closedPath && next == polyCorners)
- {
- next = 0;
- }
-
- length += this.points[i].Length;
- }
-
- return length;
- }
-
///
/// Calculate any shorter distances along the path.
///
@@ -933,7 +923,7 @@ private struct PointInfoInternal
private struct PointData
{
public PointF Point;
- public Orientation Orientation;
+ public PointOrientation Orientation;
public float Length;
public float TotalLength;
@@ -943,7 +933,7 @@ private struct PointData
private struct PassPointData
{
public bool RemoveLastIntersectionAndSkip;
- public Orientation RelativeOrientation;
+ public PointOrientation RelativeOrientation;
public bool DoIntersect;
}
diff --git a/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs b/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs
index 1b58807f..ea03a9f2 100644
--- a/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs
+++ b/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs
@@ -66,7 +66,7 @@ public LinearLineSegment(PointF[] points)
///
/// Returns the current as simple linear path.
///
- public IReadOnlyList Flatten() => this.points;
+ public ReadOnlyMemory Flatten() => this.points;
///
/// Transforms the current LineSegment using specified matrix.
diff --git a/src/ImageSharp.Drawing/Shapes/Outliner.cs b/src/ImageSharp.Drawing/Shapes/Outliner.cs
index af65a017..e226849f 100644
--- a/src/ImageSharp.Drawing/Shapes/Outliner.cs
+++ b/src/ImageSharp.Drawing/Shapes/Outliner.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
+using System.Runtime.InteropServices;
using ClipperLib;
namespace SixLabors.ImageSharp.Drawing
@@ -94,21 +95,22 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan points = p.Points.Span;
// Create a new list of points representing the new outline
- int pCount = p.Points.Count;
+ int pCount = points.Length;
if (!p.IsClosed)
{
pCount--;
}
int i = 0;
- Vector2 currentPoint = p.Points[0];
+ Vector2 currentPoint = points[0];
while (i < pCount)
{
- int next = (i + 1) % p.Points.Count;
- Vector2 targetPoint = p.Points[next];
+ int next = (i + 1) % points.Length;
+ Vector2 targetPoint = points[next];
float distToNext = Vector2.Distance(currentPoint, targetPoint);
if (distToNext > targetLength)
{
@@ -148,11 +150,11 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan paths = path.Flatten();
foreach (ISimplePath p in paths)
{
- IReadOnlyList vectors = p.Points;
- var points = new List(vectors.Count);
+ ReadOnlySpan vectors = MemoryMarshal.Cast(p.Points.Span);
+ var points = new List(vectors.Length);
foreach (Vector2 v in vectors)
{
points.Add(new IntPoint(v.X * ScalingFactor, v.Y * ScalingFactor));
diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs
index b279910c..25eb858b 100644
--- a/src/ImageSharp.Drawing/Shapes/Path.cs
+++ b/src/ImageSharp.Drawing/Shapes/Path.cs
@@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Drawing
/// A aggregate of s making a single logical path
///
///
- public class Path : IPath, ISimplePath
+ public class Path : IPath, ISimplePath, IInternalPathOwner
{
private readonly ILineSegment[] lineSegments;
private InternalPath innerPath;
@@ -57,7 +57,7 @@ public Path(params ILineSegment[] segments)
///
/// Gets the points that make up this simple linear path.
///
- IReadOnlyList ISimplePath.Points => this.InnerPath.Points();
+ ReadOnlyMemory ISimplePath.Points => this.InnerPath.Points();
///
public RectangleF Bounds => this.InnerPath.Bounds;
@@ -82,7 +82,13 @@ public Path(params ILineSegment[] segments)
///
protected virtual bool IsClosed => false;
- private InternalPath InnerPath => this.innerPath ?? (this.innerPath = new InternalPath(this.lineSegments, this.IsClosed));
+ ///
+ /// Gets or sets a value indicating whether close or collinear vertices should be removed. TEST ONLY!
+ ///
+ internal bool RemoveCloseAndCollinearPoints { get; set; } = true;
+
+ private InternalPath InnerPath =>
+ this.innerPath ??= new InternalPath(this.lineSegments, this.IsClosed, this.RemoveCloseAndCollinearPoints);
///
public PointInfo Distance(PointF point)
@@ -199,5 +205,8 @@ public SegmentInfo PointAlongPath(float distanceAlongPath)
{
return this.InnerPath.PointAlongPath(distanceAlongPath);
}
+
+ ///
+ IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() => new[] { this.InnerPath };
}
}
diff --git a/src/ImageSharp.Drawing/Shapes/PathExtensions.cs b/src/ImageSharp.Drawing/Shapes/PathExtensions.cs
index 54e377c0..8c7b48e6 100644
--- a/src/ImageSharp.Drawing/Shapes/PathExtensions.cs
+++ b/src/ImageSharp.Drawing/Shapes/PathExtensions.cs
@@ -4,6 +4,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
+using System.Linq;
using System.Numerics;
namespace SixLabors.ImageSharp.Drawing
@@ -177,5 +178,17 @@ public static IEnumerable FindIntersections(this IPath path, PointF star
ArrayPool.Shared.Return(buffer);
}
}
+
+ internal static IPath Reverse(this IPath path)
+ {
+ var segments = path.Flatten().Select(p => new LinearLineSegment(p.Points.ToArray().Reverse().ToArray()));
+ bool closed = false;
+ if (path is ISimplePath sp)
+ {
+ closed = sp.IsClosed;
+ }
+
+ return closed ? new Polygon(segments) : new Path(segments);
+ }
}
}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs
index b2b117ff..ddafbc94 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs
@@ -138,9 +138,9 @@ public void AddPath(IPath path, ClippingType clippingType)
/// AddPath: Open paths have been disabled.
internal void AddPath(ISimplePath path, ClippingType clippingType)
{
- IReadOnlyList vectors = path.Points;
+ ReadOnlySpan vectors = path.Points.Span;
- var points = new List(vectors.Count);
+ var points = new List(vectors.Length);
foreach (PointF v in vectors)
{
points.Add(new IntPoint(v.X * ScalingFactor, v.Y * ScalingFactor));
diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs
new file mode 100644
index 00000000..22be631c
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs
@@ -0,0 +1,289 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Drawing.Utilities;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization
+{
+ internal enum NonZeroIntersectionType
+ {
+ Down,
+ Up,
+ Corner,
+ CornerDummy
+ }
+
+ ///
+ /// The list of active edges as an index buffer into .
+ ///
+ internal ref struct ActiveEdgeList
+ {
+ private const int EnteringEdgeFlag = 1 << 30;
+ private const int LeavingEdgeFlag = 1 << 31;
+ private const int MaxEdges = EnteringEdgeFlag - 1;
+
+ private const int StripMask = ~(EnteringEdgeFlag | LeavingEdgeFlag);
+
+ private const float NonzeroSortingHelperEpsilon = 1e-4f;
+
+ private int count;
+ internal readonly Span Buffer;
+
+ public ActiveEdgeList(Span buffer)
+ {
+ this.count = 0;
+ this.Buffer = buffer;
+ }
+
+ private Span ActiveEdges => this.Buffer.Slice(0, this.count);
+
+ public void EnterEdge(int edgeIdx)
+ {
+ this.Buffer[this.count++] = edgeIdx | EnteringEdgeFlag;
+ }
+
+ public void LeaveEdge(int edgeIdx)
+ {
+ Span active = this.ActiveEdges;
+ for (int i = 0; i < active.Length; i++)
+ {
+ if (active[i] == edgeIdx)
+ {
+ active[i] |= LeavingEdgeFlag;
+ return;
+ }
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(edgeIdx));
+ }
+
+ public void RemoveLeavingEdges()
+ {
+ int offset = 0;
+
+ Span active = this.ActiveEdges;
+
+ for (int i = 0; i < active.Length; i++)
+ {
+ int flaggedIdx = active[i];
+ int edgeIdx = Strip(flaggedIdx);
+ if (IsLeaving(flaggedIdx))
+ {
+ offset++;
+ }
+ else
+ {
+ // Unmask and offset:
+ active[i - offset] = edgeIdx;
+ }
+ }
+
+ this.count -= offset;
+ }
+
+ public Span ScanOddEven(float y, Span edges, Span intersections)
+ {
+ DebugGuard.MustBeLessThanOrEqualTo(edges.Length, MaxEdges, "edges.Length");
+
+ int intersectionCounter = 0;
+ int offset = 0;
+
+ Span active = this.ActiveEdges;
+
+ for (int i = 0; i < active.Length; i++)
+ {
+ int flaggedIdx = active[i];
+ int edgeIdx = Strip(flaggedIdx);
+ ref ScanEdge edge = ref edges[edgeIdx];
+ float x = edge.GetX(y);
+ if (IsEntering(flaggedIdx))
+ {
+ Emit(x, edge.EmitV0, intersections, ref intersectionCounter);
+ }
+ else if (IsLeaving(flaggedIdx))
+ {
+ Emit(x, edge.EmitV1, intersections, ref intersectionCounter);
+
+ offset++;
+
+ // Do not offset:
+ continue;
+ }
+ else
+ {
+ // Emit once:
+ intersections[intersectionCounter++] = x;
+ }
+
+ // Unmask and offset:
+ active[i - offset] = edgeIdx;
+ }
+
+ this.count -= offset;
+
+ intersections = intersections.Slice(0, intersectionCounter);
+ SortUtility.Sort(intersections);
+ return intersections;
+ }
+
+ public Span ScanNonZero(
+ float y,
+ Span edges,
+ Span intersections,
+ Span intersectionTypes)
+ {
+ DebugGuard.MustBeLessThanOrEqualTo(edges.Length, MaxEdges, "edges.Length");
+
+ int intersectionCounter = 0;
+ int offset = 0;
+
+ Span active = this.ActiveEdges;
+
+ for (int i = 0; i < active.Length; i++)
+ {
+ int flaggedIdx = active[i];
+ int edgeIdx = Strip(flaggedIdx);
+ ref ScanEdge edge = ref edges[edgeIdx];
+ bool edgeUp = edge.EdgeUp;
+ float x = edge.GetX(y);
+ if (IsEntering(flaggedIdx))
+ {
+ EmitNonZero(x, edge.EmitV0, edgeUp, intersections, intersectionTypes, ref intersectionCounter);
+ }
+ else if (IsLeaving(flaggedIdx))
+ {
+ EmitNonZero(x, edge.EmitV1, edgeUp, intersections, intersectionTypes, ref intersectionCounter);
+
+ offset++;
+
+ // Do not offset:
+ continue;
+ }
+ else
+ {
+ // Emit once:
+ if (edgeUp)
+ {
+ intersectionTypes[intersectionCounter] = NonZeroIntersectionType.Up;
+ intersections[intersectionCounter++] = x + NonzeroSortingHelperEpsilon;
+ }
+ else
+ {
+ intersectionTypes[intersectionCounter] = NonZeroIntersectionType.Down;
+ intersections[intersectionCounter++] = x - NonzeroSortingHelperEpsilon;
+ }
+ }
+
+ // Unmask and offset:
+ active[i - offset] = edgeIdx;
+ }
+
+ this.count -= offset;
+
+ intersections = intersections.Slice(0, intersectionCounter);
+ intersectionTypes = intersectionTypes.Slice(0, intersectionCounter);
+ SortUtility.Sort(intersections, intersectionTypes);
+
+ return ApplyNonzeroRule(intersections, intersectionTypes);
+ }
+
+ private static Span ApplyNonzeroRule(Span intersections, Span intersectionTypes)
+ {
+ int offset = 0;
+ int tracker = 0;
+
+ for (int i = 0; i < intersectionTypes.Length; i++)
+ {
+ NonZeroIntersectionType type = intersectionTypes[i];
+ if (type == NonZeroIntersectionType.CornerDummy)
+ {
+ // we skip this one so we can emit twice on actual "Corner"
+ offset++;
+ }
+ else if (type == NonZeroIntersectionType.Corner)
+ {
+ // Assume a Down, Up serie
+ NonzeroEmitIfNeeded(intersections, i, -1, intersections[i], ref tracker, ref offset);
+ offset -= 1;
+ NonzeroEmitIfNeeded(intersections, i, 1, intersections[i], ref tracker, ref offset);
+ }
+ else
+ {
+ int diff = type == NonZeroIntersectionType.Up ? 1 : -1;
+ float emitVal = intersections[i] + (NonzeroSortingHelperEpsilon * diff * -1);
+ NonzeroEmitIfNeeded(intersections, i, diff, emitVal, ref tracker, ref offset);
+ }
+ }
+
+ return intersections.Slice(0, intersections.Length - offset);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void NonzeroEmitIfNeeded(Span intersections, int i, int diff, float emitVal, ref int tracker, ref int offset)
+ {
+ bool emit = (tracker == 0 && diff != 0) || tracker * diff == -1;
+ tracker += diff;
+
+ if (emit)
+ {
+ intersections[i - offset] = emitVal;
+ }
+ else
+ {
+ offset++;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Emit(float x, int times, Span emitSpan, ref int emitCounter)
+ {
+ if (times > 1)
+ {
+ emitSpan[emitCounter++] = x;
+ }
+
+ if (times > 0)
+ {
+ emitSpan[emitCounter++] = x;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void EmitNonZero(float x, int times, bool edgeUp, Span emitSpan, Span intersectionTypes, ref int emitCounter)
+ {
+ if (times == 2)
+ {
+ intersectionTypes[emitCounter] = NonZeroIntersectionType.CornerDummy;
+ emitSpan[emitCounter++] = x - NonzeroSortingHelperEpsilon; // To make sure the "dummy" point precedes the actual one
+
+ intersectionTypes[emitCounter] = NonZeroIntersectionType.Corner;
+ emitSpan[emitCounter++] = x;
+ }
+ else if (times == 1)
+ {
+ if (edgeUp)
+ {
+ intersectionTypes[emitCounter] = NonZeroIntersectionType.Up;
+ emitSpan[emitCounter++] = x + NonzeroSortingHelperEpsilon;
+ }
+ else
+ {
+ intersectionTypes[emitCounter] = NonZeroIntersectionType.Down;
+ emitSpan[emitCounter++] = x - NonzeroSortingHelperEpsilon;
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int Strip(int flaggedIdx) => flaggedIdx & StripMask;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsEntering(int flaggedIdx) => (flaggedIdx & EnteringEdgeFlag) == EnteringEdgeFlag;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsLeaving(int flaggedIdx) => (flaggedIdx & LeavingEdgeFlag) == LeavingEdgeFlag;
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs
new file mode 100644
index 00000000..b8c49691
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs
@@ -0,0 +1,224 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Drawing.Utilities;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization
+{
+ internal ref struct PolygonScanner
+ {
+ private readonly int minY;
+ private readonly int maxY;
+ private readonly IntersectionRule intersectionRule;
+ private ScanEdgeCollection edgeCollection;
+ private Span edges;
+
+ // Common contiguous buffer for sorted0, sorted1, intersections, activeEdges [,intersectionTypes]
+ private IMemoryOwner dataBuffer;
+
+ // | <- edgeCnt -> | <- edgeCnt -> | <- edgeCnt -> | <- maxIntersectionCount -> | <- maxIntersectionCount -> |
+ // |---------------|---------------|---------------|----------------------------|----------------------------|
+ // | sorted0 | sorted1 | activeEdges | intersections | intersectionTypes |
+ // |---------------|---------------|---------------|----------------------------|----------------------------|
+ private Span sorted0;
+ private Span sorted1;
+ private ActiveEdgeList activeEdges;
+ private Span intersections;
+ private Span intersectionTypes;
+
+ private int idx0;
+ private int idx1;
+ private float yPlusOne;
+
+ public readonly float SubpixelDistance;
+ public readonly float SubpixelArea;
+ public int PixelLineY;
+ public float SubPixelY;
+
+ private PolygonScanner(
+ ScanEdgeCollection edgeCollection,
+ int maxIntersectionCount,
+ int minY,
+ int maxY,
+ int subsampling,
+ IntersectionRule intersectionRule,
+ MemoryAllocator allocator)
+ {
+ this.minY = minY;
+ this.maxY = maxY;
+ this.SubpixelDistance = 1f / subsampling;
+ this.SubpixelArea = this.SubpixelDistance / subsampling;
+ this.intersectionRule = intersectionRule;
+ this.edgeCollection = edgeCollection;
+ this.edges = edgeCollection.Edges;
+ int edgeCount = this.edges.Length;
+ int dataBufferSize = (edgeCount * 3) + maxIntersectionCount;
+
+ // In case of IntersectionRule.Nonzero, we need more space for intersectionTypes:
+ if (intersectionRule == IntersectionRule.Nonzero)
+ {
+ dataBufferSize += maxIntersectionCount;
+ }
+
+ this.dataBuffer = allocator.Allocate(dataBufferSize);
+ Span dataBufferInt32Span = this.dataBuffer.Memory.Span;
+ Span dataBufferFloatSpan = MemoryMarshal.Cast(dataBufferInt32Span);
+
+ this.sorted0 = dataBufferInt32Span.Slice(0, edgeCount);
+ this.sorted1 = dataBufferInt32Span.Slice(edgeCount, edgeCount);
+ this.activeEdges = new ActiveEdgeList(dataBufferInt32Span.Slice(edgeCount * 2, edgeCount));
+ this.intersections = dataBufferFloatSpan.Slice(edgeCount * 3, maxIntersectionCount);
+ if (intersectionRule == IntersectionRule.Nonzero)
+ {
+ Span remainder =
+ dataBufferInt32Span.Slice((edgeCount * 3) + maxIntersectionCount, maxIntersectionCount);
+ this.intersectionTypes = MemoryMarshal.Cast(remainder);
+ }
+ else
+ {
+ this.intersectionTypes = default;
+ }
+
+ this.idx0 = 0;
+ this.idx1 = 0;
+ this.PixelLineY = minY - 1;
+ this.SubPixelY = default;
+ this.yPlusOne = default;
+ }
+
+ public static PolygonScanner Create(
+ IPath polygon,
+ int minY,
+ int maxY,
+ int subsampling,
+ IntersectionRule intersectionRule,
+ MemoryAllocator allocator)
+ {
+ var multipolygon = TessellatedMultipolygon.Create(polygon, allocator);
+ var edges = ScanEdgeCollection.Create(multipolygon, allocator, subsampling);
+ var scanner = new PolygonScanner(edges, multipolygon.TotalVertexCount * 2, minY, maxY, subsampling, intersectionRule, allocator);
+ scanner.Init();
+ return scanner;
+ }
+
+ private void Init()
+ {
+ // Reuse memory buffers of 'intersections' and 'activeEdges' for key-value sorting,
+ // since that region is unused at initialization time.
+ Span keys0 = this.intersections.Slice(0, this.sorted0.Length);
+ Span keys1 = MemoryMarshal.Cast(this.activeEdges.Buffer);
+
+ for (int i = 0; i < this.edges.Length; i++)
+ {
+ ref ScanEdge edge = ref this.edges[i];
+ keys0[i] = edge.Y0;
+ keys1[i] = edge.Y1;
+ this.sorted0[i] = i;
+ this.sorted1[i] = i;
+ }
+
+ SortUtility.Sort(keys0, this.sorted0);
+ SortUtility.Sort(keys1, this.sorted1);
+
+ this.SkipEdgesBeforeMinY();
+ }
+
+ private void SkipEdgesBeforeMinY()
+ {
+ if (this.edges.Length == 0)
+ {
+ return;
+ }
+
+ this.SubPixelY = this.edges[this.sorted0[0]].Y0;
+
+ int i0 = 1;
+ int i1 = 0;
+
+ // Do fake scans for the lines belonging to edge start and endpoints before minY
+ while (this.SubPixelY < this.minY)
+ {
+ this.EnterEdges();
+ this.LeaveEdges();
+ this.activeEdges.RemoveLeavingEdges();
+
+ float y0 = this.edges[this.sorted0[i0]].Y0;
+ float y1 = this.edges[this.sorted1[i1]].Y1;
+
+ if (y0 < y1)
+ {
+ this.SubPixelY = y0;
+ i0++;
+ }
+ else
+ {
+ this.SubPixelY = y1;
+ i1++;
+ }
+ }
+ }
+
+ public bool MoveToNextPixelLine()
+ {
+ this.PixelLineY++;
+ this.yPlusOne = this.PixelLineY + 1;
+ this.SubPixelY = this.PixelLineY - this.SubpixelDistance;
+ return this.PixelLineY < this.maxY;
+ }
+
+ public bool MoveToNextSubpixelScanLine()
+ {
+ this.SubPixelY += this.SubpixelDistance;
+ this.EnterEdges();
+ this.LeaveEdges();
+ return this.SubPixelY < this.yPlusOne;
+ }
+
+ public ReadOnlySpan ScanCurrentLine()
+ {
+ return this.intersectionRule == IntersectionRule.OddEven
+ ? this.activeEdges.ScanOddEven(this.SubPixelY, this.edges, this.intersections)
+ : this.activeEdges.ScanNonZero(this.SubPixelY, this.edges, this.intersections, this.intersectionTypes);
+ }
+
+ public void Dispose()
+ {
+ this.edgeCollection.Dispose();
+ this.dataBuffer.Dispose();
+ }
+
+ private void EnterEdges()
+ {
+ while (this.idx0 < this.sorted0.Length)
+ {
+ int edge0 = this.sorted0[this.idx0];
+ if (this.edges[edge0].Y0 > this.SubPixelY)
+ {
+ break;
+ }
+
+ this.activeEdges.EnterEdge(edge0);
+ this.idx0++;
+ }
+ }
+
+ private void LeaveEdges()
+ {
+ while (this.idx1 < this.sorted1.Length)
+ {
+ int edge1 = this.sorted1[this.idx1];
+ if (this.edges[edge1].Y1 > this.SubPixelY)
+ {
+ break;
+ }
+
+ this.activeEdges.LeaveEdge(edge1);
+ this.idx1++;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD
new file mode 100644
index 00000000..26966763
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD
@@ -0,0 +1,76 @@
+# Polygon Scanning with Active Edge List
+
+Scanning is done with a variant of the ["Active Edge Table" algorithm](https://en.wikipedia.org/wiki/Scanline_rendering#Algorithm), that doesn't build a table beforehand, just maintains the list of currently active edges.
+
+After rasterizing polygons a collection of non-horizontal edges (ScanEdge) is extracted into ScanEdgeCollection. These are then sorted by minimum and maximum Y coordinate, which enables the maintanance of the Active Edge List as we traverse the collection from `minY` to `maxY`.
+
+When intersecting a ScanEdge start (Y0) and end (Y1) intersections have special handling. Since these belong to vertices (connection points) sometimes we need to emit the intersection point 2 times. In other cases we do not want to emit it at all.
+
+### Illustration
+
+Consider the following polygon with 4 non-horizontal ScanEdge-s, being intersected by scanlines `SCANLINE 1` and `SCANLINE 2`:
+
+```
+ + - - - - - - - - - - - - - - - - +
+ | (1) (1)\
+ | \ B
+ | \
+ | (0) \
+SCANLINE 1 >>>> | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> | (1) >>>>>>>>>
+ | |
+ | A C |
+ | |
+ | (2) X | (1)
+SCANLINE 2 >>>> | >>>>>>>>>>>>>> + - - - - - - - - - - + >>>>>>>>>
+ | |
+ | |
+ | D |
+ | |
+ | (1) | (1)
+ + - - - - - - - +
+
+```
+
+
+#### Intersections at SCANLINE 1
+
+- Intersection with edge A is trivial, since it's being intersected on an internal point of the edge
+- The second intersection is more tricky: the intersection point is at the connection (vertex) between edges B and C, but we do not want to emit the intersection 2 times.
+ - To avoid this, when checking the scanline's collision against edge B we emit 0 intersections at it's endpoint (Y1), when checking against edge C we emit 1 point at its start point (Y0)
+
+#### Intersections at SCANLINE 2
+
+- Intersection with edge A is trivial, since it's being intersected on an internal point
+- However the rest is tricky: We want to to emulate the intersection with the collinear edge X not being listed in `ScanEdgeCollection`.
+ - The easiest way is to emit a point pair for the line part between A-D and a second point pair for D-C (to emulate the intersection with X)
+ - To achieve this, we should emit the start point (Y0) of the D edge 2 times when intersecting it!
+
+### Edge emit rules
+
+The emit rules are there to provide a consistent way for intersecting scanlines as described in the previous "Illustration" part, handling all corner cases.
+These rules only work well, when:
+- The outline polygons are Clockwise in screen-space (= "has positive orientation" according to the terminlogy used in the repository)
+- Holes have Counter-Clockwise ("negative") orientation.
+
+Most real-world inputs tend to follow these rules, however intersecting polygons which do not do so, leads to inaccuracies around horizontal edges. These inaccuracies are visually acceptable.
+
+The rules apply to vertices (edge connections). `⟶` and `⟵` edges are horizontal, `↑` and `↓` edges are non-horizontal.
+
+Edge In | Edge Out | Emit on "Edge In" | Emit on "Edge out"
+-- | -- | -- | --
+↑ | ↑ | 0 | 1
+↑ | ↓ | 1 | 1
+↑ | ⟵ | 2 | 0
+↑ | ⟶ | 1 | 0
+↓ | ↑ | 1 | 1
+↓ | ↓ | 0 | 1
+↓ | ⟵ | 1 | 0
+↓ | ⟶ | 2 | 0
+⟵ | ↑ | 0 | 1
+⟵ | ↓ | 0 | 2
+⟵ | ⟵ | 0 | 0
+⟵ | ⟶ | 0 | 0
+⟶ | ↑ | 0 | 2
+⟶ | ↓ | 0 | 1
+⟶ | ⟵ | 0 | 0
+⟶ | ⟶ | 0 | 0
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerExtensions.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerExtensions.cs
new file mode 100644
index 00000000..9be45bdd
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerExtensions.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using SixLabors.ImageSharp.Drawing.Utilities;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization
+{
+ internal static class RasterizerExtensions
+ {
+ public static bool ScanCurrentPixelLineInto(this ref PolygonScanner scanner, int minX, float xOffset, Span scanline)
+ {
+ bool scanlineDirty = false;
+ while (scanner.MoveToNextSubpixelScanLine())
+ {
+ scanner.ScanCurrentSubpixelLineInto(minX, xOffset, scanline, ref scanlineDirty);
+ }
+
+ return scanlineDirty;
+ }
+
+ private static void ScanCurrentSubpixelLineInto(this ref PolygonScanner scanner, int minX, float xOffset, Span scanline, ref bool scanlineDirty)
+ {
+ ReadOnlySpan points = scanner.ScanCurrentLine();
+ if (points.Length == 0)
+ {
+ // nothing on this line, skip
+ return;
+ }
+
+ for (int point = 0; point < points.Length - 1; point += 2)
+ {
+ // points will be paired up
+ float scanStart = points[point] - minX;
+ float scanEnd = points[point + 1] - minX;
+ int startX = (int)MathF.Floor(scanStart + xOffset);
+ int endX = (int)MathF.Floor(scanEnd + xOffset);
+
+ if (startX >= 0 && startX < scanline.Length)
+ {
+ // Originally, this was implemented by a loop.
+ // It's possible to emulate the old behavior with MathF.Ceiling,
+ // but omitting the rounding seems to produce more accurate results.
+ // float subpixelWidth = MathF.Ceiling((startX + 1 - scanStart) / scanner.SubpixelDistance);
+ float subpixelWidth = (startX + 1 - scanStart) / scanner.SubpixelDistance;
+
+ scanline[startX] += subpixelWidth * scanner.SubpixelArea;
+ scanlineDirty = subpixelWidth > 0;
+ }
+
+ if (endX >= 0 && endX < scanline.Length)
+ {
+ // float subpixelWidth = MathF.Ceiling((scanEnd - endX) / scanner.SubpixelDistance);
+ float subpixelWidth = (scanEnd - endX) / scanner.SubpixelDistance;
+
+ scanline[endX] += subpixelWidth * scanner.SubpixelArea;
+ scanlineDirty = subpixelWidth > 0;
+ }
+
+ int nextX = startX + 1;
+ endX = Math.Min(endX, scanline.Length); // reduce to end to the right edge
+ nextX = Math.Max(nextX, 0);
+
+ if (endX > nextX)
+ {
+ scanline.Slice(nextX, endX - nextX).AddToAllElements(scanner.SubpixelDistance);
+ scanlineDirty = true;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdge.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdge.cs
new file mode 100644
index 00000000..4a3c9f86
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdge.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization
+{
+ ///
+ /// Holds coordinates, and coefficients for a polygon edge to be horizontally scanned.
+ /// The edge's segment is defined with the reciprocal slope form:
+ /// x = p * y + q
+ ///
+ internal readonly struct ScanEdge
+ {
+ public readonly float Y0;
+ public readonly float Y1;
+ private readonly float p;
+ private readonly float q;
+
+ // Store 3 small values in a single Int32, to make EdgeData more compact:
+ // EdgeUp, Emit0, Emit1
+ private readonly int flags;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal ScanEdge(PointF p0, PointF p1, int flags)
+ {
+ this.Y0 = p0.Y;
+ this.Y1 = p1.Y;
+ this.flags = flags;
+ float dy = p1.Y - p0.Y;
+
+ // To improve accuracy, center the edge around zero before calculating the coefficients:
+ float cx = (p0.X + p1.X) * 0.5f;
+ float cy = (p0.Y + p1.Y) * 0.5f;
+ p0.X -= cx;
+ p0.Y -= cy;
+ p1.X -= cx;
+ p1.Y -= cy;
+
+ this.p = (p1.X - p0.X) / dy;
+ this.q = ((p0.X * p1.Y) - (p1.X * p0.Y)) / dy;
+
+ // After centering, the equation would be:
+ // x = p * (y-cy) + q + cx
+ // Adjust the coefficients, so we no longer need (cx,cy):
+ this.q += cx - (this.p * cy);
+ }
+
+ // True when non-horizontal edge is oriented upwards in screen coords
+ public bool EdgeUp => (this.flags & 1) == 1;
+
+ // How many times to include the intersection result
+ // When the scanline intersects the endpoint at Y0.
+ public int EmitV0 => (this.flags & 0b00110) >> 1;
+
+ // How many times to include the intersection result
+ // When the scanline intersects the endpoint at Y1.
+ public int EmitV1 => (this.flags & 0b11000) >> 3;
+
+ public float GetX(float y) => (this.p * y) + this.q;
+
+ public override string ToString()
+ => $"(Y0={this.Y0} Y1={this.Y1} E0={this.EmitV0} E1={this.EmitV1} {(this.EdgeUp ? "Up" : "Down")} p={this.p} q={this.q})";
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs
new file mode 100644
index 00000000..65175dfb
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs
@@ -0,0 +1,318 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization
+{
+ internal partial class ScanEdgeCollection
+ {
+ private enum EdgeCategory
+ {
+ Up = 0, // Non-horizontal
+ Down, // Non-horizontal
+ Left, // Horizontal
+ Right, // Horizontal
+ }
+
+ // A pair of EdgeCategories at a given vertex, defined as (fromEdge.EdgeCategory, toEdge.EdgeCategory)
+ private enum VertexCategory
+ {
+ UpUp = 0,
+ UpDown,
+ UpLeft,
+ UpRight,
+
+ DownUp,
+ DownDown,
+ DownLeft,
+ DownRight,
+
+ LeftUp,
+ LeftDown,
+ LeftLeft,
+ LeftRight,
+
+ RightUp,
+ RightDown,
+ RightLeft,
+ RightRight,
+ }
+
+ internal static ScanEdgeCollection Create(TessellatedMultipolygon multipolygon, MemoryAllocator allocator, int subsampling)
+ {
+ // We allocate more than we need, since we don't know how many horizontal edges do we have:
+ IMemoryOwner buffer = allocator.Allocate(multipolygon.TotalVertexCount);
+
+ RingWalker walker = new RingWalker(buffer.Memory.Span);
+
+ using IMemoryOwner roundedYBuffer = allocator.Allocate(multipolygon.Max(r => r.Vertices.Length));
+ Span roundedY = roundedYBuffer.Memory.Span;
+
+ foreach (TessellatedMultipolygon.Ring ring in multipolygon)
+ {
+ if (ring.VertexCount < 3)
+ {
+ continue;
+ }
+
+ ReadOnlySpan vertices = ring.Vertices;
+ RoundY(vertices, roundedY, subsampling);
+
+ walker.PreviousEdge = new EdgeData(vertices, roundedY, vertices.Length - 2); // Last edge
+ walker.CurrentEdge = new EdgeData(vertices, roundedY, 0); // First edge
+ walker.NextEdge = new EdgeData(vertices, roundedY, 1); // Second edge
+ walker.Move(false);
+
+ for (int i = 1; i < vertices.Length - 2; i++)
+ {
+ walker.NextEdge = new EdgeData(vertices, roundedY, i + 1);
+ walker.Move(true);
+ }
+
+ walker.NextEdge = new EdgeData(vertices, roundedY, 0); // First edge
+ walker.Move(true); // Emit edge before last edge
+
+ walker.NextEdge = new EdgeData(vertices, roundedY, 1); // Second edge
+ walker.Move(true); // Emit last edge
+ }
+
+ static void RoundY(ReadOnlySpan vertices, Span destination, float subsamplingRatio)
+ {
+ for (int i = 0; i < vertices.Length; i++)
+ {
+ // for future SIMD impl:
+ // https://www.ocf.berkeley.edu/~horie/rounding.html
+ // Avx.RoundToPositiveInfinity()
+ destination[i] = MathF.Round(vertices[i].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio;
+ }
+ }
+
+ return new ScanEdgeCollection(buffer, walker.EdgeCounter);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static VertexCategory CreateVertexCategory(EdgeCategory previousCategory, EdgeCategory currentCategory)
+ {
+ var value = (VertexCategory)(((int)previousCategory << 2) | (int)currentCategory);
+ VerifyVertexCategory(value);
+ return value;
+ }
+
+ [Conditional("DEBUG")]
+ private static void VerifyVertexCategory(VertexCategory vertexCategory)
+ {
+ int value = (int)vertexCategory;
+ if (value < 0 || value >= 16)
+ {
+ throw new Exception("EdgeCategoryPair value shall be: 0 <= value < 16");
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void ThrowInvalidRing(string message)
+ {
+ throw new InvalidOperationException(message);
+ }
+
+ private struct EdgeData
+ {
+ public EdgeCategory EdgeCategory;
+
+ private PointF start;
+ private PointF end;
+ private int emitStart;
+ private int emitEnd;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public EdgeData(ReadOnlySpan vertices, ReadOnlySpan roundedY, int idx)
+ : this(
+ vertices[idx].X,
+ vertices[idx + 1].X,
+ roundedY[idx],
+ roundedY[idx + 1])
+ {
+ }
+
+ public EdgeData(float startX, float endX, float startYRounded, float endYRounded)
+ {
+ this.start = new PointF(startX, startYRounded);
+ this.end = new PointF(endX, endYRounded);
+
+ if (this.start.Y == this.end.Y)
+ {
+ this.EdgeCategory = this.start.X < this.end.X ? EdgeCategory.Right : EdgeCategory.Left;
+ }
+ else
+ {
+ this.EdgeCategory = this.start.Y < this.end.Y ? EdgeCategory.Down : EdgeCategory.Up;
+ }
+
+ this.emitStart = 0;
+ this.emitEnd = 0;
+ }
+
+ public void EmitScanEdge(Span edges, ref int edgeCounter)
+ {
+ if (this.EdgeCategory == EdgeCategory.Left || this.EdgeCategory == EdgeCategory.Right)
+ {
+ return;
+ }
+
+ edges[edgeCounter++] = this.ToScanEdge();
+ }
+
+ public static void ApplyVertexCategory(
+ VertexCategory vertexCategory,
+ ref EdgeData fromEdge,
+ ref EdgeData toEdge)
+ {
+ // On PolygonScanner needs to handle intersections at edge connections (vertices) in a special way:
+ // - We need to make sure we do not report ("emit") an intersection point more times than necessary because we detected the intersection at both edges.
+ // - We need to make sure we we emit proper intersection points when scanning through a horizontal line
+ // In practice this means that vertex intersections have to emitted: 0-2 times in total:
+ // - Do not emit on vertex of collinear edges
+ // - Emit 2 times if:
+ // - One of the edges is horizontal
+ // - The corner is concave
+ // (The reason for tis rule is that we do not scan horizontal edges)
+ // - Emit once otherwise
+ // Since PolygonScanner does not process vertices, only edges, we need to define arbitrary rules
+ // about WHERE (on which edge) do we emit the vertex intersections.
+ // For visualization of the rules see:
+ // PoygonScanning.MD
+ // For an example, see:
+ // ImageSharp.Drawing.Tests/Shapes/Scan/SimplePolygon_AllEmitCases.png
+ switch (vertexCategory)
+ {
+ case VertexCategory.UpUp:
+ // 0, 1
+ toEdge.emitStart = 1;
+ break;
+ case VertexCategory.UpDown:
+ // 1, 1
+ toEdge.emitStart = 1;
+ fromEdge.emitEnd = 1;
+ break;
+ case VertexCategory.UpLeft:
+ // 2, 0
+ fromEdge.emitEnd = 2;
+ break;
+ case VertexCategory.UpRight:
+ // 1, 0
+ fromEdge.emitEnd = 1;
+ break;
+ case VertexCategory.DownUp:
+ // 1, 1
+ toEdge.emitStart = 1;
+ fromEdge.emitEnd = 1;
+ break;
+ case VertexCategory.DownDown:
+ // 0, 1
+ toEdge.emitStart = 1;
+ break;
+ case VertexCategory.DownLeft:
+ // 1, 0
+ fromEdge.emitEnd = 1;
+ break;
+ case VertexCategory.DownRight:
+ // 2, 0
+ fromEdge.emitEnd = 2;
+ break;
+ case VertexCategory.LeftUp:
+ // 0, 1
+ toEdge.emitStart = 1;
+ break;
+ case VertexCategory.LeftDown:
+ // 0, 2
+ toEdge.emitStart = 2;
+ break;
+ case VertexCategory.LeftLeft:
+ // 0, 0 - collinear
+ break;
+ case VertexCategory.LeftRight:
+ // 0, 0 - collinear
+ break;
+ case VertexCategory.RightUp:
+ // 0, 2
+ toEdge.emitStart = 2;
+ break;
+ case VertexCategory.RightDown:
+ // 0, 1
+ toEdge.emitStart = 1;
+ break;
+ case VertexCategory.RightLeft:
+ // 0, 0 - collinear
+ break;
+ case VertexCategory.RightRight:
+ // 0, 0 - collinear
+ break;
+ }
+ }
+
+ private ScanEdge ToScanEdge()
+ {
+ int up = this.EdgeCategory == EdgeCategory.Up ? 1 : 0;
+ if (up == 1)
+ {
+ Swap(ref this.start, ref this.end);
+ Swap(ref this.emitStart, ref this.emitEnd);
+ }
+
+ int flags = up | (this.emitStart << 1) | (this.emitEnd << 3);
+ return new ScanEdge(this.start, this.end, flags);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Swap(ref T left, ref T right)
+ {
+ T tmp = left;
+ left = right;
+ right = tmp;
+ }
+ }
+
+ private ref struct RingWalker
+ {
+ private readonly Span output;
+ public int EdgeCounter;
+
+ public EdgeData PreviousEdge;
+ public EdgeData CurrentEdge;
+ public EdgeData NextEdge;
+
+ public RingWalker(Span output)
+ {
+ this.output = output;
+ this.EdgeCounter = 0;
+ this.PreviousEdge = default;
+ this.CurrentEdge = default;
+ this.NextEdge = default;
+ }
+
+ public void Move(bool emitPreviousEdge)
+ {
+ VertexCategory startVertexCategory =
+ CreateVertexCategory(this.PreviousEdge.EdgeCategory, this.CurrentEdge.EdgeCategory);
+ VertexCategory endVertexCategory =
+ CreateVertexCategory(this.CurrentEdge.EdgeCategory, this.NextEdge.EdgeCategory);
+
+ EdgeData.ApplyVertexCategory(startVertexCategory, ref this.PreviousEdge, ref this.CurrentEdge);
+ EdgeData.ApplyVertexCategory(endVertexCategory, ref this.CurrentEdge, ref this.NextEdge);
+
+ if (emitPreviousEdge)
+ {
+ this.PreviousEdge.EmitScanEdge(this.output, ref this.EdgeCounter);
+ }
+
+ this.PreviousEdge = this.CurrentEdge;
+ this.CurrentEdge = this.NextEdge;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs
new file mode 100644
index 00000000..c0053512
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization
+{
+ internal partial class ScanEdgeCollection : IDisposable
+ {
+ private IMemoryOwner buffer;
+
+ private Memory memory;
+
+ private ScanEdgeCollection(IMemoryOwner buffer, int count)
+ {
+ this.buffer = buffer;
+ this.memory = buffer.Memory.Slice(0, count);
+ }
+
+ public Span Edges => this.memory.Span;
+
+ public int Count => this.Edges.Length;
+
+ public void Dispose()
+ {
+ if (this.buffer == null)
+ {
+ return;
+ }
+
+ this.buffer.Dispose();
+ this.buffer = null;
+ this.memory = default;
+ }
+
+ public static ScanEdgeCollection Create(
+ IPath polygon,
+ MemoryAllocator allocator,
+ int subsampling)
+ {
+ TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, allocator);
+ return Create(multipolygon, allocator, subsampling);
+ }
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs b/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs
index 5037a865..d5834a7e 100644
--- a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs
+++ b/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs
@@ -159,7 +159,7 @@ public RectangularPolygon(RectangleF rectangle)
///
/// Gets the points that make this up as a simple linear path.
///
- IReadOnlyList ISimplePath.Points => this.points;
+ ReadOnlyMemory ISimplePath.Points => this.points;
///
/// Gets the size.
diff --git a/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs b/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs
new file mode 100644
index 00000000..a0057723
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs
@@ -0,0 +1,144 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using SixLabors.ImageSharp.Drawing.Shapes.Helpers;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes
+{
+ ///
+ /// Compact representation of a multipolygon.
+ /// Applies some rules which are optimal to implement geometric algorithms:
+ /// - Outer contour is oriented "Positive" (CCW in world coords, CW on screen)
+ /// - Holes are oriented "Negative" (CW in world, CCW on screen)
+ /// - First vertex is always repeated at the end of the span in each ring
+ ///
+ internal class TessellatedMultipolygon : IDisposable, IReadOnlyList
+ {
+ private Ring[] rings;
+
+ private TessellatedMultipolygon(Ring[] rings)
+ {
+ this.rings = rings;
+ this.TotalVertexCount = rings.Sum(r => r.VertexCount);
+ }
+
+ public int TotalVertexCount { get; }
+
+ public int Count => this.rings.Length;
+
+ public Ring this[int index] => this.rings[index];
+
+ public static TessellatedMultipolygon Create(IPath path, MemoryAllocator memoryAllocator)
+ {
+ if (path is IInternalPathOwner ipo)
+ {
+ IReadOnlyList internalPaths = ipo.GetRingsAsInternalPath();
+
+ // If we have only one ring, we can change it's orientation without negative side-effects.
+ // Since the algorithm works best with positively-oriented polygons,
+ // we enforce the orientation for best output quality.
+ bool enforcePositiveOrientationOnFirstRing = internalPaths.Count == 1;
+
+ var rings = new Ring[internalPaths.Count];
+ IMemoryOwner pointBuffer = internalPaths[0].ExtractVertices(memoryAllocator);
+ RepeateFirstVertexAndEnsureOrientation(pointBuffer.Memory.Span, enforcePositiveOrientationOnFirstRing);
+ rings[0] = new Ring(pointBuffer);
+
+ for (int i = 1; i < internalPaths.Count; i++)
+ {
+ pointBuffer = internalPaths[i].ExtractVertices(memoryAllocator);
+ RepeateFirstVertexAndEnsureOrientation(pointBuffer.Memory.Span, false);
+ rings[i] = new Ring(pointBuffer);
+ }
+
+ return new TessellatedMultipolygon(rings);
+ }
+ else
+ {
+ ReadOnlyMemory[] points = path.Flatten().Select(sp => sp.Points).ToArray();
+
+ // If we have only one ring, we can change it's orientation without negative side-effects.
+ // Since the algorithm works best with positively-oriented polygons,
+ // we enforce the orientation for best output quality.
+ bool enforcePositiveOrientationOnFirstRing = points.Length == 1;
+
+ var rings = new Ring[points.Length];
+ rings[0] = MakeRing(points[0], enforcePositiveOrientationOnFirstRing, memoryAllocator);
+ for (int i = 1; i < points.Length; i++)
+ {
+ rings[i] = MakeRing(points[i], false, memoryAllocator);
+ }
+
+ return new TessellatedMultipolygon(rings);
+ }
+
+ static Ring MakeRing(ReadOnlyMemory points, bool enforcePositiveOrientation, MemoryAllocator allocator)
+ {
+ IMemoryOwner buffer = allocator.Allocate(points.Length + 1);
+ Span span = buffer.Memory.Span;
+ points.Span.CopyTo(span);
+ RepeateFirstVertexAndEnsureOrientation(span, enforcePositiveOrientation);
+ return new Ring(buffer);
+ }
+
+ static void RepeateFirstVertexAndEnsureOrientation(Span span, bool enforcePositiveOrientation)
+ {
+ // Repeat first vertex for perf:
+ span[span.Length - 1] = span[0];
+
+ if (enforcePositiveOrientation)
+ {
+ TopologyUtilities.EnsureOrientation(span, 1);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (this.rings == null)
+ {
+ return;
+ }
+
+ foreach (Ring ring in this.rings)
+ {
+ ring.Dispose();
+ }
+
+ this.rings = null;
+ }
+
+ public IEnumerator GetEnumerator() => this.rings.AsEnumerable().GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ internal class Ring : IDisposable
+ {
+ private IMemoryOwner buffer;
+ private Memory memory;
+
+ internal Ring(IMemoryOwner buffer)
+ {
+ this.buffer = buffer;
+ this.memory = buffer.Memory;
+ }
+
+ public ReadOnlySpan Vertices => this.memory.Span;
+
+ public int VertexCount => this.memory.Length - 1; // Last vertex is repeated
+
+ public void Dispose()
+ {
+ this.buffer?.Dispose();
+ this.buffer = null;
+ this.memory = default;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp.Drawing/Utilities/NumberUtilities.cs b/src/ImageSharp.Drawing/Utilities/NumberUtilities.cs
deleted file mode 100644
index c6f47efd..00000000
--- a/src/ImageSharp.Drawing/Utilities/NumberUtilities.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using System.Runtime.CompilerServices;
-
-namespace SixLabors.ImageSharp.Drawing
-{
- ///
- /// Utility methods for numeric primitives.
- ///
- internal static class NumberUtilities
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float ClampFloat(float value, float min, float max)
- {
- if (value >= max)
- {
- return max;
- }
-
- if (value <= min)
- {
- return min;
- }
-
- return value;
- }
- }
-}
diff --git a/src/ImageSharp.Drawing/Utilities/NumericUtilities.cs b/src/ImageSharp.Drawing/Utilities/NumericUtilities.cs
new file mode 100644
index 00000000..84e27593
--- /dev/null
+++ b/src/ImageSharp.Drawing/Utilities/NumericUtilities.cs
@@ -0,0 +1,102 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Drawing.Utilities
+{
+ internal static class NumericUtilities
+ {
+ public static void AddToAllElements(this Span span, float value)
+ {
+ ref float current = ref MemoryMarshal.GetReference(span);
+ ref float max = ref Unsafe.Add(ref current, span.Length);
+
+ if (Vector.IsHardwareAccelerated)
+ {
+ int n = span.Length / Vector.Count;
+ ref Vector currentVec = ref Unsafe.As>(ref current);
+ ref Vector maxVec = ref Unsafe.Add(ref currentVec, n);
+
+ Vector vecVal = new Vector(value);
+ while (Unsafe.IsAddressLessThan(ref currentVec, ref maxVec))
+ {
+ currentVec += vecVal;
+ currentVec = ref Unsafe.Add(ref currentVec, 1);
+ }
+
+ // current = ref Unsafe.Add(ref current, n * Vector.Count);
+ current = ref Unsafe.As, float>(ref currentVec);
+ }
+
+ while (Unsafe.IsAddressLessThan(ref current, ref max))
+ {
+ current += value;
+ current = ref Unsafe.Add(ref current, 1);
+ }
+ }
+
+ // https://apisof.net/catalog/System.Numerics.BitOperations.Log2(UInt32)
+ // BitOperations.Log2() has been introduced in .NET Core 3.0,
+ // since we do target only 3.1+, we can detect it's presence by using SUPPORTS_RUNTIME_INTRINSICS
+ // TODO: Ideally this should have a separate definition in Build.props, but that adaption should be done cross-repo. Using a workaround until then.
+#if SUPPORTS_RUNTIME_INTRINSICS
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int Log2(uint value) => System.Numerics.BitOperations.Log2(value);
+
+#else
+
+#pragma warning disable SA1515, SA1414, SA1114, SA1201
+ private static ReadOnlySpan Log2DeBruijn => new byte[32]
+ {
+ 00, 09, 01, 10, 13, 21, 02, 29,
+ 11, 14, 16, 18, 22, 25, 03, 30,
+ 08, 12, 20, 28, 15, 17, 24, 07,
+ 19, 27, 23, 06, 26, 05, 04, 31
+ };
+
+ // Adapted from:
+ // https://github.com/dotnet/runtime/blob/5c65d891f203618245184fa54397ced0a8ca806c/src/libraries/System.Private.CoreLib/src/System/Numerics/BitOperations.cs#L205-L223
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int Log2(uint value)
+ {
+ // No AggressiveInlining due to large method size
+ // Has conventional contract 0->0 (Log(0) is undefined)
+
+ // Fill trailing zeros with ones, eg 00010010 becomes 00011111
+ value |= value >> 01;
+ value |= value >> 02;
+ value |= value >> 04;
+ value |= value >> 08;
+ value |= value >> 16;
+
+ // uint.MaxValue >> 27 is always in range [0 - 31] so we use Unsafe.AddByteOffset to avoid bounds check
+ return Unsafe.AddByteOffset(
+ // Using deBruijn sequence, k=2, n=5 (2^5=32) : 0b_0000_0111_1100_0100_1010_1100_1101_1101u
+ ref MemoryMarshal.GetReference(Log2DeBruijn),
+ // uint|long -> IntPtr cast on 32-bit platforms does expensive overflow checks not needed here
+ (IntPtr)(int)((value * 0x07C4ACDDu) >> 27));
+ }
+
+#pragma warning restore
+#endif
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ClampFloat(float value, float min, float max)
+ {
+ if (value >= max)
+ {
+ return max;
+ }
+
+ if (value <= min)
+ {
+ return min;
+ }
+
+ return value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Utilities/SortUtility.KeyValueSort.cs b/src/ImageSharp.Drawing/Utilities/SortUtility.KeyValueSort.cs
new file mode 100644
index 00000000..002ad4d7
--- /dev/null
+++ b/src/ImageSharp.Drawing/Utilities/SortUtility.KeyValueSort.cs
@@ -0,0 +1,204 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Drawing.Utilities
+{
+ internal static partial class SortUtility
+ {
+ // Adapted from:
+ // https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ArraySortHelper.cs
+ // If targeting .NET 5, we can call span based sort, but probably not worth it only for that API.
+ private static class KeyValueSort
+ {
+ public static void Sort(Span keys, Span values)
+ {
+ IntrospectiveSort(keys, values);
+ }
+
+ private static void SwapIfGreaterWithValues(Span keys, Span values, int i, int j)
+ {
+ if (keys[i] > keys[j])
+ {
+ float key = keys[i];
+ keys[i] = keys[j];
+ keys[j] = key;
+
+ TValue value = values[i];
+ values[i] = values[j];
+ values[j] = value;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Swap(Span keys, Span values, int i, int j)
+ {
+ float k = keys[i];
+ keys[i] = keys[j];
+ keys[j] = k;
+
+ TValue v = values[i];
+ values[i] = values[j];
+ values[j] = v;
+ }
+
+ private static void IntrospectiveSort(Span keys, Span values)
+ {
+ if (keys.Length > 1)
+ {
+ IntroSort(keys, values, 2 * (NumericUtilities.Log2((uint)keys.Length) + 1));
+ }
+ }
+
+ private static void IntroSort(Span keys, Span values, int depthLimit)
+ {
+ int partitionSize = keys.Length;
+ while (partitionSize > 1)
+ {
+ if (partitionSize <= 16)
+ {
+ if (partitionSize == 2)
+ {
+ SwapIfGreaterWithValues(keys, values, 0, 1);
+ return;
+ }
+
+ if (partitionSize == 3)
+ {
+ SwapIfGreaterWithValues(keys, values, 0, 1);
+ SwapIfGreaterWithValues(keys, values, 0, 2);
+ SwapIfGreaterWithValues(keys, values, 1, 2);
+ return;
+ }
+
+ InsertionSort(keys.Slice(0, partitionSize), values.Slice(0, partitionSize));
+ return;
+ }
+
+ if (depthLimit == 0)
+ {
+ HeapSort(keys.Slice(0, partitionSize), values.Slice(0, partitionSize));
+ return;
+ }
+
+ depthLimit--;
+
+ int p = PickPivotAndPartition(keys.Slice(0, partitionSize), values.Slice(0, partitionSize));
+
+ // Note we've already partitioned around the pivot and do not have to move the pivot again.
+ int s = p + 1;
+ int l = partitionSize - s;
+
+ // IntroSort(keys[(p + 1) .. partitionSize], values[(p + 1) .. partitionSize], depthLimit);
+ IntroSort(keys.Slice(s, l), values.Slice(s, l), depthLimit);
+ partitionSize = p;
+ }
+ }
+
+ private static int PickPivotAndPartition(Span keys, Span values)
+ {
+ int hi = keys.Length - 1;
+
+ // Compute median-of-three. But also partition them, since we've done the comparison.
+ int middle = hi >> 1;
+
+ // Sort lo, mid and hi appropriately, then pick mid as the pivot.
+ SwapIfGreaterWithValues(keys, values, 0, middle); // swap the low with the mid point
+ SwapIfGreaterWithValues(keys, values, 0, hi); // swap the low with the high
+ SwapIfGreaterWithValues(keys, values, middle, hi); // swap the middle with the high
+
+ float pivot = keys[middle];
+ Swap(keys, values, middle, hi - 1);
+ int left = 0, right = hi - 1; // We already partitioned lo and hi and put the pivot in hi - 1. And we pre-increment & decrement below.
+
+ while (left < right)
+ {
+#pragma warning disable SA1503, SA1106
+ while (keys[++left] < pivot);
+ while (pivot < keys[--right]);
+#pragma warning restore SA1503, SA1106
+
+ if (left >= right)
+ {
+ break;
+ }
+
+ Swap(keys, values, left, right);
+ }
+
+ // Put pivot in the right location.
+ if (left != hi - 1)
+ {
+ Swap(keys, values, left, hi - 1);
+ }
+
+ return left;
+ }
+
+ private static void HeapSort(Span keys, Span values)
+ {
+ int n = keys.Length;
+ for (int i = n >> 1; i >= 1; i--)
+ {
+ DownHeap(keys, values, i, n, 0);
+ }
+
+ for (int i = n; i > 1; i--)
+ {
+ Swap(keys, values, 0, i - 1);
+ DownHeap(keys, values, 1, i - 1, 0);
+ }
+ }
+
+ private static void DownHeap(Span keys, Span values, int i, int n, int lo)
+ {
+ float d = keys[lo + i - 1];
+ TValue dValue = values[lo + i - 1];
+
+ while (i <= n >> 1)
+ {
+ int child = 2 * i;
+ if (child < n && keys[lo + child - 1] < keys[lo + child])
+ {
+ child++;
+ }
+
+ if (!(d < keys[lo + child - 1]))
+ {
+ break;
+ }
+
+ keys[lo + i - 1] = keys[lo + child - 1];
+ values[lo + i - 1] = values[lo + child - 1];
+ i = child;
+ }
+
+ keys[lo + i - 1] = d;
+ values[lo + i - 1] = dValue;
+ }
+
+ private static void InsertionSort(Span keys, Span values)
+ {
+ for (int i = 0; i < keys.Length - 1; i++)
+ {
+ float t = keys[i + 1];
+ TValue tValue = values[i + 1];
+
+ int j = i;
+ while (j >= 0 && t < keys[j])
+ {
+ keys[j + 1] = keys[j];
+ values[j + 1] = values[j];
+ j--;
+ }
+
+ keys[j + 1] = t;
+ values[j + 1] = tValue;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Utilities/QuickSort.cs b/src/ImageSharp.Drawing/Utilities/SortUtility.cs
similarity index 61%
rename from src/ImageSharp.Drawing/Utilities/QuickSort.cs
rename to src/ImageSharp.Drawing/Utilities/SortUtility.cs
index 6c7bb3ac..852485f8 100644
--- a/src/ImageSharp.Drawing/Utilities/QuickSort.cs
+++ b/src/ImageSharp.Drawing/Utilities/SortUtility.cs
@@ -4,12 +4,12 @@
using System;
using System.Runtime.CompilerServices;
-namespace SixLabors.ImageSharp.Drawing
+namespace SixLabors.ImageSharp.Drawing.Utilities
{
///
/// Optimized quick sort implementation for Span{float} input
///
- internal static class QuickSort
+ internal static partial class SortUtility
{
///
/// Sorts the elements of in ascending order
@@ -82,34 +82,34 @@ private static int Partition(ref float data0, int lo, int hi)
}
///
- /// Sorts the elements of in ascending order
+ /// Sorts the elements of in ascending order
///
- /// The items to sort on
- /// The items to sort
- public static void Sort(Span sortable, Span data)
+ /// The items to sort on
+ /// The items to sort
+ public static void Sort(Span keys, Span values)
{
- if (sortable.Length != data.Length)
+ if (keys.Length != values.Length)
{
throw new Exception("both spans must be the same length");
}
- if (sortable.Length < 2)
+ if (keys.Length < 2)
{
return;
}
- if (sortable.Length == 2)
+ if (keys.Length == 2)
{
- if (sortable[0] > sortable[1])
+ if (keys[0] > keys[1])
{
- Swap(ref sortable[0], ref sortable[1]);
- Swap(ref data[0], ref data[1]);
+ Swap(ref keys[0], ref keys[1]);
+ Swap(ref values[0], ref values[1]);
}
return;
}
- Sort(ref sortable[0], 0, sortable.Length - 1, ref data[0]);
+ KeyValueSort.Sort(keys, values);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -120,81 +120,42 @@ private static void Swap(ref T left, ref T right)
right = tmp;
}
- private static void Sort(ref float data0, int lo, int hi, ref T dataToSort)
- {
- if (lo < hi)
- {
- int p = Partition(ref data0, lo, hi, ref dataToSort);
- Sort(ref data0, lo, p, ref dataToSort);
- Sort(ref data0, p + 1, hi, ref dataToSort);
- }
- }
-
- private static int Partition(ref float data0, int lo, int hi, ref T dataToSort)
- {
- float pivot = Unsafe.Add(ref data0, lo);
- int i = lo - 1;
- int j = hi + 1;
- while (true)
- {
- do
- {
- i = i + 1;
- }
- while (Unsafe.Add(ref data0, i) < pivot && i < hi);
-
- do
- {
- j = j - 1;
- }
- while (Unsafe.Add(ref data0, j) > pivot && j > lo);
-
- if (i >= j)
- {
- return j;
- }
-
- Swap(ref Unsafe.Add(ref data0, i), ref Unsafe.Add(ref data0, j));
- Swap(ref Unsafe.Add(ref dataToSort, i), ref Unsafe.Add(ref dataToSort, j));
- }
- }
-
///
- /// Sorts the elements of in ascending order, and swapping items in and in sequance with them.
+ /// Sorts the elements of in ascending order, and swapping items in and in sequance with them.
///
- /// The items to sort on
- /// The set of items to sort
- /// The 2nd set of items to sort
- public static void Sort(Span sortable, Span data1, Span data2)
+ /// The items to sort on
+ /// The set of items to sort
+ /// The 2nd set of items to sort
+ public static void Sort(Span keys, Span values1, Span values2)
{
- if (sortable.Length != data1.Length)
+ if (keys.Length != values1.Length)
{
throw new Exception("both spans must be the same length");
}
- if (sortable.Length != data2.Length)
+ if (keys.Length != values2.Length)
{
throw new Exception("both spans must be the same length");
}
- if (sortable.Length < 2)
+ if (keys.Length < 2)
{
return;
}
- if (sortable.Length == 2)
+ if (keys.Length == 2)
{
- if (sortable[0] > sortable[1])
+ if (keys[0] > keys[1])
{
- Swap(ref sortable[0], ref sortable[1]);
- Swap(ref data1[0], ref data1[1]);
- Swap(ref data2[0], ref data2[1]);
+ Swap(ref keys[0], ref keys[1]);
+ Swap(ref values1[0], ref values1[1]);
+ Swap(ref values2[0], ref values2[1]);
}
return;
}
- Sort(ref sortable[0], 0, sortable.Length - 1, ref data1[0], ref data2[0]);
+ Sort(ref keys[0], 0, keys.Length - 1, ref values1[0], ref values2[0]);
}
private static void Sort(ref float data0, int lo, int hi, ref T1 dataToSort1, ref T2 dataToSort2)
diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets
index 422308d7..63fb05b9 100644
--- a/tests/Directory.Build.targets
+++ b/tests/Directory.Build.targets
@@ -33,6 +33,8 @@
+
+
diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawLines.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawLines.cs
deleted file mode 100644
index 2468155d..00000000
--- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawLines.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-using System.Drawing;
-using System.Drawing.Drawing2D;
-using System.IO;
-using System.Numerics;
-using BenchmarkDotNet.Attributes;
-using SixLabors.ImageSharp.Drawing.Processing;
-using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing;
-using SDPoint = System.Drawing.Point;
-
-namespace SixLabors.ImageSharp.Drawing.Benchmarks
-{
- public class DrawLines
- {
- [Benchmark(Baseline = true, Description = "System.Drawing Draw Lines")]
- public void DrawPathSystemDrawing()
- {
- using (var destination = new Bitmap(800, 800))
- using (var graphics = Graphics.FromImage(destination))
- {
- graphics.InterpolationMode = InterpolationMode.Default;
- graphics.SmoothingMode = SmoothingMode.AntiAlias;
-
- using (var pen = new System.Drawing.Pen(System.Drawing.Color.HotPink, 10))
- {
- graphics.DrawLines(
- pen,
- new[] { new SDPoint(10, 10), new SDPoint(550, 50), new SDPoint(200, 400) });
- }
-
- using (var stream = new MemoryStream())
- {
- destination.Save(stream, System.Drawing.Imaging.ImageFormat.Bmp);
- }
- }
- }
-
- [Benchmark(Description = "ImageSharp Draw Lines")]
- public void DrawLinesCore()
- {
- using (var image = new Image(800, 800))
- {
- image.Mutate(x => x.DrawLines(
- Color.HotPink,
- 10,
- new Vector2(10, 10),
- new Vector2(550, 50),
- new Vector2(200, 400)));
-
- using (var stream = new MemoryStream())
- {
- image.SaveAsBmp(stream);
- }
- }
- }
- }
-}
diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs
index 2b2bd3d6..9f552999 100644
--- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs
+++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs
@@ -4,56 +4,145 @@
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
+using System.Linq;
using System.Numerics;
using BenchmarkDotNet.Attributes;
+using GeoJSON.Net.Feature;
+using Newtonsoft.Json;
using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.Drawing.Tests;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
+using SkiaSharp;
using SDPoint = System.Drawing.Point;
+using SDPointF = System.Drawing.PointF;
namespace SixLabors.ImageSharp.Drawing.Benchmarks
{
- public class DrawPolygon
+ public abstract class DrawPolygon
{
- [Benchmark(Baseline = true, Description = "System.Drawing Draw Polygon")]
- public void DrawPolygonSystemDrawing()
+ private PointF[][] points;
+
+ private Image image;
+
+ private SDPointF[][] sdPoints;
+ private System.Drawing.Bitmap sdBitmap;
+ private Graphics sdGraphics;
+
+ private SKPath skPath;
+ private SKSurface skSurface;
+
+ protected abstract int Width { get; }
+ protected abstract int Height { get; }
+ protected abstract float Thickness { get; }
+
+ protected virtual PointF[][] GetPoints(FeatureCollection features) =>
+ features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60))).ToArray();
+
+ [GlobalSetup]
+ public void Setup()
{
- using (var destination = new Bitmap(800, 800))
- using (var graphics = Graphics.FromImage(destination))
+ string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States));
+
+ FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent);
+
+
+
+ this.points = this.GetPoints(featureCollection);
+ this.sdPoints = this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray()).ToArray();
+
+ this.skPath = new SKPath();
+ foreach (PointF[] ptArr in this.points.Where(pts => pts.Length > 2))
{
- graphics.InterpolationMode = InterpolationMode.Default;
- graphics.SmoothingMode = SmoothingMode.AntiAlias;
- using (var pen = new System.Drawing.Pen(System.Drawing.Color.HotPink, 10))
+ skPath.MoveTo(ptArr[0].X, ptArr[1].Y);
+ for (int i = 1; i < ptArr.Length; i++)
{
- graphics.DrawPolygon(
- pen,
- new[] { new SDPoint(10, 10), new SDPoint(550, 50), new SDPoint(200, 400) });
+ skPath.LineTo(ptArr[i].X, ptArr[i].Y);
}
+ skPath.LineTo(ptArr[0].X, ptArr[1].Y);
+ }
- using (var stream = new MemoryStream())
- {
- destination.Save(stream, System.Drawing.Imaging.ImageFormat.Bmp);
- }
+ this.image = new Image(Width, Height);
+ this.sdBitmap = new Bitmap(Width, Height);
+ this.sdGraphics = Graphics.FromImage(this.sdBitmap);
+ this.sdGraphics.InterpolationMode = InterpolationMode.Default;
+ this.sdGraphics.SmoothingMode = SmoothingMode.AntiAlias;
+ this.skSurface = SKSurface.Create(new SKImageInfo(Width, Height));
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ this.image.Dispose();
+ this.sdGraphics.Dispose();
+ this.sdBitmap.Dispose();
+ this.skSurface.Dispose();
+ this.skPath.Dispose();
+ }
+
+ [Benchmark]
+ public void SystemDrawing()
+ {
+ using var pen = new System.Drawing.Pen(System.Drawing.Color.White, this.Thickness);
+
+ foreach (SDPointF[] loop in this.sdPoints)
+ {
+ this.sdGraphics.DrawPolygon(pen, loop);
}
}
- [Benchmark(Description = "ImageSharp Draw Polygon")]
- public void DrawPolygonCore()
+ [Benchmark]
+ public void ImageSharp()
{
- using (var image = new Image(800, 800))
+ this.image.Mutate(c =>
{
- image.Mutate(x => x.DrawPolygon(
- Color.HotPink,
- 10,
- new Vector2(10, 10),
- new Vector2(550, 50),
- new Vector2(200, 400)));
-
- using (var ms = new MemoryStream())
+ foreach (PointF[] loop in this.points)
{
- image.SaveAsBmp(ms);
+ c.DrawPolygon(Color.White, this.Thickness, loop);
}
- }
+ });
+ }
+
+ [Benchmark(Baseline = true)]
+ public void SkiaSharp()
+ {
+ using SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.White,
+ StrokeWidth = this.Thickness,
+ IsAntialias = true,
+ };
+
+ this.skSurface.Canvas.DrawPath(this.skPath, paint);
+ }
+ }
+
+ public class DrawPolygonAll : DrawPolygon
+ {
+ protected override int Width => 7200;
+ protected override int Height => 4800;
+ protected override float Thickness => 2f;
+ }
+
+ public class DrawPolygonMediumThin : DrawPolygon
+ {
+ protected override int Width => 1000;
+ protected override int Height => 1000;
+ protected override float Thickness => 1f;
+
+ protected override PointF[][] GetPoints(FeatureCollection features)
+ {
+ Feature state = features.Features.Single(f => (string) f.Properties["NAME"] == "Mississippi");
+
+ Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54)
+ * Matrix3x2.CreateScale(60, 60);
+ return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray();
}
}
+
+ public class DrawPolygonMediumThick : DrawPolygonMediumThin
+ {
+ protected override float Thickness => 10f;
+ }
}
diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs
index c39b92c3..2e862529 100644
--- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs
+++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs
@@ -1,115 +1,116 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
+using System.IO;
using System.Linq;
+using System.Numerics;
using BenchmarkDotNet.Attributes;
+using GeoJSON.Net.Feature;
+using Newtonsoft.Json;
using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.Drawing.Tests;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
+using SkiaSharp;
+using SDPoint = System.Drawing.Point;
+using SDPointF = System.Drawing.PointF;
+using SDBitmap = System.Drawing.Bitmap;
using SDRectangleF = System.Drawing.RectangleF;
+using SDFont = System.Drawing.Font;
namespace SixLabors.ImageSharp.Drawing.Benchmarks
{
[MemoryDiagnoser]
public class DrawText
{
- [Params(10, 100)]
+ public const int Width = 800;
+ public const int Height = 800;
+
+ [Params(1, 20)]
public int TextIterations { get; set; }
- public string TextPhrase { get; set; } = "Hello World";
+ protected const string TextPhrase= "asdfghjkl123456789{}[]+$%?";
+
+ public string TextToRender => string.Join(" ", Enumerable.Repeat(TextPhrase, this.TextIterations));
- public string TextToRender => string.Join(" ", Enumerable.Repeat(this.TextPhrase, this.TextIterations));
+ private Image image;
+ private SDBitmap sdBitmap;
+ private Graphics sdGraphics;
+ private SKBitmap skBitmap;
+ private SKCanvas skCanvas;
- [Benchmark(Baseline = true, Description = "System.Drawing Draw Text")]
- public void DrawTextSystemDrawing()
+ private SDFont sdFont;
+ private Fonts.Font font;
+ private SKTypeface skTypeface;
+
+ [GlobalSetup]
+ public void Setup()
{
- using (var destination = new Bitmap(800, 800))
- using (var graphics = Graphics.FromImage(destination))
- {
- graphics.InterpolationMode = InterpolationMode.Default;
- graphics.SmoothingMode = SmoothingMode.AntiAlias;
- using (var font = new Font("Arial", 12, GraphicsUnit.Point))
- {
- graphics.DrawString(
- this.TextToRender,
- font,
- System.Drawing.Brushes.HotPink,
- new SDRectangleF(10, 10, 780, 780));
- }
- }
+ this.image = new Image(Width, Height);
+ this.sdBitmap = new Bitmap(Width, Height);
+ this.sdGraphics = Graphics.FromImage(this.sdBitmap);
+ this.sdGraphics.InterpolationMode = InterpolationMode.Default;
+ this.sdGraphics.SmoothingMode = SmoothingMode.AntiAlias;
+ this.sdGraphics.InterpolationMode = InterpolationMode.Default;
+ this.sdGraphics.SmoothingMode = SmoothingMode.AntiAlias;
+ this.skBitmap = new SKBitmap(Width, Height);
+ this.skCanvas = new SKCanvas(this.skBitmap);
+
+ this.sdFont = new SDFont("Arial", 12, GraphicsUnit.Point);
+ this.font = Fonts.SystemFonts.CreateFont("Arial", 12);
+ this.skTypeface = SKTypeface.FromFamilyName("Arial");
}
- [Benchmark(Description = "ImageSharp Draw Text - Cached Glyphs")]
- public void DrawTextCore()
+ [GlobalCleanup]
+ public void Cleanup()
{
- using (var image = new Image(800, 800))
- {
- Fonts.Font font = Fonts.SystemFonts.CreateFont("Arial", 12);
- image.Mutate(x => x
- .SetGraphicsOptions(o => o.Antialias = true)
- .SetTextOptions(o => o.WrapTextWidth = 780)
- .DrawText(
- this.TextToRender,
- font,
- Processing.Brushes.Solid(Color.HotPink),
- new PointF(10, 10)));
- }
+ this.image.Dispose();
+ this.sdGraphics.Dispose();
+ this.sdBitmap.Dispose();
+ this.skCanvas.Dispose();
+ this.skBitmap.Dispose();
+ this.sdFont.Dispose();
+ this.skTypeface.Dispose();
+ }
+
+ [Benchmark]
+ public void SystemDrawing()
+ {
+ this.sdGraphics.DrawString(
+ this.TextToRender,
+ this.sdFont,
+ System.Drawing.Brushes.HotPink,
+ new SDRectangleF(10, 10, 780, 780));
}
- [Benchmark(Description = "ImageSharp Draw Text - Naive")]
- public void DrawTextCoreOld()
+ [Benchmark]
+ public void ImageSharp()
{
- using (var image = new Image(800, 800))
- {
- Fonts.Font font = Fonts.SystemFonts.CreateFont("Arial", 12);
- image.Mutate(x => DrawTextOldVersion(
- x,
- new TextGraphicsOptions { GraphicsOptions = { Antialias = true }, TextOptions = { WrapTextWidth = 780 } },
+ this.image.Mutate(x => x
+ .SetGraphicsOptions(o => o.Antialias = true)
+ .SetTextOptions(o => o.WrapTextWidth = 780)
+ .DrawText(
this.TextToRender,
font,
Processing.Brushes.Solid(Color.HotPink),
- null,
new PointF(10, 10)));
- }
+ }
- IImageProcessingContext DrawTextOldVersion(
- IImageProcessingContext source,
- TextGraphicsOptions options,
- string text,
- Fonts.Font font,
- IBrush brush,
- IPen pen,
- PointF location)
+ [Benchmark(Baseline = true)]
+ public void SkiaSharp()
+ {
+ using SKPaint paint = new SKPaint
{
- const float dpiX = 72;
- const float dpiY = 72;
-
- var style = new Fonts.RendererOptions(font, dpiX, dpiY, location)
- {
- ApplyKerning = options.TextOptions.ApplyKerning,
- TabWidth = options.TextOptions.TabWidth,
- WrappingWidth = options.TextOptions.WrapTextWidth,
- HorizontalAlignment = options.TextOptions.HorizontalAlignment,
- VerticalAlignment = options.TextOptions.VerticalAlignment
- };
-
- IPathCollection glyphs = TextBuilder.GenerateGlyphs(text, style);
-
- var pathOptions = new ShapeGraphicsOptions() { GraphicsOptions = options.GraphicsOptions };
- if (brush != null)
- {
- source.Fill(pathOptions, brush, glyphs);
- }
-
- if (pen != null)
- {
- source.Draw(pathOptions, pen, glyphs);
- }
-
- return source;
- }
+ Color = SKColors.HotPink,
+ IsAntialias = true,
+ TextSize = 16, // 12*1.3333
+ Typeface = skTypeface
+ };
+
+ this.skCanvas.DrawText(TextToRender, 10, 10, paint);
}
}
}
diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs
index f4c37d73..ee5c1b23 100644
--- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs
+++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs
@@ -115,5 +115,23 @@ IImageProcessingContext DrawTextOldVersion(
return source;
}
}
+
+ // 11/12/2020
+ // BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1198 (1909/November2018Update/19H2)
+ // Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
+ // .NET Core SDK=5.0.100-preview.6.20318.15
+ // [Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
+ // DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
+ //
+ //
+ // | Method | TextIterations | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
+ // |-------------- |--------------- |-------------:|-----------:|-----------:|-------:|--------:|----------:|---------:|------:|----------:|
+ // | SystemDrawing | 1 | 55.03 us | 0.199 us | 0.186 us | 5.43 | 0.03 | - | - | - | 40 B |
+ // | ImageSharp | 1 | 2,161.92 us | 4.203 us | 3.510 us | 213.14 | 0.52 | 253.9063 | - | - | 804452 B |
+ // | SkiaSharp | 1 | 10.14 us | 0.040 us | 0.031 us | 1.00 | 0.00 | 0.5341 | - | - | 1680 B |
+ // | | | | | | | | | | | |
+ // | SystemDrawing | 20 | 1,450.12 us | 3.583 us | 3.176 us | 27.36 | 0.11 | - | - | - | 3696 B |
+ // | ImageSharp | 20 | 28,559.17 us | 244.615 us | 216.844 us | 538.85 | 3.98 | 2312.5000 | 781.2500 | - | 9509056 B |
+ // | SkiaSharp | 20 | 53.00 us | 0.166 us | 0.147 us | 1.00 | 0.00 | 1.6479 | - | - | 5336 B |
}
}
diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs
index b9272b35..b42e6ac1 100644
--- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs
+++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs
@@ -1,81 +1,180 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
+using System.Linq;
using System.Numerics;
using BenchmarkDotNet.Attributes;
+using GeoJSON.Net.Feature;
+using Newtonsoft.Json;
using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.Drawing.Tests;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
+using SkiaSharp;
using SDPoint = System.Drawing.Point;
+using SDPointF = System.Drawing.PointF;
+using SDBitmap = System.Drawing.Bitmap;
namespace SixLabors.ImageSharp.Drawing.Benchmarks
{
- public class FillPolygon
+ public abstract class FillPolygon
{
- private readonly Polygon shape;
+ private PointF[][] points;
+ private Polygon[] polygons;
+ private SDPointF[][] sdPoints;
+ private List skPaths;
- public FillPolygon()
- {
- this.shape = new Polygon(new LinearLineSegment(
- new Vector2(10, 10),
- new Vector2(550, 50),
- new Vector2(200, 400)));
- }
+ private Image image;
+ private SDBitmap sdBitmap;
+ private Graphics sdGraphics;
+ private SKBitmap skBitmap;
+ private SKCanvas skCanvas;
+
+ protected abstract int Width { get; }
+ protected abstract int Height { get; }
- [Benchmark(Baseline = true, Description = "System.Drawing Fill Polygon")]
- public void DrawSolidPolygonSystemDrawing()
+ protected virtual PointF[][] GetPoints(FeatureCollection features) =>
+ features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60))).ToArray();
+
+ [GlobalSetup]
+ public void Setup()
{
- using (var destination = new Bitmap(800, 800))
+ string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States));
- using (var graphics = Graphics.FromImage(destination))
- {
- graphics.SmoothingMode = SmoothingMode.AntiAlias;
- graphics.FillPolygon(
- System.Drawing.Brushes.HotPink,
- new[] { new SDPoint(10, 10), new SDPoint(550, 50), new SDPoint(200, 400) });
+ FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent);
- using (var stream = new MemoryStream())
+ this.points = this.GetPoints(featureCollection);
+ this.polygons = this.points.Select(pts => new Polygon(new LinearLineSegment(pts))).ToArray();
+
+ this.sdPoints = this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray()).ToArray();
+
+ this.skPaths = new List();
+ foreach (PointF[] ptArr in this.points.Where(pts => pts.Length > 2))
+ {
+ SKPath skPath = new SKPath();
+ skPath.MoveTo(ptArr[0].X, ptArr[1].Y);
+ for (int i = 1; i < ptArr.Length; i++)
{
- destination.Save(stream, System.Drawing.Imaging.ImageFormat.Bmp);
+ skPath.LineTo(ptArr[i].X, ptArr[i].Y);
}
+ skPath.LineTo(ptArr[0].X, ptArr[1].Y);
+ this.skPaths.Add(skPath);
}
+
+ this.image = new Image(Width, Height);
+ this.sdBitmap = new Bitmap(Width, Height);
+ this.sdGraphics = Graphics.FromImage(this.sdBitmap);
+ this.sdGraphics.InterpolationMode = InterpolationMode.Default;
+ this.sdGraphics.SmoothingMode = SmoothingMode.AntiAlias;
+ this.skBitmap = new SKBitmap(Width, Height);
+ this.skCanvas = new SKCanvas(skBitmap);
}
- [Benchmark(Description = "ImageSharp Fill Polygon")]
- public void DrawSolidPolygonCore()
+ [GlobalCleanup]
+ public void Cleanup()
{
- using (var image = new Image(800, 800))
+ this.image.Dispose();
+ this.sdGraphics.Dispose();
+ this.sdBitmap.Dispose();
+ this.skCanvas.Dispose();
+ this.skBitmap.Dispose();
+ foreach (SKPath skPath in this.skPaths)
{
- image.Mutate(x => x.FillPolygon(
- Color.HotPink,
- new Vector2(10, 10),
- new Vector2(550, 50),
- new Vector2(200, 400)));
-
- using (var stream = new MemoryStream())
- {
- image.SaveAsBmp(stream);
- }
+ skPath.Dispose();
}
}
- [Benchmark(Description = "ImageSharp Fill Polygon - Cached shape")]
- public void DrawSolidPolygonCoreCached()
+ [Benchmark]
+ public void SystemDrawing()
{
- using (var image = new Image(800, 800))
+ using var brush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
+
+ foreach (SDPointF[] loop in this.sdPoints)
{
- image.Mutate(x => x.Fill(
- Color.HotPink,
- this.shape));
+ this.sdGraphics.FillPolygon(brush, loop);
+ }
+ }
- using (var stream = new MemoryStream())
+ [Benchmark]
+ public void ImageSharp()
+ {
+ this.image.Mutate(c =>
+ {
+ foreach (Polygon polygon in this.polygons)
{
- image.SaveAsBmp(stream);
+ c.Fill(Color.White, polygon);
}
+ });
+ }
+
+ [Benchmark(Baseline = true)]
+ public void SkiaSharp()
+ {
+ foreach (SKPath path in this.skPaths)
+ {
+ // Emulate using different color for each polygon:
+ using SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.White,
+ IsAntialias = true,
+ };
+ this.skCanvas.DrawPath(path, paint);
}
}
}
+
+ public class FillPolygonAll : FillPolygon
+ {
+ protected override int Width => 7200;
+ protected override int Height => 4800;
+ }
+
+ public class FillPolygonMedium : FillPolygon
+ {
+ protected override int Width => 1000;
+ protected override int Height => 1000;
+
+ protected override PointF[][] GetPoints(FeatureCollection features)
+ {
+ Feature state = features.Features.Single(f => (string) f.Properties["NAME"] == "Mississippi");
+
+ Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54)
+ * Matrix3x2.CreateScale(60, 60);
+ return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray();
+ }
+
+ // ** 11/13/2020 @ Anton's PC ***
+ // BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1198 (1909/November2018Update/19H2)
+ // Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
+ // .NET Core SDK=5.0.100-preview.6.20318.15
+ // [Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
+ // DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
+ //
+ //
+ // | Method | Mean | Error | StdDev | Ratio | RatioSD |
+ // |-------------- |-----------:|---------:|----------:|------:|--------:|
+ // | SystemDrawing | 457.4 us | 9.07 us | 23.40 us | 2.15 | 0.10 |
+ // | ImageSharp | 3,079.5 us | 61.45 us | 138.71 us | 14.30 | 0.89 |
+ // | SkiaSharp | 217.7 us | 4.29 us | 6.55 us | 1.00 | 0.00 |
+ }
+
+ public class FillPolygonSmall : FillPolygon
+ {
+ protected override int Width => 1000;
+ protected override int Height => 1000;
+
+ protected override PointF[][] GetPoints(FeatureCollection features)
+ {
+ Feature state = features.Features.Single(f => (string) f.Properties["NAME"] == "Utah");
+
+ Matrix3x2 transform = Matrix3x2.CreateTranslation(-60, -40)
+ * Matrix3x2.CreateScale(60, 60);
+ return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray();
+ }
+ }
}
diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj
index fe40cdbf..6c2317de 100644
--- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj
+++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj
@@ -13,10 +13,27 @@
+
+
+
+
+ TestFile.cs
+
+
+ TestImages.cs
+
+
+ PolygonFactory.cs
+
+
+ TestEnvironment.cs
+
+
+
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs
index dc45eb4c..6e4c162d 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs
@@ -5,7 +5,6 @@
using System.Numerics;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
-
using Xunit;
namespace SixLabors.ImageSharp.Drawing.Tests.Drawing
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs
new file mode 100644
index 00000000..1b2ffb59
--- /dev/null
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs
@@ -0,0 +1,96 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using GeoJSON.Net.Feature;
+using Newtonsoft.Json;
+using SixLabors.Fonts;
+using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Drawing.Tests.Drawing
+{
+ public class DrawingProfilingBenchmarks : IDisposable
+ {
+ private Image image;
+ private Polygon[] polygons;
+
+ public DrawingProfilingBenchmarks()
+ {
+ string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States));
+
+ FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent);
+
+ PointF[][] points = GetPoints(featureCollection);
+ this.polygons = points.Select(pts => new Polygon(new LinearLineSegment(pts))).ToArray();
+
+ this.image = new Image(1000, 1000);
+
+ static PointF[][] GetPoints(FeatureCollection features)
+ {
+ Feature state = features.Features.Single(f => (string) f.Properties["NAME"] == "Mississippi");
+
+ Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54)
+ * Matrix3x2.CreateScale(60, 60);
+ return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray();
+ }
+ }
+
+ [Theory(Skip = "For local profiling only")]
+ [InlineData(IntersectionRule.OddEven)]
+ [InlineData(IntersectionRule.Nonzero)]
+ public void FillPolygon(IntersectionRule intersectionRule)
+ {
+ const int Times = 100;
+
+ for (int i = 0; i < Times; i++)
+ {
+ this.image.Mutate(c =>
+ {
+ c.SetShapeOptions(new ShapeOptions()
+ {
+ IntersectionRule = intersectionRule
+ });
+ foreach (Polygon polygon in this.polygons)
+ {
+ c.Fill(Color.White, polygon);
+ }
+ });
+ }
+ }
+
+ [Theory(Skip = "For local profiling only")]
+ [InlineData(1)]
+ [InlineData(10)]
+ public void DrawText(int textIterations)
+ {
+ const int Times = 20;
+ const string TextPhrase= "asdfghjkl123456789{}[]+$%?";
+ string textToRender = string.Join("/n", Enumerable.Repeat(TextPhrase, textIterations));
+
+ Font font = SystemFonts.CreateFont("Arial", 12);
+
+ for (int i = 0; i < Times; i++)
+ {
+ this.image.Mutate(x => x
+ .SetGraphicsOptions(o => o.Antialias = true)
+ .SetTextOptions(o => o.WrapTextWidth = 780)
+ .DrawText(
+ textToRender,
+ font,
+ Brushes.Solid(Color.HotPink),
+ new PointF(10, 10)));
+ }
+ }
+
+ public void Dispose()
+ {
+ this.image.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs
new file mode 100644
index 00000000..56b8406f
--- /dev/null
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs
@@ -0,0 +1,232 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using GeoJSON.Net.Feature;
+using Newtonsoft.Json;
+using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using SkiaSharp;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Drawing.Tests.Drawing
+{
+ [GroupOutput("Drawing")]
+ public class DrawingRobustnessTests
+ {
+ [Theory(Skip = "For local testing")]
+ [WithSolidFilledImages(32, 32, "Black", PixelTypes.Rgba32)]
+ public void CompareToSkiaResults_SmallCircle(TestImageProvider provider)
+ {
+ EllipsePolygon circle = new EllipsePolygon(16, 16, 10);
+
+ CompareToSkiaResultsImpl(provider, circle);
+ }
+
+ [Theory(Skip = "For local testing")]
+ [WithSolidFilledImages(64, 64, "Black", PixelTypes.Rgba32)]
+ public void CompareToSkiaResults_StarCircle(TestImageProvider provider)
+ {
+ EllipsePolygon circle = new EllipsePolygon(32, 32, 30);
+ Star star = new Star(32, 32, 7, 10, 27);
+ IPath shape = circle.Clip(star);
+
+ CompareToSkiaResultsImpl(provider, shape);
+ }
+
+ private static void CompareToSkiaResultsImpl(TestImageProvider provider, IPath shape)
+ {
+ using Image image = provider.GetImage();
+ image.Mutate(c => c.Fill(Color.White, shape));
+ image.DebugSave(provider, "ImageSharp", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
+
+ using SKBitmap bitmap = new SKBitmap(new SKImageInfo(image.Width, image.Height));
+
+ using SKPath skPath = new SKPath();
+
+ foreach (var loop in shape.Flatten())
+ {
+ ReadOnlySpan points = MemoryMarshal.Cast(loop.Points.Span);
+ skPath.AddPoly(points.ToArray());
+ }
+
+ using SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.White,
+ IsAntialias = true,
+ };
+
+ using SKCanvas canvas = new SKCanvas(bitmap);
+ canvas.Clear(new SKColor(0, 0, 0));
+ canvas.DrawPath(skPath, paint);
+
+ using Image skResultImage =
+ Image.LoadPixelData(bitmap.GetPixelSpan(), image.Width, image.Height);
+ skResultImage.DebugSave(provider, "SkiaSharp", appendPixelTypeToFileName: false,
+ appendSourceFileOrDescription: false);
+
+ var result = ImageComparer.Exact.CompareImagesOrFrames(image, skResultImage);
+ throw new Exception(result.DifferencePercentageString);
+ }
+
+ [Theory(Skip = "For local testing")]
+ [WithSolidFilledImages(3600, 2400, "Black", PixelTypes.Rgba32, TestImages.GeoJson.States, 16, 30, 30)]
+ public void LargeGeoJson_Lines(TestImageProvider provider, string geoJsonFile, int aa, float sx, float sy)
+ {
+ string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(geoJsonFile));
+
+ PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix3x2.CreateScale(sx, sy));
+
+ using Image image = provider.GetImage();
+ var options = new ShapeGraphicsOptions()
+ {
+ GraphicsOptions = new GraphicsOptions() {Antialias = aa > 0, AntialiasSubpixelDepth = aa},
+ };
+ foreach (PointF[] loop in points)
+ {
+ image.Mutate(c => c.DrawLines(options, Color.White, 1.0f, loop));
+ }
+
+ string details = $"_{System.IO.Path.GetFileName(geoJsonFile)}_{sx}x{sy}_aa{aa}";
+
+ image.DebugSave(provider,
+ details,
+ appendPixelTypeToFileName: false,
+ appendSourceFileOrDescription: false);
+ }
+
+ [Theory]
+ [WithSolidFilledImages(7200, 3300, "Black", PixelTypes.Rgba32)]
+ public void LargeGeoJson_States_Fill(TestImageProvider provider)
+ {
+ using Image image = FillGeoJsonPolygons(provider, TestImages.GeoJson.States, 16, new Vector2(60), new Vector2(0, -1000));
+ ImageComparer comparer = ImageComparer.TolerantPercentage(0.001f);
+
+ image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
+ image.CompareToReferenceOutput(comparer, provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
+ }
+
+ private Image FillGeoJsonPolygons(TestImageProvider provider, string geoJsonFile, int aa, Vector2 scale, Vector2 pixelOffset)
+ {
+ string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(geoJsonFile));
+
+ PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix3x2.CreateScale(scale) * Matrix3x2.CreateTranslation(pixelOffset));
+
+ Image image = provider.GetImage();
+ var options = new ShapeGraphicsOptions()
+ {
+ GraphicsOptions = new GraphicsOptions() {Antialias = aa > 0, AntialiasSubpixelDepth = aa},
+ };
+ var rnd = new Random(42);
+ byte[] rgb = new byte[3];
+ foreach (PointF[] loop in points)
+ {
+ rnd.NextBytes(rgb);
+
+ Color color = Color.FromRgb(rgb[0], rgb[1], rgb[2]);
+ image.Mutate(c => c.FillPolygon(options, color, loop));
+ }
+
+ return image;
+ }
+
+ [Theory]
+ [WithSolidFilledImages(400, 400, "Black", PixelTypes.Rgba32, 0)]
+ [WithSolidFilledImages(6000, 6000, "Black", PixelTypes.Rgba32, 5500)]
+ public void LargeGeoJson_Mississippi_Lines(TestImageProvider provider, int pixelOffset)
+ {
+ string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States));
+
+ FeatureCollection features = JsonConvert.DeserializeObject(jsonContent);
+
+ var missisipiGeom = features.Features.Single(f => (string) f.Properties["NAME"] == "Mississippi");
+
+ var transform = Matrix3x2.CreateTranslation(-87, -54)
+ * Matrix3x2.CreateScale(60, 60)
+ * Matrix3x2.CreateTranslation(pixelOffset, pixelOffset);
+ var points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform);
+
+ using Image image = provider.GetImage();
+
+ foreach (PointF[] loop in points)
+ {
+ image.Mutate(c => c.DrawLines(Color.White, 1.0f, loop));
+ }
+
+ // Very strict tolerance, since the image is sparse (relaxed on .NET Framework)
+ ImageComparer comparer = TestEnvironment.IsFramework
+ ? ImageComparer.TolerantPercentage(1e-3f)
+ : ImageComparer.TolerantPercentage(1e-7f);
+
+ string details = $"PixelOffset({pixelOffset})";
+ image.DebugSave(provider, details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
+ image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
+ }
+
+
+
+ [Theory(Skip = "For local experiments only")]
+ [InlineData(0)]
+ [InlineData(5000)]
+ [InlineData(9000)]
+ public void Missisippi_Skia(int offset)
+ {
+ string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States));
+
+ FeatureCollection features = JsonConvert.DeserializeObject(jsonContent);
+
+ var missisipiGeom = features.Features.Single(f => (string) f.Properties["NAME"] == "Mississippi");
+
+ var transform = Matrix3x2.CreateTranslation(-87, -54)
+ * Matrix3x2.CreateScale(60, 60)
+ * Matrix3x2.CreateTranslation(offset, offset);
+ IReadOnlyList points =PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform);
+
+
+ SKPath path = new SKPath();
+
+ foreach (PointF[] pts in points.Where(p => p.Length > 2))
+ {
+ path.MoveTo(pts[0].X, pts[0].Y);
+
+ for (int i = 0; i < pts.Length; i++)
+ {
+ path.LineTo(pts[i].X, pts[i].Y);
+ }
+ path.LineTo(pts[0].X, pts[0].Y);
+ }
+
+ SKImageInfo imageInfo = new SKImageInfo(10000, 10000);
+
+ using SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.White,
+ StrokeWidth = 1f,
+ IsAntialias = true,
+ };
+
+ using SKSurface surface = SKSurface.Create(imageInfo);
+ SKCanvas canvas = surface.Canvas;
+ canvas.Clear(new SKColor(0,0, 0));
+ canvas.DrawPath(path, paint);
+
+ string outDir = TestEnvironment.CreateOutputDirectory("Skia");
+ string fn = System.IO.Path.Combine(outDir, $"Missisippi_Skia_{offset}.png");
+
+ using SKImage image = surface.Snapshot();
+ using SKData data = image.Encode(SKEncodedImageFormat.Png, 100);
+
+ using FileStream fs = File.Create(fn);
+ data.SaveTo(fs);
+ }
+ }
+}
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs
new file mode 100644
index 00000000..d50a014e
--- /dev/null
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs
@@ -0,0 +1,55 @@
+using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Drawing.Tests.Drawing
+{
+ [GroupOutput("Drawing")]
+ public class FillOutsideBoundsTests
+ {
+ [Theory]
+ [InlineData(-100)] //Crash
+ [InlineData(-99)] //Fine
+ [InlineData(99)] //Fine
+ [InlineData(100)] //Crash
+ public void DrawRectactangleOutsideBoundsDrawingArea(int xpos)
+ {
+ int width = 100;
+ int height = 100;
+
+ using (var image = new Image(width, height, Color.Red))
+ {
+ var rectangle = new Rectangle(xpos, 0, width, height);
+
+ image.Mutate(x => x.Fill(Color.Black, rectangle));
+ }
+ }
+
+ public static TheoryData CircleCoordinates = new TheoryData()
+ {
+ {-110, -60}, { 0, -60 }, {110, -60},
+ {-110, -50}, { 0, -50 }, {110, -50},
+ {-110, -49}, { 0, -49 }, {110, -49},
+ {-110, -20}, { 0, -20 }, {110, -20},
+ {-110, -50}, { 0, -60 }, {110, -60},
+ {-110, 0}, { -99, 0}, { 0, 0 }, {99, 0}, { 110, 0},
+ };
+
+ [Theory]
+ [WithSolidFilledImages(nameof(CircleCoordinates), 100, 100, nameof(Color.Red), PixelTypes.Rgba32)]
+ public void DrawCircleOutsideBoundsDrawingArea(TestImageProvider provider, int xpos, int ypos)
+ {
+ int width = 100;
+ int height = 100;
+
+ using var image = provider.GetImage();
+ var circle = new EllipsePolygon(xpos, ypos, width, height);
+
+ provider.RunValidatingProcessorTest(x => x.Fill(Color.Black, circle),
+ $"({xpos}_{ypos})",
+ appendPixelTypeToFileName: false,
+ appendSourceFileOrDescription: false);
+ }
+ }
+}
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs
index c6420dfa..6cc20303 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs
@@ -2,8 +2,16 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.IO;
+using System.Linq;
using System.Numerics;
+using GeoJSON.Net.Converters;
+using GeoJSON.Net.Feature;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.Drawing.Shapes;
+using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
@@ -14,6 +22,26 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Drawing
[GroupOutput("Drawing")]
public class FillPolygonTests
{
+ [Theory]
+ [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 0)]
+ [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 8)]
+ [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 16)]
+ public void FillPolygon_Solid_Basic(TestImageProvider provider, int antialias)
+ where TPixel : unmanaged, IPixel
+ {
+ PointF[] polygon1 = PolygonFactory.CreatePointArray((2, 2), (6, 2), (6, 4), (2, 4));
+ PointF[] polygon2 = PolygonFactory.CreatePointArray((2, 8), (4, 6), (6, 8), (4, 10));
+
+ var options = new GraphicsOptions { Antialias = antialias > 0, AntialiasSubpixelDepth = antialias };
+ provider.RunValidatingProcessorTest(
+ c => c.SetGraphicsOptions(options)
+ .FillPolygon(Color.White, polygon1)
+ .FillPolygon(Color.White, polygon2),
+ appendPixelTypeToFileName: false,
+ appendSourceFileOrDescription: false,
+ testOutputDetails: $"aa{antialias}");
+ }
+
[Theory]
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, true)]
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, true)]
@@ -39,9 +67,52 @@ public void FillPolygon_Solid(TestImageProvider provider, string
appendSourceFileOrDescription: false);
}
+ public static TheoryData FillPolygon_Complex_Data =
+ new TheoryData()
+ {
+ {false, IntersectionRule.OddEven},
+ {false, IntersectionRule.Nonzero},
+ {true, IntersectionRule.OddEven},
+ {true, IntersectionRule.Nonzero},
+ };
+
[Theory]
- [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32)]
- public void FillPolygon_Concave(TestImageProvider provider)
+ [WithBasicTestPatternImages(nameof(FillPolygon_Complex_Data), 100, 100, PixelTypes.Rgba32)]
+ public void FillPolygon_Complex(TestImageProvider provider, bool reverse, IntersectionRule intersectionRule)
+ where TPixel : unmanaged, IPixel
+ {
+ PointF[] contour = PolygonFactory.CreatePointArray((20, 20), (80, 20), (80, 80), (20, 80));
+ PointF[] hole = PolygonFactory.CreatePointArray((40, 40), (40, 60), (60, 60), (60, 40));
+
+ if (reverse)
+ {
+ Array.Reverse(contour);
+ Array.Reverse(hole);
+ }
+
+ ComplexPolygon polygon = new ComplexPolygon(
+ new Path(new LinearLineSegment(contour)),
+ new Path(new LinearLineSegment(hole)));
+
+ provider.RunValidatingProcessorTest(
+ c =>
+ {
+ c.SetShapeOptions(new ShapeOptions()
+ {
+ IntersectionRule = intersectionRule
+ });
+ c.Fill(Color.White, polygon);
+ },
+ testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})",
+ comparer: ImageComparer.TolerantPercentage(0.01f),
+ appendSourceFileOrDescription: false,
+ appendPixelTypeToFileName: false);
+ }
+
+ [Theory]
+ [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, false)]
+ [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, true)]
+ public void FillPolygon_Concave(TestImageProvider