Skip to content

Commit

Permalink
Merge pull request #96 from SixLabors/af/polygon-lab
Browse files Browse the repository at this point in the history
Fast polygon scanning with active edge list
  • Loading branch information
tocsoft authored Nov 20, 2020
2 parents acf438b + 722ea9f commit 3d737b5
Show file tree
Hide file tree
Showing 198 changed files with 4,305 additions and 1,252 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

<!--Src Dependencies-->
<PackageReference Update="SixLabors.Fonts" Version="1.0.0-beta0013" />
<PackageReference Update="SixLabors.ImageSharp" Version="1.0.0-rc0003" />
<PackageReference Update="SixLabors.ImageSharp" Version="1.0.2" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="SixLabors.Fonts" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="Microsoft.SourceLink.GitHub"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" />
<PackageReference Include="MinVer" PrivateAssets="All" />
</ItemGroup>

Expand Down
19 changes: 2 additions & 17 deletions src/ImageSharp.Drawing/Primitives/Region.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,12 @@ namespace SixLabors.ImageSharp.Drawing
/// </summary>
public abstract class Region
{
/// <summary>
/// Gets the maximum number of intersections to could be returned.
/// </summary>
public abstract int MaxIntersections { get; }

/// <summary>
/// Gets the bounding box that entirely surrounds this region.
/// </summary>
/// <remarks>
/// This should always contains all possible points returned from <see cref="Scan"/>.
/// </remarks>
public abstract Rectangle Bounds { get; }

/// <summary>
/// Scans the X axis for intersections at the Y axis position.
/// </summary>
/// <param name="y">The position along the y axis to find intersections.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="configuration">A <see cref="Configuration"/> instance in the context of the caller.</param>
/// <param name="intersectionRule">How intersections are handled.</param>
/// <returns>The number of intersections found.</returns>
public abstract int Scan(float y, Span<float> buffer, Configuration configuration, IntersectionRule intersectionRule);
// We should consider removing Region, so keeping this internal for now.
internal abstract IPath Shape { get; }
}
}
26 changes: 1 addition & 25 deletions src/ImageSharp.Drawing/Primitives/ShapeRegion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -31,32 +30,9 @@ public ShapeRegion(IPath shape)
/// <summary>
/// Gets the fillable shape
/// </summary>
public IPath Shape { get; }

/// <inheritdoc/>
public override int MaxIntersections { get; }
internal override IPath Shape { get; }

/// <inheritdoc/>
public override Rectangle Bounds { get; }

/// <inheritdoc/>
public override int Scan(float y, Span<float> 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<PointF> tempBuffer = configuration.MemoryAllocator.Allocate<PointF>(buffer.Length))
{
Span<PointF> 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;
}
}
}
}
2 changes: 1 addition & 1 deletion src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/ImageSharp.Drawing/Processing/PatternBrush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -155,7 +155,7 @@ public override void Apply(Span<float> 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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing
/// </summary>
public class FillRegionProcessor : IImageProcessor
{
/// <summary>
/// Minimum subpixel count for rasterization, being applied even if antialiasing is off.
/// </summary>
internal const int MinimumSubpixelCount = 8;

/// <summary>
/// Initializes a new instance of the <see cref="FillRegionProcessor" /> class.
/// </summary>
Expand Down Expand Up @@ -42,7 +47,6 @@ public FillRegionProcessor(ShapeGraphicsOptions options, IBrush brush, Region re

/// <inheritdoc />
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : unmanaged, IPixel<TPixel>
=> new FillRegionProcessor<TPixel>(configuration, this, source, sourceRectangle);
where TPixel : unmanaged, IPixel<TPixel> => new FillRegionProcessor<TPixel>(configuration, this, source, sourceRectangle);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,127 +55,85 @@ protected override void OnFrameApply(ImageFrame<TPixel> 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]
// and this can cause missed fills when not using antialiasing.so we offset the pixel grid by 0.5 in the x & y direction thus causing the#
// 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<TPixel> applicator = brush.CreateApplicator(configuration, graphicsOptions, source, rect))
using BrushApplicator<TPixel> 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<float> bBuffer = allocator.Allocate<float>(maxIntersections))
using (IMemoryOwner<float> bScanline = allocator.Allocate<float>(scanlineWidth))
using IMemoryOwner<float> bScanline = allocator.Allocate<float>(scanlineWidth);
Span<float> scanline = bScanline.Memory.Span;

while (scanner.MoveToNextPixelLine())
{
bool scanlineDirty = true;
float subpixelFraction = 1f / subpixelCount;
float subpixelFractionPoint = subpixelFraction / subpixelCount;
if (scanlineDirty)
{
scanline.Clear();
}

Span<float> buffer = bBuffer.Memory.Span;
Span<float> 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)
Expand Down
Loading

0 comments on commit 3d737b5

Please sign in to comment.