Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ void Draw(List<DrawingOperation> operations, IBrush brush)
int startY = operation.Location.Y;
int startX = operation.Location.X;
int offsetSpan = 0;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the issue where it fails when you try and render a glyph off image.

if (startX + buffer.Height < 0)
{
continue;
}

if (startX + buffer.Width < 0)
{
continue;
}

if (startX < 0)
{
offsetSpan = -startX;
Expand Down Expand Up @@ -335,7 +346,7 @@ private Buffer2D<float> Render(IPath path)
{
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);
int pointsFound = path.FindIntersections(start, end, intersectionSpan, IntersectionRule.Nonzero);

if (pointsFound == 0)
{
Expand Down
58 changes: 48 additions & 10 deletions src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// Licensed under the Apache License, Version 2.0.

using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Orientation = SixLabors.ImageSharp.Drawing.InternalPath.Orientation;

namespace SixLabors.ImageSharp.Drawing
{
Expand All @@ -15,6 +17,7 @@ namespace SixLabors.ImageSharp.Drawing
public sealed class ComplexPolygon : IPath
{
private readonly IPath[] paths;
private List<InternalPath> internalPaths = null;

/// <summary>
/// Initializes a new instance of the <see cref="ComplexPolygon" /> class.
Expand Down Expand Up @@ -190,21 +193,56 @@ public int FindIntersections(PointF start, PointF end, PointF[] buffer, int offs
/// <inheritdoc />
public int FindIntersections(PointF start, PointF end, Span<PointF> buffer, IntersectionRule intersectionRule)
{
int totalAdded = 0;
for (int i = 0; i < this.paths.Length; i++)
if (internalPaths == null)
{
Span<PointF> subBuffer = buffer.Slice(totalAdded);
int added = this.paths[i].FindIntersections(start, end, subBuffer, intersectionRule);
totalAdded += added;
lock (paths)
{
internalPaths = new List<InternalPath>(paths.Length);

foreach (var p in this.paths)
{
foreach (var s in p.Flatten())
{
var ip = new InternalPath(s.Points, s.IsClosed);
internalPaths.Add(ip);
}
}
}
}

Span<float> distances = stackalloc float[totalAdded];
for (int i = 0; i < totalAdded; i++)
int totalAdded = 0;
Orientation[] orientations = ArrayPool<Orientation>.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<Orientation> orientationsSpan = orientations;
try
{
distances[i] = Vector2.DistanceSquared(start, buffer[i]);
}
foreach (var ip in this.internalPaths)
{
Span<PointF> subBuffer = buffer.Slice(totalAdded);
Span<Orientation> subOrientationsSpan = orientationsSpan.Slice(totalAdded);

var position = ip.FindIntersectionsWithOrientation(start, end, subBuffer, subOrientationsSpan);
totalAdded += position;
}

QuickSort.Sort(distances, buffer.Slice(0, totalAdded));
Span<float> distances = stackalloc float[totalAdded];
for (int i = 0; i < totalAdded; i++)
{
distances[i] = Vector2.DistanceSquared(start, buffer[i]);
}

var activeBuffer = buffer.Slice(0, totalAdded);
var activeOrientationsSpan = orientationsSpan.Slice(0, totalAdded);
QuickSort.Sort(distances, activeBuffer, activeOrientationsSpan);

if (intersectionRule == IntersectionRule.Nonzero)
{
totalAdded = InternalPath.ApplyNonZeroIntersectionRules(activeBuffer, activeOrientationsSpan);
}
}
finally
{
ArrayPool<Orientation>.Shared.Return(orientations);
}

return totalAdded;
}
Expand Down
113 changes: 77 additions & 36 deletions src/ImageSharp.Drawing/Shapes/InternalPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private InternalPath(PointData[] points, bool isClosedPath)
/// <summary>
/// the orrientateion of an point form a line
/// </summary>
private enum Orientation
internal enum Orientation
{
/// <summary>
/// Point is colienear
Expand Down Expand Up @@ -189,6 +189,41 @@ public int FindIntersections(PointF start, PointF end, Span<PointF> buffer)
/// <param name="intersectionRule">Intersection rule types</param>
/// <returns>number of intersections hit</returns>
public int FindIntersections(PointF start, PointF end, Span<PointF> buffer, IntersectionRule intersectionRule)
{
Orientation[] orientations = ArrayPool<Orientation>.Shared.Rent(buffer.Length);
try
{
Span<Orientation> orientationsSpan = orientations.AsSpan(0, buffer.Length);
var position = this.FindIntersectionsWithOrientation(start, end, buffer, orientationsSpan);

var activeBuffer = buffer.Slice(0, position);
var activeOrientationsSpan = orientationsSpan.Slice(0, position);

// intersection rules only really apply to closed paths
if (intersectionRule == IntersectionRule.Nonzero && this.closedPath)
{
position = ApplyNonZeroIntersectionRules(activeBuffer, activeOrientationsSpan);
}

return position;

}
finally
{
ArrayPool<Orientation>.Shared.Return(orientations);
}
}

/// <summary>
/// Based on a line described by <paramref name="start" /> and <paramref name="end" />
/// populates a buffer for all points on the path that the line intersects.
/// </summary>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="orientationsSpan">The buffer for storeing the orientation of each intersection.</param>
/// <returns>number of intersections hit</returns>
public int FindIntersectionsWithOrientation(PointF start, PointF end, Span<PointF> buffer, Span<Orientation> orientationsSpan)
{
if (this.points.Length < 2)
{
Expand All @@ -212,8 +247,9 @@ public int FindIntersections(PointF start, PointF end, Span<PointF> buffer, Inte
Vector2 lastPoint = MaxVector;

PassPointData[] precaclulate = ArrayPool<PassPointData>.Shared.Rent(this.points.Length);
Orientation[] orientations = ArrayPool<Orientation>.Shared.Rent(this.points.Length);
Span<Orientation> orientationsSpan = orientations.AsSpan(0, this.points.Length);

//Orientation[] orientations = ArrayPool<Orientation>.Shared.Rent(this.points.Length);
//Span<Orientation> orientationsSpan = orientations.AsSpan(0, this.points.Length);
Span<PassPointData> precaclulateSpan = precaclulate.AsSpan(0, this.points.Length);

try
Expand Down Expand Up @@ -341,48 +377,53 @@ public int FindIntersections(PointF start, PointF end, Span<PointF> buffer, Inte
distances[i] = Vector2.DistanceSquared(startVector, buffer[i]);
}

QuickSort.Sort(distances, buffer.Slice(0, position), orientationsSpan.Slice(0, position));

// intersection rules only really apply to closed paths
if (intersectionRule == IntersectionRule.Nonzero && this.closedPath)
{
int newpositions = 0;
int tracker = 0;
for (int i = 0; i < position; i++)
{
bool include = tracker == 0;
switch (orientationsSpan[i])
{
case Orientation.Clockwise:
tracker++;
break;
case Orientation.Counterclockwise:
tracker--;
break;
case Orientation.Colinear:
default:
break;
}

if (include || tracker == 0)
{
buffer[newpositions] = buffer[i];
newpositions++;
}
}

position = newpositions;
}
var activeBuffer = buffer.Slice(0, position);
var activeOrientationsSpan = orientationsSpan.Slice(0, position);
QuickSort.Sort(distances, activeBuffer, activeOrientationsSpan);

return position;
}
finally
{
ArrayPool<Orientation>.Shared.Return(orientations);
ArrayPool<PassPointData>.Shared.Return(precaclulate);
}
}

internal static int ApplyNonZeroIntersectionRules(Span<PointF> buffer, Span<Orientation> orientationsSpan)
{
int newpositions = 0;
int tracker = 0;
int diff = 0;
for (int i = 0; i < buffer.Length; i++)
{
bool include = tracker == 0;
switch (orientationsSpan[i])
{
case Orientation.Counterclockwise:
diff = 1;
break;
case Orientation.Clockwise:
diff = -1;
break;
case Orientation.Colinear:
default:
diff *= -1;
break;
}

tracker += diff;

if (include || tracker == 0)
{
buffer[newpositions] = buffer[i];
newpositions++;
}
}

return newpositions;
}


/// <summary>
/// Determines if the specified point is inside or outside the path.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ public void DoesntThrowExceptionWhenOverlappingRightEdge_Issue688_2<TPixel>(Test
}


[Theory]
[WithSolidFilledImages(200, 200, "White", PixelTypes.Rgba32)]
public void OpenSansJWithNoneZeroShouldntExtendPastGlyphe<TPixel>(TestImageProvider<TPixel> provider)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tracking a regression the switch to nonzero caused

where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> img = provider.GetImage())
{
Font font = CreateFont("OpenSans-Regular.ttf", 50);
Color color = Color.Black;

img.Mutate(ctx => ctx.DrawText(TestText, font, Color.Black, new PointF(-50, 2)));

Assert.Equal(Color.White.ToPixel<TPixel>(), img[173, 2]);
}
}

[Theory]
[WithSolidFilledImages(200, 100, "White", PixelTypes.Rgba32, 50, 0, 0, "SixLaborsSampleAB.woff", AB)]
[WithSolidFilledImages(900, 100, "White", PixelTypes.Rgba32, 50, 0, 0, "OpenSans-Regular.ttf", TestText)]
Expand Down
52 changes: 52 additions & 0 deletions tests/ImageSharp.Drawing.Tests/Shapes/ComplexPolygonTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,58 @@ public ComplexPolygonTests(ITestOutputHelper output)

private ITestOutputHelper Output { get; }

[Fact]
public void NonezeroFindIntersections()
{
var simplePath1 = new Polygon(new LinearLineSegment(
new PointF(2, 1),
new PointF(2, 5),
new PointF(3, 5),
new PointF(3, 1)));

var simplePath2 = new Polygon(new LinearLineSegment(
new PointF(1, 2),
new PointF(1, 3),
new PointF(4, 3),
new PointF(4, 2)));

var complex = new ComplexPolygon(simplePath1, simplePath2);

var buffer = new PointF[10];
var points = complex.FindIntersections(new PointF(0, 2.5f), new PointF(6, 2.5f), buffer, 0, IntersectionRule.Nonzero);

Assert.Equal(2, points);
Assert.Equal(1, buffer[0].X);
Assert.Equal(4, buffer[1].X);
}

[Fact]
public void OddEvenFindIntersections()
{
var simplePath1 = new Polygon(new LinearLineSegment(
new PointF(2, 1),
new PointF(2, 5),
new PointF(3, 5),
new PointF(3, 1)));

var simplePath2 = new Polygon(new LinearLineSegment(
new PointF(1, 2),
new PointF(1, 3),
new PointF(4, 3),
new PointF(4, 2)));

var complex = new ComplexPolygon(simplePath1, simplePath2);

var buffer = new PointF[10];
var points = complex.FindIntersections(new PointF(0, 2.5f), new PointF(6, 2.5f), buffer, 0, IntersectionRule.OddEven);

Assert.Equal(4, points);
Assert.Equal(1, buffer[0].X);
Assert.Equal(2, buffer[1].X);
Assert.Equal(3, buffer[2].X);
Assert.Equal(4, buffer[3].X);
}

[Fact]
public void MissingIntersection()
{
Expand Down