Skip to content

Commit

Permalink
Merge pull request SixLabors#1007 from mysticfall/master
Browse files Browse the repository at this point in the history
Prevent PathGradientBrush from throwing an error with corner cases
  • Loading branch information
tocsoft authored Sep 12, 2019
2 parents 62dd62f + 97f415a commit ee7471f
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 95 deletions.
53 changes: 27 additions & 26 deletions src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,27 @@ namespace SixLabors.ImageSharp.Processing
/// </summary>
public sealed class PathGradientBrush : IBrush
{
private readonly Polygon path;

private readonly IList<Edge> edges;

private readonly Color centerColor;

/// <summary>
/// Initializes a new instance of the <see cref="PathGradientBrush"/> class.
/// </summary>
/// <param name="lines">Line segments of a polygon that represents the gradient area.</param>
/// <param name="points">Points that constitute a polygon that represents the gradient area.</param>
/// <param name="colors">Array of colors that correspond to each point in the polygon.</param>
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
public PathGradientBrush(ILineSegment[] lines, Color[] colors, Color centerColor)
public PathGradientBrush(PointF[] points, Color[] colors, Color centerColor)
{
if (lines == null)
if (points == null)
{
throw new ArgumentNullException(nameof(lines));
throw new ArgumentNullException(nameof(points));
}

if (lines.Length < 3)
if (points.Length < 3)
{
throw new ArgumentOutOfRangeException(
nameof(lines),
nameof(points),
"There must be at least 3 lines to construct a path gradient brush.");
}

Expand All @@ -56,22 +54,30 @@ public PathGradientBrush(ILineSegment[] lines, Color[] colors, Color centerColor
"One or more color is needed to construct a path gradient brush.");
}

this.path = new Polygon(lines);
int size = points.Length;

var lines = new ILineSegment[size];

for (int i = 0; i < size; i++)
{
lines[i] = new LinearLineSegment(points[i % size], points[(i + 1) % size]);
}

this.centerColor = centerColor;

Color ColorAt(int index) => colors[index % colors.Length];

this.edges = this.path.LineSegments.Select(s => new Path(s))
this.edges = lines.Select(s => new Path(s))
.Select((path, i) => new Edge(path, ColorAt(i), ColorAt(i + 1))).ToList();
}

/// <summary>
/// Initializes a new instance of the <see cref="PathGradientBrush"/> class.
/// </summary>
/// <param name="lines">Line segments of a polygon that represents the gradient area.</param>
/// <param name="points">Points that constitute a polygon that represents the gradient area.</param>
/// <param name="colors">Array of colors that correspond to each point in the polygon.</param>
public PathGradientBrush(ILineSegment[] lines, Color[] colors)
: this(lines, colors, CalculateCenterColor(colors))
public PathGradientBrush(PointF[] points, Color[] colors)
: this(points, colors, CalculateCenterColor(colors))
{
}

Expand All @@ -82,7 +88,7 @@ public BrushApplicator<TPixel> CreateApplicator<TPixel>(
GraphicsOptions options)
where TPixel : struct, IPixel<TPixel>
{
return new PathGradientBrushApplicator<TPixel>(source, this.path, this.edges, this.centerColor, options);
return new PathGradientBrushApplicator<TPixel>(source, this.edges, this.centerColor, options);
}

private static Color CalculateCenterColor(Color[] colors)
Expand Down Expand Up @@ -182,8 +188,6 @@ public Vector4 ColorAt(float distance)
private class PathGradientBrushApplicator<TPixel> : BrushApplicator<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly Path path;

private readonly PointF center;

private readonly Vector4 centerColor;
Expand All @@ -196,24 +200,21 @@ private class PathGradientBrushApplicator<TPixel> : BrushApplicator<TPixel>
/// Initializes a new instance of the <see cref="PathGradientBrushApplicator{TPixel}"/> class.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="path">A polygon that represents the gradient area.</param>
/// <param name="edges">Edges of the polygon.</param>
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
/// <param name="options">The options.</param>
public PathGradientBrushApplicator(
ImageFrame<TPixel> source,
Path path,
IList<Edge> edges,
Color centerColor,
GraphicsOptions options)
: base(source, options)
{
this.path = path;
this.edges = edges;

PointF[] points = path.LineSegments.Select(s => s.EndPoint).ToArray();
PointF[] points = edges.Select(s => s.Start).ToArray();

this.center = points.Aggregate((p1, p2) => p1 + p2) / points.Length;
this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count;
this.centerColor = centerColor.ToVector4();

this.maxDistance = points.Select(p => (Vector2)(p - this.center)).Select(d => d.Length()).Max();
Expand All @@ -231,17 +232,17 @@ public PathGradientBrushApplicator(
return new Color(this.centerColor).ToPixel<TPixel>();
}

if (!this.path.Contains(point))
{
return Color.Transparent.ToPixel<TPixel>();
}

Vector2 direction = Vector2.Normalize(point - this.center);

PointF end = point + (PointF)(direction * this.maxDistance);

(Edge edge, Intersection? info) = this.FindIntersection(point, end);

if (!info.HasValue)
{
return Color.Transparent.ToPixel<TPixel>();
}

PointF intersection = info.Value.Point;

Vector4 edgeColor = edge.ColorAt(intersection);
Expand Down
86 changes: 17 additions & 69 deletions tests/ImageSharp.Tests/Drawing/FillPathGradientBrushTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using SixLabors.Shapes;

using Xunit;

Expand All @@ -27,17 +26,10 @@ public void FillRectangleWithDifferentColors<TPixel>(TestImageProvider<TPixel> p
TolerantComparer,
image =>
{
ILineSegment[] path =
{
new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
};

PointF[] points = { new PointF(0, 0), new PointF(10, 0), new PointF(10, 10), new PointF(0, 10) };
Color[] colors = { Color.Black, Color.Red, Color.Yellow, Color.Green };

var brush = new PathGradientBrush(path, colors);
var brush = new PathGradientBrush(points, colors);

image.Mutate(x => x.Fill(brush));
image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
Expand All @@ -53,16 +45,10 @@ public void FillTriangleWithDifferentColors<TPixel>(TestImageProvider<TPixel> pr
TolerantComparer,
image =>
{
ILineSegment[] path =
{
new LinearLineSegment(new PointF(5, 0), new PointF(10, 10)),
new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
new LinearLineSegment(new PointF(0, 10), new PointF(5, 0))
};

PointF[] points = { new PointF(5, 0), new PointF(10, 10), new PointF(0, 10) };
Color[] colors = { Color.Red, Color.Green, Color.Blue };

var brush = new PathGradientBrush(path, colors);
var brush = new PathGradientBrush(points, colors);

image.Mutate(x => x.Fill(brush));
image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
Expand All @@ -76,17 +62,10 @@ public void FillRectangleWithSingleColor<TPixel>(TestImageProvider<TPixel> provi
{
using (Image<TPixel> image = provider.GetImage())
{
ILineSegment[] path =
{
new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
};

PointF[] points = { new PointF(0, 0), new PointF(10, 0), new PointF(10, 10), new PointF(0, 10) };
Color[] colors = { Color.Red };

var brush = new PathGradientBrush(path, colors);
var brush = new PathGradientBrush(points, colors);

image.Mutate(x => x.Fill(brush));

Expand All @@ -103,17 +82,10 @@ public void ShouldRotateTheColorsWhenThereAreMorePoints<TPixel>(TestImageProvide
TolerantComparer,
image =>
{
ILineSegment[] path =
{
new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
};

PointF[] points = { new PointF(0, 0), new PointF(10, 0), new PointF(10, 10), new PointF(0, 10) };
Color[] colors = { Color.Red, Color.Yellow };

var brush = new PathGradientBrush(path, colors);
var brush = new PathGradientBrush(points, colors);

image.Mutate(x => x.Fill(brush));
image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
Expand All @@ -129,17 +101,10 @@ public void FillWithCustomCenterColor<TPixel>(TestImageProvider<TPixel> provider
TolerantComparer,
image =>
{
ILineSegment[] path =
{
new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
};

PointF[] points = { new PointF(0, 0), new PointF(10, 0), new PointF(10, 10), new PointF(0, 10) };
Color[] colors = { Color.Black, Color.Red, Color.Yellow, Color.Green };

var brush = new PathGradientBrush(path, colors, Color.White);
var brush = new PathGradientBrush(points, colors, Color.White);

image.Mutate(x => x.Fill(brush));
image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
Expand All @@ -157,51 +122,34 @@ public void ShouldThrowArgumentNullExceptionWhenLinesAreNull()
}

[Fact]
public void ShouldThrowArgumentOutOfRangeExceptionWhenLessThan3LinesAreGiven()
public void ShouldThrowArgumentOutOfRangeExceptionWhenLessThan3PointsAreGiven()
{
ILineSegment[] path =
{
new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
new LinearLineSegment(new PointF(10, 0), new PointF(10, 10))
};

PointF[] points = { new PointF(0, 0), new PointF(10, 0) };
Color[] colors = { Color.Black, Color.Red, Color.Yellow, Color.Green };

PathGradientBrush Create() => new PathGradientBrush(path, colors, Color.White);
PathGradientBrush Create() => new PathGradientBrush(points, colors, Color.White);

Assert.Throws<ArgumentOutOfRangeException>(Create);
}

[Fact]
public void ShouldThrowArgumentNullExceptionWhenColorsAreNull()
{
ILineSegment[] path =
{
new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
};
PointF[] points = { new PointF(0, 0), new PointF(10, 0), new PointF(10, 10), new PointF(0, 10) };

PathGradientBrush Create() => new PathGradientBrush(path, null, Color.White);
PathGradientBrush Create() => new PathGradientBrush(points, null, Color.White);

Assert.Throws<ArgumentNullException>(Create);
}

[Fact]
public void ShouldThrowArgumentOutOfRangeExceptionWhenEmptyColorArrayIsGiven()
{
ILineSegment[] path =
{
new LinearLineSegment(new PointF(0, 0), new PointF(10, 0)),
new LinearLineSegment(new PointF(10, 0), new PointF(10, 10)),
new LinearLineSegment(new PointF(10, 10), new PointF(0, 10)),
new LinearLineSegment(new PointF(0, 10), new PointF(0, 0))
};
PointF[] points = { new PointF(0, 0), new PointF(10, 0), new PointF(10, 10), new PointF(0, 10) };

var colors = new Color[0];

PathGradientBrush Create() => new PathGradientBrush(path, colors, Color.White);
PathGradientBrush Create() => new PathGradientBrush(points, colors, Color.White);

Assert.Throws<ArgumentOutOfRangeException>(Create);
}
Expand Down

0 comments on commit ee7471f

Please sign in to comment.