Browse Source

Add drawing methods.

Adds a 4 `IPath`s;
 - `Path` : a generic open path described by an arbitrary collection of `ILineSegments` which are closed.
 - `Polygon` : a generic closed path described by an arbitrary collection of `ILineSegments` which are closed.

Adds 2 `ILineSegment`s;
 - `LinearLineSegment` : a segment made up of control points representing straight lines.
 - `BezierLineSegment` : a path made up of control points describing a bezier path.

Adds 4 `IShape`s
 - `Polygon` : a generic closed path described by an arbitrary collection of `ILineSegments` which are closed.
 - `LinearPolygon` : a shape made up of control points representing straight lines.
 - `BezierPolygon` : a shape made up of control points describing a set of bezier curves which are closed by a straight line
 - `ComplexPolygon` : a polygon described by one or more `IShape`s as either outlines or holes.
All `IShape`s can be represents as a collection of `IPath`.

Adds 2 `IBrush`s
 - `SolidBrush` : described by a `TColor`
 - `Patternbrush` : described by a foreground and background `TColor` and a pattern.

Adds 1 `IPen`:
 - `Pen` : described by an `IBrush` a width/thickness to draw the line and an optional pattern.

Adds 3 Image Processors
 - `FillProcessor` : flood fills the image with an `IBrush`.
 - `FillShapeProcessor` : flood fills inside an `IShape`.
 - `DrawPathProcessor` : draws a collection of `IPath`s or and `IShape`s onto the image using an `IPen` to determine `TColor`.

Adds a multitude of Image Extensions for accessing the above processors in a manor similar to `System.Drawing`
pull/37/head
Scott Williams 10 years ago
parent
commit
b9a63120c1
  1. 5
      global.json
  2. 302
      src/ImageSharp/Drawing/Brushes/Brushes.cs
  3. 35
      src/ImageSharp/Drawing/Brushes/IBrush.cs
  4. 146
      src/ImageSharp/Drawing/Brushes/PatternBrush.cs
  5. 28
      src/ImageSharp/Drawing/Brushes/Processors/IBrushApplicator.cs
  6. 107
      src/ImageSharp/Drawing/Brushes/SolidBrush.cs
  7. 421
      src/ImageSharp/Drawing/Draw.cs
  8. 150
      src/ImageSharp/Drawing/Fill.cs
  9. 128
      src/ImageSharp/Drawing/Paths/BezierLineSegment.cs
  10. 25
      src/ImageSharp/Drawing/Paths/ILineSegment.cs
  11. 51
      src/ImageSharp/Drawing/Paths/IPath.cs
  12. 279
      src/ImageSharp/Drawing/Paths/InternalPath.cs
  13. 83
      src/ImageSharp/Drawing/Paths/LinearLineSegment.cs
  14. 81
      src/ImageSharp/Drawing/Paths/Path.cs
  15. 39
      src/ImageSharp/Drawing/Paths/PointInfo.cs
  16. 31
      src/ImageSharp/Drawing/Pens/IPen.cs
  17. 315
      src/ImageSharp/Drawing/Pens/Pen.cs
  18. 208
      src/ImageSharp/Drawing/Pens/Pens.cs
  19. 27
      src/ImageSharp/Drawing/Pens/Processors/ColoredPointInfo.cs
  20. 35
      src/ImageSharp/Drawing/Pens/Processors/IPenApplicator.cs
  21. 160
      src/ImageSharp/Drawing/Processors/DrawPathProcessor.cs
  22. 97
      src/ImageSharp/Drawing/Processors/FillProcessor.cs
  23. 129
      src/ImageSharp/Drawing/Processors/FillShapeProcessor.cs
  24. 78
      src/ImageSharp/Drawing/Shapes/BezierPolygon.cs
  25. 4924
      src/ImageSharp/Drawing/Shapes/Clipper.cs
  26. 197
      src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs
  27. 33
      src/ImageSharp/Drawing/Shapes/IShape.cs
  28. 72
      src/ImageSharp/Drawing/Shapes/LinearPolygon.cs
  29. 134
      src/ImageSharp/Drawing/Shapes/Polygon.cs
  30. 287
      src/ImageSharp/Numerics/PointF.cs
  31. 13
      src/ImageSharp/Numerics/Rectangle.cs
  32. 334
      src/ImageSharp/Numerics/RectangleF.cs
  33. 64
      tests/ImageSharp.Benchmarks/Drawing/DrawBeziers.cs
  34. 62
      tests/ImageSharp.Benchmarks/Drawing/DrawLines.cs
  35. 62
      tests/ImageSharp.Benchmarks/Drawing/DrawPolygon.cs
  36. 59
      tests/ImageSharp.Benchmarks/Drawing/FillPolygon.cs
  37. 103
      tests/ImageSharp.Tests/Drawing/BeziersTests.cs
  38. 105
      tests/ImageSharp.Tests/Drawing/DrawPathTests.cs
  39. 235
      tests/ImageSharp.Tests/Drawing/FillPatternTests.cs
  40. 89
      tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs
  41. 195
      tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs
  42. 197
      tests/ImageSharp.Tests/Drawing/LineTests.cs
  43. 132
      tests/ImageSharp.Tests/Drawing/PolygonTests.cs
  44. 101
      tests/ImageSharp.Tests/Drawing/SolidBezierTests.cs
  45. 141
      tests/ImageSharp.Tests/Drawing/SolidComplexPolygonTests.cs
  46. 120
      tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs
  47. 10
      tests/ImageSharp.Tests/FileTestBase.cs

5
global.json

@ -1,3 +1,6 @@
{
"projects": [ "src" ]
"projects": [ "src" ],
"sdk": {
"version": "1.0.0-preview2-003121"
}
}

302
src/ImageSharp/Drawing/Brushes/Brushes.cs

@ -0,0 +1,302 @@
// <copyright file="Brushes.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Brushes
{
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// A collection of methods for creating brushes
/// </summary>
public class Brushes
{
/// <summary>
/// Create as brush that will paint a solid color
/// </summary>
/// <param name="color">The color.</param>
/// <returns>A Brush</returns>
public static SolidBrush Solid(Color color)
=> new SolidBrush(color);
/// <summary>
/// Create as brush that will paint a Percent10 Hatch Pattern with
/// in the specified foreground color and a transparent background
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <returns>A Brush</returns>
public static PatternBrush Percent10(Color foreColor)
=> new PatternBrush(Brushes<Color, uint>.Percent10(foreColor, Color.Transparent));
/// <summary>
/// Create as brush that will paint a Percent10 Hatch Pattern with
/// in the specified foreground and background colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush Percent10(Color foreColor, Color backColor)
=> new PatternBrush(Brushes<Color, uint>.Percent10(foreColor, backColor));
/// <summary>
/// Create as brush that will paint a Percent20 Hatch Pattern with
/// in the specified foreground color and a transparent background
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <returns>A Brush</returns>
public static PatternBrush Percent20(Color foreColor)
=> new PatternBrush(Brushes<Color, uint>.Percent20(foreColor, Color.Transparent));
/// <summary>
/// Create as brush that will paint a Percent20 Hatch Pattern with
/// in the specified foreground and background colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush Percent20(Color foreColor, Color backColor)
=> new PatternBrush(Brushes<Color, uint>.Percent20(foreColor, backColor));
/// <summary>
/// Create as brush that will paint a Horizontal Hatch Pattern with
/// in the specified foreground color and a transparent background
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <returns>A Brush</returns>
public static PatternBrush Horizontal(Color foreColor)
=> new PatternBrush(Brushes<Color, uint>.Horizontal(foreColor, Color.Transparent));
/// <summary>
/// Create as brush that will paint a Horizontal Hatch Pattern with
/// in the specified foreground and background colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush Horizontal(Color foreColor, Color backColor)
=> new PatternBrush(Brushes<Color, uint>.Horizontal(foreColor, backColor));
/// <summary>
/// Create as brush that will paint a Min Hatch Pattern with
/// in the specified foreground color and a transparent background
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <returns>A Brush</returns>
public static PatternBrush Min(Color foreColor)
=> new PatternBrush(Brushes<Color, uint>.Min(foreColor, Color.Transparent));
/// <summary>
/// Create as brush that will paint a Min Hatch Pattern with
/// in the specified foreground and background colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush Min(Color foreColor, Color backColor)
=> new PatternBrush(Brushes<Color, uint>.Min(foreColor, backColor));
/// <summary>
/// Create as brush that will paint a Vertical Hatch Pattern with
/// in the specified foreground color and a transparent background
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <returns>A Brush</returns>
public static PatternBrush Vertical(Color foreColor)
=> new PatternBrush(Brushes<Color, uint>.Vertical(foreColor, Color.Transparent));
/// <summary>
/// Create as brush that will paint a Vertical Hatch Pattern with
/// in the specified foreground and background colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush Vertical(Color foreColor, Color backColor)
=> new PatternBrush(Brushes<Color, uint>.Vertical(foreColor, backColor));
/// <summary>
/// Create as brush that will paint a Forward Diagonal Hatch Pattern with
/// in the specified foreground color and a transparent background
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <returns>A Brush</returns>
public static PatternBrush ForwardDiagonal(Color foreColor)
=> new PatternBrush(Brushes<Color, uint>.ForwardDiagonal(foreColor, Color.Transparent));
/// <summary>
/// Create as brush that will paint a Forward Diagonal Hatch Pattern with
/// in the specified foreground and background colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush ForwardDiagonal(Color foreColor, Color backColor)
=> new PatternBrush(Brushes<Color, uint>.ForwardDiagonal(foreColor, backColor));
/// <summary>
/// Create as brush that will paint a Backward Diagonal Hatch Pattern with
/// in the specified foreground color and a transparent background
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <returns>A Brush</returns>
public static PatternBrush BackwardDiagonal(Color foreColor)
=> new PatternBrush(Brushes<Color, uint>.BackwardDiagonal(foreColor, Color.Transparent));
/// <summary>
/// Create as brush that will paint a Backward Diagonal Hatch Pattern with
/// in the specified foreground and background colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush BackwardDiagonal(Color foreColor, Color backColor)
=> new PatternBrush(Brushes<Color, uint>.BackwardDiagonal(foreColor, backColor));
}
/// <summary>
/// A collection of methods for creating brushes.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <returns>A Brush</returns>
public partial class Brushes<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
// note 2d arrays when configured using initalizer look inverted
// ---> Y axis
// ^
// | X - axis
// |
private static readonly bool[,] Percent10Pattern = new bool[,]
{
{ true, false, false, false },
{ false, false, false, false },
{ false, false, true, false },
{ false, false, false, false }
};
private static readonly bool[,] Percent20Pattern = new bool[,]
{
{ true, false, true, false },
{ false, false, false, false },
{ false, true, false, true },
{ false, false, false, false }
};
private static readonly bool[,] HorizontalPattern = new bool[,]
{
{ false, true, false, false },
{ false, true, false, false },
{ false, true, false, false },
{ false, true, false, false }
};
private static readonly bool[,] MinPattern = new bool[,]
{
{ false, false, false, true },
{ false, false, false, true },
{ false, false, false, true },
{ false, false, false, true }
};
private static readonly bool[,] VerticalPattern = new bool[,]
{
{ false, false, false, false },
{ true, true, true, true },
{ false, false, false, false },
{ false, false, false, false }
};
private static readonly bool[,] ForwardDiagonalPattern = new bool[,]
{
{ true, false, false, false },
{ false, true, false, false },
{ false, false, true, false },
{ false, false, false, true }
};
private static readonly bool[,] BackwardDiagonalPattern = new bool[,]
{
{ false, false, false, true },
{ false, false, true, false },
{ false, true, false, false },
{ true, false, false, false }
};
/// <summary>
/// Create as brush that will paint a solid color
/// </summary>
/// <param name="color">The color.</param>
/// <returns>A Brush</returns>
public static SolidBrush<TColor, TPacked> Solid(TColor color)
=> new SolidBrush<TColor, TPacked>(color);
/// <summary>
/// Create as brush that will paint a Percent10 Hatch Pattern within the specified colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush<TColor, TPacked> Percent10(TColor foreColor, TColor backColor)
=> new PatternBrush<TColor, TPacked>(foreColor, backColor, Percent10Pattern);
/// <summary>
/// Create as brush that will paint a Percent20 Hatch Pattern within the specified colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush<TColor, TPacked> Percent20(TColor foreColor, TColor backColor)
=> new PatternBrush<TColor, TPacked>(foreColor, backColor, Percent20Pattern);
/// <summary>
/// Create as brush that will paint a Horizontal Hatch Pattern within the specified colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush<TColor, TPacked> Horizontal(TColor foreColor, TColor backColor)
=> new PatternBrush<TColor, TPacked>(foreColor, backColor, HorizontalPattern);
/// <summary>
/// Create as brush that will paint a Min Hatch Pattern within the specified colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush<TColor, TPacked> Min(TColor foreColor, TColor backColor)
=> new PatternBrush<TColor, TPacked>(foreColor, backColor, MinPattern);
/// <summary>
/// Create as brush that will paint a Vertical Hatch Pattern within the specified colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush<TColor, TPacked> Vertical(TColor foreColor, TColor backColor)
=> new PatternBrush<TColor, TPacked>(foreColor, backColor, VerticalPattern);
/// <summary>
/// Create as brush that will paint a Forward Diagonal Hatch Pattern within the specified colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush<TColor, TPacked> ForwardDiagonal(TColor foreColor, TColor backColor)
=> new PatternBrush<TColor, TPacked>(foreColor, backColor, ForwardDiagonalPattern);
/// <summary>
/// Create as brush that will paint a Backward Diagonal Hatch Pattern within the specified colors
/// </summary>
/// <param name="foreColor">Color of the foreground.</param>
/// <param name="backColor">Color of the background.</param>
/// <returns>A Brush</returns>
public static PatternBrush<TColor, TPacked> BackwardDiagonal(TColor foreColor, TColor backColor)
=> new PatternBrush<TColor, TPacked>(foreColor, backColor, BackwardDiagonalPattern);
}
}

35
src/ImageSharp/Drawing/Brushes/IBrush.cs

@ -0,0 +1,35 @@
// <copyright file="IBrush.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing
{
using System;
using Processors;
/// <summary>
/// Brush represents a logical configuration of a brush whcih can be used to source pixel colors
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <remarks>
/// A brush is a simple class that will return an <see cref="IBrushApplicator{TColor, TPacked}" /> that will perform the
/// logic for converting a pixel location to a <typeparamref name="TColor"/>.
/// </remarks>
public interface IBrush<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
/// <summary>
/// Creates the applicator for this brush.
/// </summary>
/// <param name="region">The region the brush will be applied to.</param>
/// <returns>The brush applicator for this brush</returns>
/// <remarks>
/// The <paramref name="region" /> when being applied to things like shapes would usually be the
/// bounding box of the shape not necessarily the bounds of the whole image
/// </remarks>
IBrushApplicator<TColor, TPacked> CreateApplicator(RectangleF region);
}
}

146
src/ImageSharp/Drawing/Brushes/PatternBrush.cs

@ -0,0 +1,146 @@
// <copyright file="PatternBrush.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Brushes
{
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using Processors;
/// <summary>
/// Provides an implementaion of a pattern brush for painting patterns.
/// </summary>
public partial class PatternBrush : PatternBrush<Color, uint>
{
/// <summary>
/// Initializes a new instance of the <see cref="PatternBrush"/> class.
/// </summary>
/// <param name="foreColor">Color of the fore.</param>
/// <param name="backColor">Color of the back.</param>
/// <param name="pattern">The pattern.</param>
public PatternBrush(Color foreColor, Color backColor, bool[,] pattern)
: base(foreColor, backColor, pattern)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PatternBrush"/> class.
/// </summary>
/// <param name="brush">The brush.</param>
internal PatternBrush(PatternBrush<Color, uint> brush)
: base(brush)
{
}
}
/// <summary>
/// Provides an implementaion of a pattern brush for painting patterns.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
public partial class PatternBrush<TColor, TPacked> : IBrush<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
private readonly TColor foreColor;
private readonly TColor backColor;
private readonly bool[,] pattern;
/// <summary>
/// Initializes a new instance of the <see cref="PatternBrush{TColor, TPacked}"/> class.
/// </summary>
/// <param name="foreColor">Color of the fore.</param>
/// <param name="backColor">Color of the back.</param>
/// <param name="pattern">The pattern.</param>
public PatternBrush(TColor foreColor, TColor backColor, bool[,] pattern)
{
this.foreColor = foreColor;
this.backColor = backColor;
this.pattern = pattern;
}
/// <summary>
/// Initializes a new instance of the <see cref="PatternBrush{TColor, TPacked}"/> class.
/// </summary>
/// <param name="brush">The brush.</param>
internal PatternBrush(PatternBrush<TColor, TPacked> brush)
: this(brush.foreColor, brush.backColor, brush.pattern)
{
}
/// <summary>
/// Creates the applicator for this bursh.
/// </summary>
/// <param name="region">The region the brush will be applied to.</param>
/// <returns>
/// The brush applicator for this brush
/// </returns>
/// <remarks>
/// The <paramref name="region" /> when being applied to things like shapes would ussually be the
/// bounding box of the shape not necessarily the bounds of the whole image
/// </remarks>
public IBrushApplicator<TColor, TPacked> CreateApplicator(RectangleF region)
{
return new PatternBrushApplicator(this.foreColor, this.backColor, this.pattern);
}
private class PatternBrushApplicator : IBrushApplicator<TColor, TPacked>
{
private readonly int xLength;
private readonly int yLength;
private readonly bool[,] pattern;
private readonly TColor backColor = default(TColor);
private readonly TColor foreColor = default(TColor);
/// <summary>
/// Initializes a new instance of the <see cref="PatternBrushApplicator"/> class.
/// </summary>
/// <param name="foreColor">Color of the fore.</param>
/// <param name="backColor">Color of the back.</param>
/// <param name="pattern">The pattern.</param>
public PatternBrushApplicator(TColor foreColor, TColor backColor, bool[,] pattern)
{
this.foreColor = foreColor;
this.backColor = backColor;
this.pattern = pattern;
this.xLength = this.pattern.GetLength(0);
this.yLength = this.pattern.GetLength(1);
}
/// <summary>
/// Gets the color for a single pixel.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>
/// The color
/// </returns>
public TColor GetColor(Vector2 point)
{
var x = (int)point.X % this.xLength;
var y = (int)point.Y % this.yLength;
if (this.pattern[x, y])
{
return this.foreColor;
}
else
{
return this.backColor;
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
// noop
}
}
}
}

28
src/ImageSharp/Drawing/Brushes/Processors/IBrushApplicator.cs

@ -0,0 +1,28 @@
// <copyright file="IBrushApplicator.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Processors
{
using System;
using System.Numerics;
/// <summary>
/// primitive that converts a point in to a color for discoving the fill color based on an implmentation
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <seealso cref="System.IDisposable" />
public interface IBrushApplicator<TColor, TPacked> : IDisposable // disposable will be required if/when there is an ImageBrush
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
/// <summary>
/// Gets the color for a single pixel.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>The color</returns>
TColor GetColor(Vector2 point);
}
}

107
src/ImageSharp/Drawing/Brushes/SolidBrush.cs

@ -0,0 +1,107 @@
// <copyright file="SolidBrush.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Brushes
{
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using Processors;
/// <summary>
/// Provides an implementaion of a solid brush for painting solid color areas.
/// </summary>
public class SolidBrush : SolidBrush<Color, uint>
{
/// <summary>
/// Initializes a new instance of the <see cref="SolidBrush" /> class.
/// </summary>
/// <param name="color">The color.</param>
public SolidBrush(Color color)
: base(color)
{
}
}
/// <summary>
/// Provides an implementaion of a solid brush for painting solid color areas.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
public class SolidBrush<TColor, TPacked> : IBrush<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
private readonly TColor color;
/// <summary>
/// Initializes a new instance of the <see cref="SolidBrush{TColor, TPacked}"/> class.
/// </summary>
/// <param name="color">The color.</param>
public SolidBrush(TColor color)
{
this.color = color;
}
/// <summary>
/// Gets the color.
/// </summary>
/// <value>
/// The color.
/// </value>
public TColor Color => this.color;
/// <summary>
/// Creates the applicator for this brush.
/// </summary>
/// <param name="region">The region the brush will be applied to.</param>
/// <returns>
/// The brush applicator for this brush
/// </returns>
/// <remarks>
/// The <paramref name="region" /> when being applied to things like shapes would ussually be the
/// bounding box of the shape not necessarily the bounds of the whole image
/// </remarks>
public IBrushApplicator<TColor, TPacked> CreateApplicator(RectangleF region)
{
return new SolidBrushApplicator(this.color);
}
private class SolidBrushApplicator : IBrushApplicator<TColor, TPacked>
{
private TColor color;
/// <summary>
/// Initializes a new instance of the <see cref="SolidBrushApplicator"/> class.
/// </summary>
/// <param name="color">The color.</param>
public SolidBrushApplicator(TColor color)
{
this.color = color;
}
/// <summary>
/// Gets the color for a single pixel.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>
/// The color
/// </returns>
public TColor GetColor(Vector2 point)
{
return this.color;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
// noop
}
}
}
}

421
src/ImageSharp/Drawing/Draw.cs

@ -0,0 +1,421 @@
// <copyright file="Draw.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp
{
using Drawing;
using Drawing.Brushes;
using Drawing.Paths;
using Drawing.Pens;
using Drawing.Processors;
using Drawing.Shapes;
using Processors;
/// <summary>
/// Extension methods for the <see cref="Image{TColor, TPacked}"/> type.
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Draws the outline of the polygon with the provided pen.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="pen">The pen.</param>
/// <param name="shape">The shape.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, IPen<TColor, TPacked> pen, IShape shape)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.Process(new DrawPathProcessor<TColor, TPacked>(pen, shape));
}
/// <summary>
/// Draws the outline of the polygon with the provided brush at the provided thickness.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="shape">The shape.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, float thickness, IShape shape)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPolygon(new Pen<TColor, TPacked>(brush, thickness), shape);
}
/// <summary>
/// Draws the outline of the polygon with the provided brush at the provided thickness.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="shape">The shape.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, float thickness, IShape shape)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPolygon(new SolidBrush<TColor, TPacked>(color), thickness, shape);
}
/// <summary>
/// Draws the provided Points as a closed Linear Polygon with the provided brush at the provided thickness.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, float thickness, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPolygon(new Pen<TColor, TPacked>(brush, thickness), new Polygon(new LinearLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as a closed Linear Polygon with the provided brush at the provided thickness.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, float thickness, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPolygon(new SolidBrush<TColor, TPacked>(color), thickness, points);
}
/// <summary>
/// Draws the provided Points as a closed Linear Polygon with the provided Pen.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="pen">The pen.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, IPen<TColor, TPacked> pen, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPolygon(pen, new Polygon(new LinearLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as a closed Linear Polygon with the provided brush at the provided thickness.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, float thickness, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPolygon(new Pen<TColor, TPacked>(brush, thickness), new Polygon(new LinearLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as a closed Linear Polygon with the provided brush at the provided thickness.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, float thickness, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPolygon(new SolidBrush<TColor, TPacked>(color), thickness, points);
}
/// <summary>
/// Draws the provided Points as a closed Linear Polygon with the provided Pen.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="pen">The pen.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, IPen<TColor, TPacked> pen, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPolygon(pen, new Polygon(new LinearLineSegment(points)));
}
/// <summary>
/// Draws the path with the provided pen.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="pen">The pen.</param>
/// <param name="path">The path.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPath<TColor, TPacked>(this Image<TColor, TPacked> source, IPen<TColor, TPacked> pen, IPath path)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.Process(new DrawPathProcessor<TColor, TPacked>(pen, path));
}
/// <summary>
/// Draws the path with the bursh at the privdied thickness.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="path">The path.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPath<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, float thickness, IPath path)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(new Pen<TColor, TPacked>(brush, thickness), path);
}
/// <summary>
/// Draws the path with the bursh at the privdied thickness.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="path">The path.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawPath<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, float thickness, IPath path)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(new SolidBrush<TColor, TPacked>(color), thickness, path);
}
/// <summary>
/// Draws the provided Points as an open Linear path at the provided thickness with the supplied brush
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawLines<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, float thickness, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(new Pen<TColor, TPacked>(brush, thickness), new Path(new LinearLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as an open Linear path at the provided thickness with the supplied brush
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawLines<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, float thickness, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawLines(new SolidBrush<TColor, TPacked>(color), thickness, points);
}
/// <summary>
/// Draws the provided Points as an open Linear path with the supplied pen
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="pen">The pen.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawLines<TColor, TPacked>(this Image<TColor, TPacked> source, IPen<TColor, TPacked> pen, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(pen, new Path(new LinearLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as an open Linear path at the provided thickness with the supplied brush
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawLines<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, float thickness, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(new Pen<TColor, TPacked>(brush, thickness), new Path(new LinearLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as an open Linear path at the provided thickness with the supplied brush
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawLines<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, float thickness, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawLines(new SolidBrush<TColor, TPacked>(color), thickness, points);
}
/// <summary>
/// Draws the provided Points as an open Linear path with the supplied pen
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="pen">The pen.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawLines<TColor, TPacked>(this Image<TColor, TPacked> source, IPen<TColor, TPacked> pen, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(pen, new Path(new LinearLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as an open Bezier path at the provided thickness with the supplied brush
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawBeziers<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, float thickness, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(new Pen<TColor, TPacked>(brush, thickness), new Path(new BezierLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as an open Bezier path at the provided thickness with the supplied brush
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawBeziers<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, float thickness, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawBeziers(new SolidBrush<TColor, TPacked>(color), thickness, points);
}
/// <summary>
/// Draws the provided Points as an open Bezier path with the supplied pen
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="pen">The pen.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawBeziers<TColor, TPacked>(this Image<TColor, TPacked> source, IPen<TColor, TPacked> pen, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(pen, new Path(new BezierLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as an open Bezier path at the provided thickness with the supplied brush
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawBeziers<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, float thickness, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(new Pen<TColor, TPacked>(brush, thickness), new Path(new BezierLineSegment(points)));
}
/// <summary>
/// Draws the provided Points as an open Bezier path at the provided thickness with the supplied brush
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="thickness">The thickness.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawBeziers<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, float thickness, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawBeziers(new SolidBrush<TColor, TPacked>(color), thickness, points);
}
/// <summary>
/// Draws the provided Points as an open Bezier path with the supplied pen
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="pen">The pen.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> DrawBeziers<TColor, TPacked>(this Image<TColor, TPacked> source, IPen<TColor, TPacked> pen, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.DrawPath(pen, new Path(new BezierLineSegment(points)));
}
}
}

150
src/ImageSharp/Drawing/Fill.cs

@ -0,0 +1,150 @@
// <copyright file="Fill.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp
{
using Drawing;
using Drawing.Brushes;
using Drawing.Paths;
using Drawing.Processors;
using Drawing.Shapes;
using Processors;
/// <summary>
/// Extension methods for the <see cref="Image{TColor, TPacked}"/> type.
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Flood fills the image with the specified brush.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> Fill<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.Process(new FillProcessor<TColor, TPacked>(brush));
}
/// <summary>
/// Flood fills the image with the specified color.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> Fill<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.Fill(new SolidBrush<TColor, TPacked>(color));
}
/// <summary>
/// Flood fills the image in the shape o fhte provided polygon with the specified brush..
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="shape">The shape.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> Fill<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, IShape shape)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.Process(new FillShapeProcessor<TColor, TPacked>(brush, shape));
}
/// <summary>
/// Flood fills the image in the shape o fhte provided polygon with the specified brush..
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="shape">The shape.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> Fill<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, IShape shape)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
return source.Fill(new SolidBrush<TColor, TPacked>(color), shape);
}
/// <summary>
/// Flood fills the image in the shape of a Linear polygon described by the points
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> FillPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
// using Polygon directly instead of LinearPolygon as its will have less indirection
return source.Fill(brush, new Polygon(new LinearLineSegment(points)));
}
/// <summary>
/// Flood fills the image in the shape of a Linear polygon described by the points
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> FillPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, PointF[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
// using Polygon directly instead of LinearPolygon as its will have less indirection
return source.Fill(new SolidBrush<TColor, TPacked>(color), new Polygon(new LinearLineSegment(points)));
}
/// <summary>
/// Flood fills the image in the shape of a Linear polygon described by the points
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="brush">The brush.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> FillPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, IBrush<TColor, TPacked> brush, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
// using Polygon directly instead of LinearPolygon as its will have less indirection
return source.Fill(brush, new Polygon(new LinearLineSegment(points)));
}
/// <summary>
/// Flood fills the image in the shape of a Linear polygon described by the points
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <param name="source">The source.</param>
/// <param name="color">The color.</param>
/// <param name="points">The points.</param>
/// <returns>The Image</returns>
public static Image<TColor, TPacked> FillPolygon<TColor, TPacked>(this Image<TColor, TPacked> source, TColor color, Point[] points)
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
// using Polygon directly instead of LinearPolygon as its will have less indirection
return source.Fill(new SolidBrush<TColor, TPacked>(color), new Polygon(new LinearLineSegment(points)));
}
}
}

128
src/ImageSharp/Drawing/Paths/BezierLineSegment.cs

@ -0,0 +1,128 @@
// <copyright file="BezierLineSegment.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Paths
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Brushes;
/// <summary>
/// Represents a line segment that conistst of control points that will be rendered as a cubic bezier curve
/// </summary>
/// <seealso cref="ImageSharp.Drawing.Paths.ILineSegment" />
public class BezierLineSegment : ILineSegment
{
// code for this taken from http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/
private const int SegmentsPerCurve = 50;
private List<Vector2> linePoints;
private int curveCount; // how many bezier curves in this path?
/// <summary>
/// Initializes a new instance of the <see cref="BezierLineSegment" /> class.
/// </summary>
/// <param name="points">The points.</param>
public BezierLineSegment(IEnumerable<PointF> points)
: this(points?.Select(x => x.ToVector2()).ToArray())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BezierLineSegment"/> class.
/// </summary>
/// <param name="points">The points.</param>
public BezierLineSegment(IEnumerable<Point> points)
: this(points?.Select(x => x.ToVector2()).ToArray())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BezierLineSegment"/> class.
/// </summary>
/// <param name="points">The points.</param>
public BezierLineSegment(params PointF[] points)
: this(points?.Select(x => x.ToVector2()).ToArray())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BezierLineSegment"/> class.
/// </summary>
/// <param name="points">The points.</param>
internal BezierLineSegment(Vector2[] points)
{
Guard.NotNull(points, nameof(points));
Guard.MustBeGreaterThanOrEqualTo(points.Length, 4, nameof(points));
this.curveCount = (points.Length - 1) / 3;
this.linePoints = this.GetDrawingPoints(points);
}
/// <summary>
/// Returns the current <see cref="ILineSegment" /> a simple linear path.
/// </summary>
/// <returns>
/// Returns the current <see cref="ILineSegment" /> as simple linear path.
/// </returns>
public IEnumerable<Vector2> AsSimpleLinearPath()
{
return this.linePoints;
}
private List<Vector2> GetDrawingPoints(Vector2[] controlPoints)
{
// TODO we need to calculate an optimal SegmentsPerCurve value
// depending on the calcualted length of this curve
var maxPoints = (int)Math.Ceiling(SegmentsPerCurve * (float)this.curveCount);
List<Vector2> drawingPoints = new List<Vector2>(maxPoints); // set a default size to be efficient?
var targetPoint = controlPoints.Length - 3;
for (int i = 0; i < targetPoint; i += 3)
{
Vector2 p0 = controlPoints[i];
Vector2 p1 = controlPoints[i + 1];
Vector2 p2 = controlPoints[i + 2];
Vector2 p3 = controlPoints[i + 3];
// only do this for the first end point. When i != 0, this coincides with the end point of the previous segment,
if (i == 0)
{
drawingPoints.Add(this.CalculateBezierPoint(0, p0, p1, p2, p3));
}
for (int j = 1; j <= SegmentsPerCurve; j++)
{
float t = j / (float)SegmentsPerCurve;
drawingPoints.Add(this.CalculateBezierPoint(t, p0, p1, p2, p3));
}
}
return drawingPoints;
}
private Vector2 CalculateBezierPoint(float t, Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
Vector2 p = uuu * p0; // first term
p += 3 * uu * t * p1; // second term
p += 3 * u * tt * p2; // third term
p += ttt * p3; // fourth term
return p;
}
}
}

25
src/ImageSharp/Drawing/Paths/ILineSegment.cs

@ -0,0 +1,25 @@
// <copyright file="ILineSegment.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Paths
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// Represents a simple path segment
/// </summary>
public interface ILineSegment
{
/// <summary>
/// Converts the <see cref="ILineSegment" /> into a simple linear path..
/// </summary>
/// <returns>Returns the current <see cref="ILineSegment" /> as simple linear path.</returns>
IEnumerable<Vector2> AsSimpleLinearPath();
}
}

51
src/ImageSharp/Drawing/Paths/IPath.cs

@ -0,0 +1,51 @@
// <copyright file="IPath.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Paths
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// Represents a logic path that can be drawn
/// </summary>
public interface IPath : ILineSegment
{
/// <summary>
/// Gets the bounds enclosing the path
/// </summary>
/// <value>
/// The bounds.
/// </value>
RectangleF Bounds { get; }
/// <summary>
/// Gets a value indicating whether this instance is closed.
/// </summary>
/// <value>
/// <c>true</c> if this instance is closed; otherwise, <c>false</c>.
/// </value>
bool IsClosed { get; }
/// <summary>
/// Gets the length of the path
/// </summary>
/// <value>
/// The length.
/// </value>
float Length { get; }
/// <summary>
/// Calcualtes the distance along and away from the path for a specified point.
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>Returns details about the point and its distance away from the path.</returns>
PointInfo Distance(int x, int y);
}
}

279
src/ImageSharp/Drawing/Paths/InternalPath.cs

@ -0,0 +1,279 @@
// <copyright file="InternalPath.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Paths
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// Internal logic for interigating linear paths.
/// </summary>
internal class InternalPath
{
private readonly Vector2[] points;
private readonly bool closedPath;
private readonly Lazy<float> totalDistance;
private float[] constant;
private float[] multiple;
private float[] distance;
private object locker = new object();
private bool calculated = false;
/// <summary>
/// Initializes a new instance of the <see cref="InternalPath"/> class.
/// </summary>
/// <param name="segments">The segments.</param>
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
internal InternalPath(IEnumerable<ILineSegment> segments, bool isClosedPath)
{
Guard.NotNull(segments, nameof(segments));
this.points = this.Simplify(segments);
this.closedPath = isClosedPath;
var minX = this.points.Min(x => x.X);
var maxX = this.points.Max(x => x.X);
var minY = this.points.Min(x => x.Y);
var maxY = this.points.Max(x => x.Y);
this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY);
this.totalDistance = new Lazy<float>(this.CalculateLength);
}
/// <summary>
/// Gets the bounds.
/// </summary>
/// <value>
/// The bounds.
/// </value>
public RectangleF Bounds
{
get;
}
/// <summary>
/// Gets the length.
/// </summary>
/// <value>
/// The length.
/// </value>
public float Length => this.totalDistance.Value;
/// <summary>
/// Gets the points.
/// </summary>
/// <value>
/// The points.
/// </value>
internal Vector2[] Points => this.points;
/// <summary>
/// Calculates the distance from the path.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>Returns the distance from the path</returns>
public PointInfo DistanceFromPath(Vector2 point)
{
this.CalculateConstants();
var internalInfo = default(PointInfoInternal);
internalInfo.DistanceSquared = float.MaxValue; // set it to max so that CalculateShorterDistance can reduce it back down
var polyCorners = this.points.Length;
if (!this.closedPath)
{
polyCorners -= 1;
}
int closestPoint = 0;
for (var i = 0; i < polyCorners; i++)
{
var next = i + 1;
if (this.closedPath && next == polyCorners)
{
next = 0;
}
if (this.CalculateShorterDistance(this.points[i], this.points[next], point, ref internalInfo))
{
closestPoint = i;
}
}
return new PointInfo
{
DistanceAlongPath = this.distance[closestPoint] + Vector2.Distance(this.points[closestPoint], point),
DistanceFromPath = (float)Math.Sqrt(internalInfo.DistanceSquared),
SearchPoint = point,
ClosestPointOnPath = internalInfo.PointOnLine
};
}
/// <summary>
/// Points the in polygon.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>Returns true if the point is inside the closed path.</returns>
public bool PointInPolygon(Vector2 point)
{
// you can only be inside a path if its "closed"
if (!this.closedPath)
{
return false;
}
if (!this.Bounds.Contains(point.X, point.Y))
{
return false;
}
this.CalculateConstants();
var poly = this.points;
var polyCorners = poly.Length;
var j = polyCorners - 1;
bool oddNodes = false;
for (var i = 0; i < polyCorners; i++)
{
if ((poly[i].Y < point.Y && poly[j].Y >= point.Y)
|| (poly[j].Y < point.Y && poly[i].Y >= point.Y))
{
oddNodes ^= (point.Y * this.multiple[i]) + this.constant[i] < point.X;
}
j = i;
}
return oddNodes;
}
private Vector2[] Simplify(IEnumerable<ILineSegment> segments)
{
return segments.SelectMany(x => x.AsSimpleLinearPath()).ToArray();
}
private float CalculateLength()
{
float length = 0;
var polyCorners = this.points.Length;
if (!this.closedPath)
{
polyCorners -= 1;
}
for (var i = 0; i < polyCorners; i++)
{
var next = i + 1;
if (this.closedPath && next == polyCorners)
{
next = 0;
}
length += Vector2.Distance(this.points[i], this.points[next]);
}
return length;
}
private void CalculateConstants()
{
// http://alienryderflex.com/polygon/ source for point in polygon logic
if (this.calculated)
{
return;
}
lock (this.locker)
{
if (this.calculated)
{
return;
}
var poly = this.points;
var polyCorners = poly.Length;
this.constant = new float[polyCorners];
this.multiple = new float[polyCorners];
this.distance = new float[polyCorners];
int i, j = polyCorners - 1;
this.distance[0] = 0;
for (i = 0; i < polyCorners; i++)
{
this.distance[j] = this.distance[i] + Vector2.Distance(poly[i], poly[j]);
if (poly[j].Y == poly[i].Y)
{
this.constant[i] = poly[i].X;
this.multiple[i] = 0;
}
else
{
var subtracted = poly[j] - poly[i];
this.constant[i] = (poly[i].X - ((poly[i].Y * poly[j].X) / subtracted.Y)) + ((poly[i].Y * poly[i].X) / subtracted.Y);
this.multiple[i] = subtracted.X / subtracted.Y;
}
j = i;
}
this.calculated = true;
}
}
private bool CalculateShorterDistance(Vector2 start, Vector2 end, Vector2 point, ref PointInfoInternal info)
{
var diffEnds = end - start;
float lengthSquared = diffEnds.LengthSquared();
var diff = point - start;
var multiplied = diff * diffEnds;
var u = (multiplied.X + multiplied.Y) / lengthSquared;
if (u > 1)
{
u = 1;
}
else if (u < 0)
{
u = 0;
}
var multipliedByU = diffEnds * u;
var pointOnLine = start + multipliedByU;
var d = pointOnLine - point;
var dist = d.LengthSquared();
if (info.DistanceSquared > dist)
{
info.DistanceSquared = dist;
info.PointOnLine = pointOnLine;
return true;
}
return false;
}
private struct PointInfoInternal
{
public float DistanceSquared;
public Vector2 PointOnLine;
}
}
}

83
src/ImageSharp/Drawing/Paths/LinearLineSegment.cs

@ -0,0 +1,83 @@
// <copyright file="LinearLineSegment.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Paths
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// Represents a seriese of control points that will be joined by staight lines
/// </summary>
/// <seealso cref="ImageSharp.Drawing.Paths.ILineSegment" />
public class LinearLineSegment : ILineSegment
{
private Vector2[] points;
/// <summary>
/// Initializes a new instance of the <see cref="LinearLineSegment"/> class.
/// </summary>
/// <param name="points">The points.</param>
public LinearLineSegment(IEnumerable<PointF> points)
: this(points?.Select(x => x.ToVector2()).ToArray())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LinearLineSegment"/> class.
/// </summary>
/// <param name="points">The points.</param>
public LinearLineSegment(IEnumerable<Point> points)
: this(points?.Select(x => x.ToVector2()).ToArray())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LinearLineSegment"/> class.
/// </summary>
/// <param name="points">The points.</param>
public LinearLineSegment(params PointF[] points)
: this(points?.Select(x => x.ToVector2()).ToArray())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LinearLineSegment"/> class.
/// </summary>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
internal LinearLineSegment(Vector2 start, Vector2 end)
: this(new[] { start, end })
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LinearLineSegment"/> class.
/// </summary>
/// <param name="points">The points.</param>
internal LinearLineSegment(Vector2[] points)
{
Guard.NotNull(points, nameof(points));
Guard.MustBeGreaterThanOrEqualTo(points.Count(), 2, nameof(points));
this.points = points;
}
/// <summary>
/// Converts the <see cref="ILineSegment" /> into a simple linear path..
/// </summary>
/// <returns>
/// Returns the current <see cref="ILineSegment" /> as simple linear path.
/// </returns>
public IEnumerable<Vector2> AsSimpleLinearPath()
{
return this.points;
}
}
}

81
src/ImageSharp/Drawing/Paths/Path.cs

@ -0,0 +1,81 @@
// <copyright file="Path.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Paths
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// A aggragate of <see cref="ILineSegment"/>s making a single logical path
/// </summary>
/// <seealso cref="ImageSharp.Drawing.Paths.IPath" />
public class Path : IPath
{
private readonly InternalPath innerPath;
/// <summary>
/// Initializes a new instance of the <see cref="Path"/> class.
/// </summary>
/// <param name="segment">The segment.</param>
internal Path(params ILineSegment[] segment)
{
this.innerPath = new InternalPath(segment, false);
}
/// <summary>
/// Gets the bounds enclosing the path
/// </summary>
/// <value>
/// The bounds.
/// </value>
public RectangleF Bounds => this.innerPath.Bounds;
/// <summary>
/// Gets a value indicating whether this instance is closed.
/// </summary>
/// <value>
/// <c>true</c> if this instance is closed; otherwise, <c>false</c>.
/// </value>
public bool IsClosed => false;
/// <summary>
/// Gets the length of the path
/// </summary>
/// <value>
/// The length.
/// </value>
public float Length => this.innerPath.Length;
/// <summary>
/// Returns the current <see cref="ILineSegment" /> a simple linear path.
/// </summary>
/// <returns>
/// Returns the current <see cref="ILineSegment" /> as simple linear path.
/// </returns>
public IEnumerable<Vector2> AsSimpleLinearPath()
{
return this.innerPath.Points;
}
/// <summary>
/// Calcualtes the distance along and away from the path for a specified point.
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>
/// Returns details about the point and its distance away from the path.
/// </returns>
public PointInfo Distance(int x, int y)
{
return this.innerPath.DistanceFromPath(new Vector2(x, y));
}
}
}

39
src/ImageSharp/Drawing/Paths/PointInfo.cs

@ -0,0 +1,39 @@
// <copyright file="PointInfo.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Paths
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// Returns some meta data about the nearest point on a path from a vector
/// </summary>
public struct PointInfo
{
/// <summary>
/// The search point
/// </summary>
public Vector2 SearchPoint;
/// <summary>
/// The distance along path <see cref="ClosestPointOnPath"/> is away from the start of the path
/// </summary>
public float DistanceAlongPath;
/// <summary>
/// The distance <see cref="SearchPoint"/> is away from <see cref="ClosestPointOnPath"/>.
/// </summary>
public float DistanceFromPath;
/// <summary>
/// The closest point to <see cref="SearchPoint"/> that lies on the path.
/// </summary>
public Vector2 ClosestPointOnPath;
}
}

31
src/ImageSharp/Drawing/Pens/IPen.cs

@ -0,0 +1,31 @@
// <copyright file="IPen.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Pens
{
using System;
using Processors;
/// <summary>
/// interface preresenting a Pen
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
public interface IPen<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
/// <summary>
/// Creates the applicator for applying this pen to an Image
/// </summary>
/// <param name="region">The region the pen will be applied to.</param>
/// <returns>Returns a the applicator for the pen.</returns>
/// <remarks>
/// The <paramref name="region" /> when being applied to things like shapes would ussually be the
/// bounding box of the shape not necorserrally the shape of the whole image
/// </remarks>
IPenApplicator<TColor, TPacked> CreateApplicator(RectangleF region);
}
}

315
src/ImageSharp/Drawing/Pens/Pen.cs

@ -0,0 +1,315 @@
// <copyright file="Pen.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Pens
{
using System;
using System.Numerics;
using Brushes;
using Drawing.Processors;
using Paths;
using Processors;
/// <summary>
/// Represenets a <see cref="Pen{TColor, TPacked}"/> in the <see cref="Color"/> color space.
/// </summary>
public partial class Pen : Pen<Color, uint>
{
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
public Pen(Color color, float width)
: base(color, width)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
public Pen(IBrush<Color, uint> brush, float width)
: base(brush, width)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <param name="pattern">The pattern.</param>
public Pen(IBrush<Color, uint> brush, float width, float[] pattern)
: base(brush, width, pattern)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
/// <param name="pen">The pen.</param>
internal Pen(Pen<Color, uint> pen)
: base(pen)
{
}
}
/// <summary>
/// Provides a pen that can apply a pattern to a line with a set brush and thickness
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <remarks>
/// The pattern will be in to the form of new float[]{ 1f, 2f, 0.5f} this will be
/// converted into a pattern that is 3.5 times longer that the width with 3 sections
/// section 1 will be width long (making a square) and will be filled by the brush
/// section 2 will be width * 2 long and will be empty
/// section 3 will be width/2 long and will be filled
/// the the pattern will imidiatly repeat without gap.
/// </remarks>
public partial class Pen<TColor, TPacked> : IPen<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
private static readonly float[] EmptyPattern = new float[0];
private readonly float[] pattern;
/// <summary>
/// Initializes a new instance of the <see cref="Pen{TColor, TPacked}"/> class.
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <param name="pattern">The pattern.</param>
public Pen(TColor color, float width, float[] pattern)
: this(new SolidBrush<TColor, TPacked>(color), width, pattern)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Pen{TColor, TPacked}"/> class.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <param name="pattern">The pattern.</param>
public Pen(IBrush<TColor, TPacked> brush, float width, float[] pattern)
{
this.Brush = brush;
this.Width = width;
this.pattern = pattern;
}
/// <summary>
/// Initializes a new instance of the <see cref="Pen{TColor, TPacked}"/> class.
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
public Pen(TColor color, float width)
: this(new SolidBrush<TColor, TPacked>(color), width)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Pen{TColor, TPacked}"/> class.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
public Pen(IBrush<TColor, TPacked> brush, float width)
: this(brush, width, EmptyPattern)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Pen{TColor, TPacked}"/> class.
/// </summary>
/// <param name="pen">The pen.</param>
internal Pen(Pen<TColor, TPacked> pen)
: this(pen.Brush, pen.Width, pen.pattern)
{
}
/// <summary>
/// Gets the brush.
/// </summary>
/// <value>
/// The brush.
/// </value>
public IBrush<TColor, TPacked> Brush { get; }
/// <summary>
/// Gets the width.
/// </summary>
/// <value>
/// The width.
/// </value>
public float Width { get; }
/// <summary>
/// Creates the applicator for applying this pen to an Image
/// </summary>
/// <param name="region">The region the pen will be applied to.</param>
/// <returns>
/// Returns a the applicator for the pen.
/// </returns>
/// <remarks>
/// The <paramref name="region" /> when being applied to things like shapes would ussually be the
/// bounding box of the shape not necorserrally the shape of the whole image
/// </remarks>
public IPenApplicator<TColor, TPacked> CreateApplicator(RectangleF region)
{
if (this.pattern == null || this.pattern.Length < 2)
{
// if there is only one item in the pattern then 100% of it will
// be solid so use the quicker applicator
return new SolidPenApplicator(this.Brush, region, this.Width);
}
return new PatternPenApplicator(this.Brush, region, this.Width, this.pattern);
}
private class SolidPenApplicator : IPenApplicator<TColor, TPacked>
{
private readonly IBrushApplicator<TColor, TPacked> brush;
private readonly float halfWidth;
public SolidPenApplicator(IBrush<TColor, TPacked> brush, RectangleF region, float width)
{
this.brush = brush.CreateApplicator(region);
this.halfWidth = width / 2;
this.RequiredRegion = RectangleF.Outset(region, width);
}
public RectangleF RequiredRegion
{
get;
}
public void Dispose()
{
this.brush.Dispose();
}
public ColoredPointInfo<TColor, TPacked> GetColor(PointInfo info)
{
var result = default(ColoredPointInfo<TColor, TPacked>);
result.Color = this.brush.GetColor(info.SearchPoint);
if (info.DistanceFromPath < this.halfWidth)
{
// inside strip
result.DistanceFromElement = 0;
}
else
{
result.DistanceFromElement = info.DistanceFromPath - this.halfWidth;
}
return result;
}
}
private class PatternPenApplicator : IPenApplicator<TColor, TPacked>
{
private readonly IBrushApplicator<TColor, TPacked> brush;
private readonly float halfWidth;
private readonly float[] pattern;
private readonly float totalLength;
public PatternPenApplicator(IBrush<TColor, TPacked> brush, RectangleF region, float width, float[] pattern)
{
this.brush = brush.CreateApplicator(region);
this.halfWidth = width / 2;
this.totalLength = 0;
this.pattern = new float[pattern.Length + 1];
this.pattern[0] = 0;
for (var i = 0; i < pattern.Length; i++)
{
this.totalLength += pattern[i] * width;
this.pattern[i + 1] = this.totalLength;
}
this.RequiredRegion = RectangleF.Outset(region, width);
}
public RectangleF RequiredRegion
{
get;
}
public void Dispose()
{
this.brush.Dispose();
}
public ColoredPointInfo<TColor, TPacked> GetColor(PointInfo info)
{
var infoResult = default(ColoredPointInfo<TColor, TPacked>);
infoResult.DistanceFromElement = float.MaxValue; // is really outside the element
var length = info.DistanceAlongPath % this.totalLength;
// we can treat the DistanceAlongPath and DistanceFromPath as x,y coords for the pattern
// we need to calcualte the distance from the outside edge of the pattern
// and set them on the ColoredPointInfo<TColor, TPacked> along with the color.
infoResult.Color = this.brush.GetColor(info.SearchPoint);
float distanceWAway = 0;
if (info.DistanceFromPath < this.halfWidth)
{
// inside strip
distanceWAway = 0;
}
else
{
distanceWAway = info.DistanceFromPath - this.halfWidth;
}
for (var i = 0; i < this.pattern.Length - 1; i++)
{
var start = this.pattern[i];
var end = this.pattern[i + 1];
if (length >= start && length < end)
{
// in section
if (i % 2 == 0)
{
// solid part return the maxDistance
infoResult.DistanceFromElement = distanceWAway;
return infoResult;
}
else
{
// this is a none solid part
var distanceFromStart = length - start;
var distanceFromEnd = end - length;
var closestEdge = Math.Min(distanceFromStart, distanceFromEnd);
var distanceAcross = closestEdge;
if (distanceWAway > 0)
{
infoResult.DistanceFromElement = new Vector2(distanceAcross, distanceWAway).Length();
}
else
{
infoResult.DistanceFromElement = closestEdge;
}
return infoResult;
}
}
}
return infoResult;
}
}
}
}

208
src/ImageSharp/Drawing/Pens/Pens.cs

@ -0,0 +1,208 @@
// <copyright file="Pens.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Pens
{
/// <summary>
/// Common Pen styles
/// </summary>
public partial class Pens
{
/// <summary>
/// Create a solid pen with out any drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen Solid(Color color, float width)
=> new Pen(color, width);
/// <summary>
/// Create a solid pen with out any drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen Solid(IBrush<Color, uint> brush, float width)
=> new Pen(brush, width);
/// <summary>
/// Create a pen with a 'Dash' drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen Dash(Color color, float width)
=> new Pen(Pens<Color, uint>.Dash(color, width));
/// <summary>
/// Create a pen with a 'Dash' drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen Dash(IBrush<Color, uint> brush, float width)
=> new Pen(Pens<Color, uint>.Dash(brush, width));
/// <summary>
/// Create a pen with a 'Dot' drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen Dot(Color color, float width)
=> new Pen(Pens<Color, uint>.Dot(color, width));
/// <summary>
/// Create a pen with a 'Dot' drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen Dot(IBrush<Color, uint> brush, float width)
=> new Pen(Pens<Color, uint>.Dot(brush, width));
/// <summary>
/// Create a pen with a 'Dash Dot' drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen DashDot(Color color, float width)
=> new Pen(Pens<Color, uint>.DashDot(color, width));
/// <summary>
/// Create a pen with a 'Dash Dot' drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen DashDot(IBrush<Color, uint> brush, float width)
=> new Pen(Pens<Color, uint>.DashDot(brush, width));
/// <summary>
/// Create a pen with a 'Dash Dot Dot' drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen DashDotDot(Color color, float width)
=> new Pen(Pens<Color, uint>.DashDotDot(color, width));
/// <summary>
/// Create a pen with a 'Dash Dot Dot' drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen DashDotDot(IBrush<Color, uint> brush, float width)
=> new Pen(Pens<Color, uint>.DashDotDot(brush, width));
}
/// <summary>
/// Common Pen styles
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
public partial class Pens<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
private static readonly float[] DashDotPattern = new[] { 3f, 1f, 1f, 1f };
private static readonly float[] DashDotDotPattern = new[] { 3f, 1f, 1f, 1f, 1f, 1f };
private static readonly float[] DottedPattern = new[] { 1f, 1f };
private static readonly float[] DashedPattern = new[] { 3f, 1f };
/// <summary>
/// Create a solid pen with out any drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> Solid(TColor color, float width)
=> new Pen<TColor, TPacked>(color, width);
/// <summary>
/// Create a solid pen with out any drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> Solid(IBrush<TColor, TPacked> brush, float width)
=> new Pen<TColor, TPacked>(brush, width);
/// <summary>
/// Create a pen with a 'Dash' drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> Dash(TColor color, float width)
=> new Pen<TColor, TPacked>(color, width, DashedPattern);
/// <summary>
/// Create a pen with a 'Dash' drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> Dash(IBrush<TColor, TPacked> brush, float width)
=> new Pen<TColor, TPacked>(brush, width, DashedPattern);
/// <summary>
/// Create a pen with a 'Dot' drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> Dot(TColor color, float width)
=> new Pen<TColor, TPacked>(color, width, DottedPattern);
/// <summary>
/// Create a pen with a 'Dot' drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> Dot(IBrush<TColor, TPacked> brush, float width)
=> new Pen<TColor, TPacked>(brush, width, DottedPattern);
/// <summary>
/// Create a pen with a 'Dash Dot' drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> DashDot(TColor color, float width)
=> new Pen<TColor, TPacked>(color, width, DashDotPattern);
/// <summary>
/// Create a pen with a 'Dash Dot' drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> DashDot(IBrush<TColor, TPacked> brush, float width)
=> new Pen<TColor, TPacked>(brush, width, DashDotPattern);
/// <summary>
/// Create a pen with a 'Dash Dot Dot' drawing patterns
/// </summary>
/// <param name="color">The color.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> DashDotDot(TColor color, float width)
=> new Pen<TColor, TPacked>(color, width, DashDotDotPattern);
/// <summary>
/// Create a pen with a 'Dash Dot Dot' drawing patterns
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="width">The width.</param>
/// <returns>The Pen</returns>
public static Pen<TColor, TPacked> DashDotDot(IBrush<TColor, TPacked> brush, float width)
=> new Pen<TColor, TPacked>(brush, width, DashDotDotPattern);
}
}

27
src/ImageSharp/Drawing/Pens/Processors/ColoredPointInfo.cs

@ -0,0 +1,27 @@
// <copyright file="ColoredPointInfo.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Pens.Processors
{
/// <summary>
/// Returns details about how far awau from the inside of a shape and the color the pixel could be.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
public struct ColoredPointInfo<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
/// <summary>
/// The color
/// </summary>
public TColor Color;
/// <summary>
/// The distance from element
/// </summary>
public float DistanceFromElement;
}
}

35
src/ImageSharp/Drawing/Pens/Processors/IPenApplicator.cs

@ -0,0 +1,35 @@
// <copyright file="IPenApplicator.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Pens.Processors
{
using System;
using Paths;
/// <summary>
/// primitive that converts a <see cref="PointInfo"/> into a color and a distance away from the drawable part of the path.
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
public interface IPenApplicator<TColor, TPacked> : IDisposable
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
/// <summary>
/// Gets the required region.
/// </summary>
/// <value>
/// The required region.
/// </value>
RectangleF RequiredRegion { get; }
/// <summary>
/// Gets a <see cref="ColoredPointInfo{TColor, TPacked}" /> from a point represented by a <see cref="PointInfo" />.
/// </summary>
/// <param name="info">The information to extract color details about.</param>
/// <returns>Returns the color details and distance from a solid bit of the line.</returns>
ColoredPointInfo<TColor, TPacked> GetColor(PointInfo info);
}
}

160
src/ImageSharp/Drawing/Processors/DrawPathProcessor.cs

@ -0,0 +1,160 @@
// <copyright file="DrawPathProcessor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Processors
{
using System;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using ImageSharp.Processors;
using Paths;
using Pens;
using Pens.Processors;
using Shapes;
/// <summary>
/// Draws a path using the processor pipeline
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <seealso cref="ImageSharp.Processors.ImageFilteringProcessor{TColor, TPacked}" />
public class DrawPathProcessor<TColor, TPacked> : ImageFilteringProcessor<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
private const float AntialiasFactor = 1f;
private const int PaddingFactor = 1; // needs to been the same or greater than AntialiasFactor
private const float Epsilon = 0.001f;
private readonly IPen<TColor, TPacked> pen;
private readonly IPath[] paths;
private readonly RectangleF region;
/// <summary>
/// Initializes a new instance of the <see cref="DrawPathProcessor{TColor, TPacked}"/> class.
/// </summary>
/// <param name="pen">The pen.</param>
/// <param name="shape">The shape.</param>
public DrawPathProcessor(IPen<TColor, TPacked> pen, IShape shape)
: this(pen, shape.ToArray())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DrawPathProcessor{TColor, TPacked}"/> class.
/// </summary>
/// <param name="pen">The pen.</param>
/// <param name="paths">The paths.</param>
public DrawPathProcessor(IPen<TColor, TPacked> pen, params IPath[] paths)
{
this.paths = paths;
this.pen = pen;
if (paths.Length != 1)
{
var maxX = paths.Max(x => x.Bounds.Right);
var minX = paths.Min(x => x.Bounds.Left);
var maxY = paths.Max(x => x.Bounds.Bottom);
var minY = paths.Min(x => x.Bounds.Top);
this.region = new RectangleF(minX, minY, maxX - minX, maxY - minY);
}
else
{
this.region = paths[0].Bounds;
}
}
/// <inheritdoc/>
protected override void OnApply(ImageBase<TColor, TPacked> source, Rectangle sourceRectangle)
{
using (IPenApplicator<TColor, TPacked> applicator = this.pen.CreateApplicator(this.region))
{
var rect = RectangleF.Ceiling(applicator.RequiredRegion);
int polyStartY = rect.Y - PaddingFactor;
int polyEndY = rect.Bottom + PaddingFactor;
int startX = rect.X - PaddingFactor;
int endX = rect.Right + PaddingFactor;
int minX = Math.Max(sourceRectangle.Left, startX);
int maxX = Math.Min(sourceRectangle.Right, endX);
int minY = Math.Max(sourceRectangle.Top, polyStartY);
int maxY = Math.Min(sourceRectangle.Bottom, polyEndY);
// Align start/end positions.
minX = Math.Max(0, minX);
maxX = Math.Min(source.Width, maxX);
minY = Math.Max(0, minY);
maxY = Math.Min(source.Height, maxY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
polyStartY = 0;
}
using (PixelAccessor<TColor, TPacked> sourcePixels = source.Lock())
{
Parallel.For(
minY,
maxY,
this.ParallelOptions,
y =>
{
int offsetY = y - polyStartY;
for (int x = minX; x < maxX; x++)
{
int offsetX = x - startX;
var dist = this.paths.Select(p => p.Distance(offsetX, offsetY)).OrderBy(p => p.DistanceFromPath).First();
var color = applicator.GetColor(dist);
var opacity = this.Opacity(color.DistanceFromElement);
if (opacity > Epsilon)
{
int offsetColorX = x - minX;
Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
Vector4 sourceVector = color.Color.ToVector4();
var finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
finalColor.W = backgroundVector.W;
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[offsetX, offsetY] = packed;
}
}
});
}
}
}
private float Opacity(float distance)
{
if (distance <= 0)
{
return 1;
}
else if (distance < AntialiasFactor)
{
return 1 - (distance / AntialiasFactor);
}
return 0;
}
}
}

97
src/ImageSharp/Drawing/Processors/FillProcessor.cs

@ -0,0 +1,97 @@
// <copyright file="FillProcessor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Processors
{
using System;
using System.Numerics;
using System.Threading.Tasks;
using Drawing;
using ImageSharp.Processors;
/// <summary>
/// Using the bursh as a source of pixels colors blends the brush color with source.
/// </summary>
/// <typeparam name="TColor">The pixel format.</typeparam>
/// <typeparam name="TPacked">The packed format. <example>uint, long, float.</example></typeparam>
public class FillProcessor<TColor, TPacked> : ImageFilteringProcessor<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
private const float Epsilon = 0.001f;
private readonly IBrush<TColor, TPacked> brush;
/// <summary>
/// Initializes a new instance of the <see cref="FillProcessor{TColor, TPacked}"/> class.
/// </summary>
/// <param name="brush">The brush to source pixel colors from.</param>
public FillProcessor(IBrush<TColor, TPacked> brush)
{
this.brush = brush;
}
/// <inheritdoc/>
protected override void OnApply(ImageBase<TColor, TPacked> source, Rectangle sourceRectangle)
{
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
// we could possibly do some optermising by having knowledge about the individual brushes operate
// for example If brush is SolidBrush<TColor, TPacked> then we could just get the color upfront
// and skip using the IBrushApplicator<TColor, TPacked>?.
using (PixelAccessor<TColor, TPacked> sourcePixels = source.Lock())
using (IBrushApplicator<TColor, TPacked> applicator = this.brush.CreateApplicator(sourceRectangle))
{
Parallel.For(
minY,
maxY,
this.ParallelOptions,
y =>
{
int offsetY = y - startY;
Vector2 currentPoint = default(Vector2);
for (int x = minX; x < maxX; x++)
{
int offsetX = x - startX;
int offsetColorX = x - minX;
currentPoint.X = offsetX;
currentPoint.Y = offsetY;
Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
var finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, 1);
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[offsetX, offsetY] = packed;
}
});
}
}
}
}

129
src/ImageSharp/Drawing/Processors/FillShapeProcessor.cs

@ -0,0 +1,129 @@
// <copyright file="FillShapeProcessor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Processors
{
using System;
using System.Numerics;
using System.Threading.Tasks;
using Drawing;
using ImageSharp.Processors;
using Shapes;
/// <summary>
/// Usinf a brsuh and a shape fills shape with contents of brush the
/// </summary>
/// <typeparam name="TColor">The type of the color.</typeparam>
/// <typeparam name="TPacked">The type of the packed.</typeparam>
/// <seealso cref="ImageSharp.Processors.ImageFilteringProcessor{TColor, TPacked}" />
public class FillShapeProcessor<TColor, TPacked> : ImageFilteringProcessor<TColor, TPacked>
where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct
{
private const float Epsilon = 0.001f;
private const float AntialiasFactor = 1f;
private const int DrawPadding = 1;
private readonly IBrush<TColor, TPacked> fillColor;
private readonly IShape poly;
/// <summary>
/// Initializes a new instance of the <see cref="FillShapeProcessor{TColor, TPacked}"/> class.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="shape">The shape.</param>
public FillShapeProcessor(IBrush<TColor, TPacked> brush, IShape shape)
{
this.poly = shape;
this.fillColor = brush;
}
/// <inheritdoc/>
protected override void OnApply(ImageBase<TColor, TPacked> source, Rectangle sourceRectangle)
{
var rect = RectangleF.Ceiling(this.poly.Bounds); // rounds the points out away from the center
int polyStartY = rect.Y - DrawPadding;
int polyEndY = rect.Bottom + DrawPadding;
int startX = rect.X - DrawPadding;
int endX = rect.Right + DrawPadding;
int minX = Math.Max(sourceRectangle.Left, startX);
int maxX = Math.Min(sourceRectangle.Right, endX);
int minY = Math.Max(sourceRectangle.Top, polyStartY);
int maxY = Math.Min(sourceRectangle.Bottom, polyEndY);
// Align start/end positions.
minX = Math.Max(0, minX);
maxX = Math.Min(source.Width, maxX);
minY = Math.Max(0, minY);
maxY = Math.Min(source.Height, maxY);
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
polyStartY = 0;
}
using (PixelAccessor<TColor, TPacked> sourcePixels = source.Lock())
using (IBrushApplicator<TColor, TPacked> applicator = this.fillColor.CreateApplicator(rect))
{
Parallel.For(
minY,
maxY,
this.ParallelOptions,
y =>
{
int offsetY = y - polyStartY;
Vector2 currentPoint = default(Vector2);
for (int x = minX; x < maxX; x++)
{
int offsetX = x - startX;
currentPoint.X = offsetX;
currentPoint.Y = offsetY;
var dist = this.poly.Distance(offsetX, offsetY);
var opacity = this.Opacity(dist);
if (opacity > Epsilon)
{
int offsetColorX = x - minX;
Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4();
var finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
finalColor.W = backgroundVector.W;
TColor packed = default(TColor);
packed.PackFromVector4(finalColor);
sourcePixels[offsetX, offsetY] = packed;
}
}
});
}
}
private float Opacity(float distance)
{
if (distance <= 0)
{
return 1;
}
else if (distance < AntialiasFactor)
{
return 1 - (distance / AntialiasFactor);
}
return 0;
}
}
}

78
src/ImageSharp/Drawing/Shapes/BezierPolygon.cs

@ -0,0 +1,78 @@
// <copyright file="BezierPolygon.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Shapes
{
using System.Collections;
using System.Collections.Generic;
using Paths;
/// <summary>
/// Represents a polygon made up exclusivly of a single close cubic Bezier curve.
/// </summary>
public sealed class BezierPolygon : IShape
{
private Polygon innerPolygon;
/// <summary>
/// Initializes a new instance of the <see cref="BezierPolygon"/> class.
/// </summary>
/// <param name="points">The points.</param>
public BezierPolygon(params Point[] points)
{
this.innerPolygon = new Polygon(new BezierLineSegment(points));
}
/// <summary>
/// Initializes a new instance of the <see cref="BezierPolygon"/> class.
/// </summary>
/// <param name="points">The points.</param>
public BezierPolygon(params PointF[] points)
{
this.innerPolygon = new Polygon(new BezierLineSegment(points));
}
/// <summary>
/// Gets the bounding box of this shape.
/// </summary>
/// <value>
/// The bounds.
/// </value>
public RectangleF Bounds => this.innerPolygon.Bounds;
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>
/// The distance from the shape.
/// </returns>
public float Distance(int x, int y) => this.innerPolygon.Distance(x, y);
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<IPath> GetEnumerator()
{
return this.innerPolygon.GetEnumerator();
}
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator()
{
return this.innerPolygon.GetEnumerator();
}
}
}

4924
src/ImageSharp/Drawing/Shapes/Clipper.cs

File diff suppressed because it is too large

197
src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs

@ -0,0 +1,197 @@
// <copyright file="ComplexPolygon.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Shapes
{
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Paths;
/// <summary>
/// Represents a complex polygon made up of one or more outline
/// polygons and one or more holes to punch out of them.
/// </summary>
/// <seealso cref="ImageSharp.Drawing.Shapes.IShape" />
public sealed class ComplexPolygon : IShape
{
private const float ClipperScaleFactor = 100f;
private IEnumerable<IShape> holes;
private IEnumerable<IShape> outlines;
private IEnumerable<IPath> paths;
/// <summary>
/// Initializes a new instance of the <see cref="ComplexPolygon"/> class.
/// </summary>
/// <param name="outline">The outline.</param>
/// <param name="holes">The holes.</param>
public ComplexPolygon(IShape outline, params IShape[] holes)
: this(new[] { outline }, holes)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ComplexPolygon"/> class.
/// </summary>
/// <param name="outlines">The outlines.</param>
/// <param name="holes">The holes.</param>
public ComplexPolygon(IEnumerable<IShape> outlines, IEnumerable<IShape> holes)
{
Guard.NotNull(outlines, nameof(outlines));
Guard.MustBeGreaterThanOrEqualTo(outlines.Count(), 1, nameof(outlines));
this.FixAndSetShapes(outlines, holes);
var minX = outlines.Min(x => x.Bounds.Left);
var maxX = outlines.Max(x => x.Bounds.Right);
var minY = outlines.Min(x => x.Bounds.Top);
var maxY = outlines.Max(x => x.Bounds.Bottom);
this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY);
}
/// <summary>
/// Gets the bounding box of this shape.
/// </summary>
/// <value>
/// The bounds.
/// </value>
public RectangleF Bounds { get; }
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>
/// Returns the distance from thr shape to the point
/// </returns>
float IShape.Distance(int x, int y)
{
// get the outline we are closest to the center of
// by rights we should only be inside 1 outline
// othersie we will start returning the distanct to the nearest shape
var dist = this.outlines.Select(o => o.Distance(x, y)).OrderBy(p => p).First();
if (dist <= 0)
{
// inside poly
foreach (var hole in this.holes)
{
var distFromHole = hole.Distance(x, y);
// less than zero we are inside shape
if (distFromHole <= 0)
{
// invert distance
dist = distFromHole * -1;
break;
}
}
}
return dist;
}
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<IPath> GetEnumerator()
{
return this.paths.GetEnumerator();
}
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private void AddPoints(ClipperLib.Clipper clipper, IShape shape, ClipperLib.PolyType polyType)
{
foreach (var path in shape)
{
var points = path.AsSimpleLinearPath();
var clipperPoints = new List<ClipperLib.IntPoint>();
foreach (var point in points)
{
var p = point * ClipperScaleFactor;
clipperPoints.Add(new ClipperLib.IntPoint((long)p.X, (long)p.Y));
}
clipper.AddPath(
clipperPoints,
polyType,
path.IsClosed);
}
}
private void AddPoints(ClipperLib.Clipper clipper, IEnumerable<IShape> shapes, ClipperLib.PolyType polyType)
{
foreach (var shape in shapes)
{
this.AddPoints(clipper, shape, polyType);
}
}
private void ExtractOutlines(ClipperLib.PolyNode tree, List<Polygon> outlines, List<Polygon> holes)
{
if (tree.Contour.Any())
{
// convert the Clipper Contour from scaled ints back down to the origional size (this is going to be lossy but not significantly)
var polygon = new Polygon(new LinearLineSegment(tree.Contour.Select(x => new Vector2(x.X / ClipperScaleFactor, x.Y / ClipperScaleFactor)).ToArray()));
if (tree.IsHole)
{
holes.Add(polygon);
}
else
{
outlines.Add(polygon);
}
}
foreach (var c in tree.Childs)
{
this.ExtractOutlines(c, outlines, holes);
}
}
private void FixAndSetShapes(IEnumerable<IShape> outlines, IEnumerable<IShape> holes)
{
var clipper = new ClipperLib.Clipper();
// add the outlines and the holes to clipper, scaling up from the float source to the int based system clipper uses
this.AddPoints(clipper, outlines, ClipperLib.PolyType.ptSubject);
this.AddPoints(clipper, holes, ClipperLib.PolyType.ptClip);
var tree = new ClipperLib.PolyTree();
clipper.Execute(ClipperLib.ClipType.ctDifference, tree);
List<Polygon> newOutlines = new List<Polygon>();
List<Polygon> newHoles = new List<Polygon>();
// convert the 'tree' back to paths
this.ExtractOutlines(tree, newOutlines, newHoles);
this.outlines = newOutlines;
this.holes = newHoles;
// extract the final list of paths out of the new polygons we just converted down to.
this.paths = newOutlines.Union(newHoles).ToArray();
}
}
}

33
src/ImageSharp/Drawing/Shapes/IShape.cs

@ -0,0 +1,33 @@
// <copyright file="IShape.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Shapes
{
using System.Collections.Generic;
using Paths;
/// <summary>
/// Represents a closed set of paths making up a single shape.
/// </summary>
public interface IShape : IEnumerable<IPath>
{
/// <summary>
/// Gets the bounding box of this shape.
/// </summary>
/// <value>
/// The bounds.
/// </value>
RectangleF Bounds { get; }
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>Returns the distance from the shape to the point</returns>
float Distance(int x, int y);
}
}

72
src/ImageSharp/Drawing/Shapes/LinearPolygon.cs

@ -0,0 +1,72 @@
// <copyright file="LinearPolygon.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Shapes
{
using System.Collections;
using System.Collections.Generic;
using Paths;
/// <summary>
/// Represents a polygon made up exclusivly of a single Linear path.
/// </summary>
public sealed class LinearPolygon : IShape
{
private Polygon innerPolygon;
/// <summary>
/// Initializes a new instance of the <see cref="LinearPolygon"/> class.
/// </summary>
/// <param name="points">The points.</param>
public LinearPolygon(params Point[] points)
{
this.innerPolygon = new Polygon(new LinearLineSegment(points));
}
/// <summary>
/// Initializes a new instance of the <see cref="LinearPolygon"/> class.
/// </summary>
/// <param name="points">The points.</param>
public LinearPolygon(params PointF[] points)
{
this.innerPolygon = new Polygon(new LinearLineSegment(points));
}
/// <summary>
/// Gets the bounding box of this shape.
/// </summary>
/// <value>
/// The bounds.
/// </value>
public RectangleF Bounds => this.innerPolygon.Bounds;
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>
/// Returns the distance from the shape to the point
/// </returns>
public float Distance(int x, int y) => this.innerPolygon.Distance(x, y);
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<IPath> GetEnumerator() => this.innerPolygon.GetEnumerator();
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator() => this.innerPolygon.GetEnumerator();
}
}

134
src/ImageSharp/Drawing/Shapes/Polygon.cs

@ -0,0 +1,134 @@
// <copyright file="Polygon.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Drawing.Shapes
{
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using Paths;
/// <summary>
/// A shape made up of a single path made up of one of more <see cref="ILineSegment"/>s
/// </summary>
public sealed class Polygon : IShape, IPath
{
private readonly InternalPath innerPath;
private readonly IEnumerable<IPath> pathCollection;
/// <summary>
/// Initializes a new instance of the <see cref="Polygon"/> class.
/// </summary>
/// <param name="segments">The segments.</param>
public Polygon(params ILineSegment[] segments)
: this((IEnumerable<ILineSegment>)segments)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Polygon"/> class.
/// </summary>
/// <param name="segments">The segments.</param>
public Polygon(IEnumerable<ILineSegment> segments)
{
this.innerPath = new InternalPath(segments, true);
this.pathCollection = new[] { this };
}
/// <summary>
/// Gets the bounding box of this shape.
/// </summary>
/// <value>
/// The bounds.
/// </value>
public RectangleF Bounds => this.innerPath.Bounds;
/// <summary>
/// Gets the length of the path
/// </summary>
/// <value>
/// The length.
/// </value>
public float Length => this.innerPath.Length;
/// <summary>
/// Gets a value indicating whether this instance is closed.
/// </summary>
/// <value>
/// <c>true</c> if this instance is closed; otherwise, <c>false</c>.
/// </value>
public bool IsClosed => true;
/// <summary>
/// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>
/// The distance of the point away from the shape
/// </returns>
public float Distance(int x, int y)
{
var point = new Vector2(x, y);
bool isInside = this.innerPath.PointInPolygon(point);
var distance = this.innerPath.DistanceFromPath(point).DistanceFromPath;
if (isInside)
{
return -distance;
}
return distance;
}
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<IPath> GetEnumerator()
{
return this.pathCollection.GetEnumerator();
}
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator()
{
return this.pathCollection.GetEnumerator();
}
/// <summary>
/// Calcualtes the distance along and away from the path for a specified point.
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>
/// distance metadata about the point.
/// </returns>
PointInfo IPath.Distance(int x, int y)
{
return this.innerPath.DistanceFromPath(new Vector2(x, y));
}
/// <summary>
/// Returns the current shape as a simple linear path.
/// </summary>
/// <returns>
/// Returns the current <see cref="ILineSegment" /> as simple linear path.
/// </returns>
public IEnumerable<Vector2> AsSimpleLinearPath()
{
return this.innerPath.Points;
}
}
}

287
src/ImageSharp/Numerics/PointF.cs

@ -0,0 +1,287 @@
// <copyright file="PointF.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp
{
using System;
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
/// <summary>
/// Represents an ordered pair of floating point x- and y-coordinates that defines a point in
/// a two-dimensional plane.
/// </summary>
/// <remarks>
/// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
/// as it avoids the need to create new values for modification operations.
/// </remarks>
public struct PointF : IEquatable<PointF>
{
/// <summary>
/// Represents a <see cref="Point"/> that has X and Y values set to zero.
/// </summary>
public static readonly PointF Empty = default(PointF);
private Vector2 backingVector;
/// <summary>
/// Initializes a new instance of the <see cref="PointF"/> struct.
/// </summary>
/// <param name="x">The horizontal position of the point.</param>
/// <param name="y">The vertical position of the point.</param>
public PointF(float x, float y)
: this(new Vector2(x, y))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PointF"/> struct.
/// </summary>
/// <param name="vector">
/// The vector representing the width and height.
/// </param>
public PointF(Vector2 vector)
{
this.backingVector = vector;
}
/// <summary>
/// Gets or sets the x-coordinate of this <see cref="PointF"/>.
/// </summary>
public float X
{
get { return this.backingVector.X; }
set { this.backingVector.X = value; }
}
/// <summary>
/// Gets or sets the y-coordinate of this <see cref="PointF"/>.
/// </summary>
public float Y
{
get { return this.backingVector.Y; }
set { this.backingVector.Y = value; }
}
/// <summary>
/// Gets a value indicating whether this <see cref="PointF"/> is empty.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty);
/// <summary>
/// Performs an implicit conversion from <see cref="Point"/> to <see cref="PointF"/>.
/// </summary>
/// <param name="d">The d.</param>
/// <returns>
/// The result of the conversion.
/// </returns>
public static implicit operator PointF(Point d)
{
return new PointF(d.ToVector2());
}
/// <summary>
/// Computes the sum of adding two points.
/// </summary>
/// <param name="left">The point on the left hand of the operand.</param>
/// <param name="right">The point on the right hand of the operand.</param>
/// <returns>
/// The <see cref="Point"/>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF operator +(PointF left, PointF right)
{
return new PointF(left.backingVector + right.backingVector);
}
/// <summary>
/// Computes the difference left by subtracting one point from another.
/// </summary>
/// <param name="left">The point on the left hand of the operand.</param>
/// <param name="right">The point on the right hand of the operand.</param>
/// <returns>
/// The <see cref="PointF"/>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF operator -(PointF left, PointF right)
{
return new PointF(left.backingVector - right.backingVector);
}
/// <summary>
/// Compares two <see cref="PointF"/> objects for equality.
/// </summary>
/// <param name="left">
/// The <see cref="PointF"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="PointF"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(PointF left, PointF right)
{
return left.Equals(right);
}
/// <summary>
/// Compares two <see cref="Point"/> objects for inequality.
/// </summary>
/// <param name="left">
/// The <see cref="Point"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="Point"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(PointF left, PointF right)
{
return !left.Equals(right);
}
/// <summary>
/// Creates a rotation matrix for the given point and angle.
/// </summary>
/// <param name="origin">The origin point to rotate around</param>
/// <param name="degrees">Rotation in degrees</param>
/// <returns>The rotation <see cref="Matrix3x2"/></returns>
public static Matrix3x2 CreateRotation(PointF origin, float degrees)
{
float radians = ImageMaths.DegreesToRadians(degrees);
return Matrix3x2.CreateRotation(radians, new Vector2(origin.X, origin.Y));
}
/// <summary>
/// Rotates a point around a given a rotation matrix.
/// </summary>
/// <param name="point">The point to rotate</param>
/// <param name="rotation">Rotation matrix used</param>
/// <returns>The rotated <see cref="Point"/></returns>
public static PointF Rotate(PointF point, Matrix3x2 rotation)
{
return new PointF(Vector2.Transform(new Vector2(point.X, point.Y), rotation));
}
/// <summary>
/// Rotates a point around a given origin by the specified angle in degrees.
/// </summary>
/// <param name="point">The point to rotate</param>
/// <param name="origin">The center point to rotate around.</param>
/// <param name="degrees">The angle in degrees.</param>
/// <returns>The rotated <see cref="Point"/></returns>
public static PointF Rotate(PointF point, PointF origin, float degrees)
{
return new PointF(Vector2.Transform(new Vector2(point.X, point.Y), CreateRotation(origin, degrees)));
}
/// <summary>
/// Creates a skew matrix for the given point and angle.
/// </summary>
/// <param name="origin">The origin point to rotate around</param>
/// <param name="degreesX">The x-angle in degrees.</param>
/// <param name="degreesY">The y-angle in degrees.</param>
/// <returns>The rotation <see cref="Matrix3x2"/></returns>
public static Matrix3x2 CreateSkew(PointF origin, float degreesX, float degreesY)
{
float radiansX = ImageMaths.DegreesToRadians(degreesX);
float radiansY = ImageMaths.DegreesToRadians(degreesY);
return Matrix3x2.CreateSkew(radiansX, radiansY, origin.backingVector);
}
/// <summary>
/// Skews a point using a given a skew matrix.
/// </summary>
/// <param name="point">The point to rotate</param>
/// <param name="skew">Rotation matrix used</param>
/// <returns>The rotated <see cref="Point"/></returns>
public static PointF Skew(PointF point, Matrix3x2 skew)
{
return new PointF(Vector2.Transform(point.backingVector, skew));
}
/// <summary>
/// Skews a point around a given origin by the specified angles in degrees.
/// </summary>
/// <param name="point">The point to skew.</param>
/// <param name="origin">The center point to rotate around.</param>
/// <param name="degreesX">The x-angle in degrees.</param>
/// <param name="degreesY">The y-angle in degrees.</param>
/// <returns>The skewed <see cref="Point"/></returns>
public static PointF Skew(PointF point, PointF origin, float degreesX, float degreesY)
{
return new PointF(Vector2.Transform(point.backingVector, CreateSkew(origin, degreesX, degreesY)));
}
/// <summary>
/// Gets a <see cref="Vector2"/> representation for this <see cref="Point"/>.
/// </summary>
/// <returns>A <see cref="Vector2"/> representation for this object.</returns>
public Vector2 ToVector2()
{
// should this be a return of the mutable vector2 backing vector instead of a copy?
return new Vector2(this.X, this.Y);
}
/// <summary>
/// Translates this <see cref="Point"/> by the specified amount.
/// </summary>
/// <param name="dx">The amount to offset the x-coordinate.</param>
/// <param name="dy">The amount to offset the y-coordinate.</param>
public void Offset(float dx, float dy)
{
this.backingVector += new Vector2(dx, dy);
}
/// <summary>
/// Translates this <see cref="PointF"/> by the specified amount.
/// </summary>
/// <param name="p">The <see cref="PointF"/> used offset this <see cref="PointF"/>.</param>
public void Offset(PointF p)
{
this.backingVector += p.backingVector;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.backingVector.GetHashCode();
}
/// <inheritdoc/>
public override string ToString()
{
if (this.IsEmpty)
{
return "Point [ Empty ]";
}
return $"Point [ X={this.X}, Y={this.Y} ]";
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
if (obj is PointF)
{
return this.Equals((PointF)obj);
}
return false;
}
/// <inheritdoc/>
public bool Equals(PointF other)
{
return this.backingVector == other.backingVector;
}
}
}

13
src/ImageSharp/Numerics/Rectangle.cs

@ -244,6 +244,19 @@ namespace ImageSharp
&& y < this.Bottom;
}
/// <summary>
/// Determines if the specfied <see cref="Rectangle"/> intersects the rectangular region defined by
/// this <see cref="Rectangle"/>.
/// </summary>
/// <param name="rect">The other Rectange </param>
/// <returns>The <see cref="bool"/></returns>
public bool Intersects(Rectangle rect)
{
return rect.Left <= this.Right && rect.Right >= this.Left
&&
rect.Top <= this.Bottom && rect.Bottom >= this.Top;
}
/// <inheritdoc/>
public override int GetHashCode()
{

334
src/ImageSharp/Numerics/RectangleF.cs

@ -0,0 +1,334 @@
// <copyright file="RectangleF.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp
{
using System;
using System.ComponentModel;
using System.Numerics;
/// <summary>
/// Stores a set of four integers that represent the location and size of a rectangle.
/// </summary>
/// <remarks>
/// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
/// as it avoids the need to create new values for modification operations.
/// </remarks>
public struct RectangleF : IEquatable<RectangleF>
{
/// <summary>
/// Represents a <see cref="Rectangle"/> that has X, Y, Width, and Height values set to zero.
/// </summary>
public static readonly RectangleF Empty = default(RectangleF);
/// <summary>
/// The backing vector for SIMD support.
/// </summary>
private Vector4 backingVector;
/// <summary>
/// Initializes a new instance of the <see cref="RectangleF"/> struct.
/// </summary>
/// <param name="x">The horizontal position of the rectangle.</param>
/// <param name="y">The vertical position of the rectangle.</param>
/// <param name="width">The width of the rectangle.</param>
/// <param name="height">The height of the rectangle.</param>
public RectangleF(float x, float y, float width, float height)
{
this.backingVector = new Vector4(x, y, width, height);
}
/// <summary>
/// Initializes a new instance of the <see cref="RectangleF"/> struct.
/// </summary>
/// <param name="vector">The vector.</param>
public RectangleF(Vector4 vector)
{
this.backingVector = vector;
}
/// <summary>
/// Gets or sets the x-coordinate of this <see cref="RectangleF"/>.
/// </summary>
public float X
{
get
{
return this.backingVector.X;
}
set
{
this.backingVector.X = value;
}
}
/// <summary>
/// Gets or sets the y-coordinate of this <see cref="RectangleF"/>.
/// </summary>
public float Y
{
get
{
return this.backingVector.Y;
}
set
{
this.backingVector.Y = value;
}
}
/// <summary>
/// Gets or sets the width of this <see cref="RectangleF"/>.
/// </summary>
public float Width
{
get
{
return this.backingVector.Z;
}
set
{
this.backingVector.Z = value;
}
}
/// <summary>
/// Gets or sets the height of this <see cref="RectangleF"/>.
/// </summary>
public float Height
{
get
{
return this.backingVector.W;
}
set
{
this.backingVector.W = value;
}
}
/// <summary>
/// Gets a value indicating whether this <see cref="RectangleF"/> is empty.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty);
/// <summary>
/// Gets the y-coordinate of the top edge of this <see cref="RectangleF"/>.
/// </summary>
public float Top => this.Y;
/// <summary>
/// Gets the x-coordinate of the right edge of this <see cref="RectangleF"/>.
/// </summary>
public float Right => this.X + this.Width;
/// <summary>
/// Gets the y-coordinate of the bottom edge of this <see cref="RectangleF"/>.
/// </summary>
public float Bottom => this.Y + this.Height;
/// <summary>
/// Gets the x-coordinate of the left edge of this <see cref="RectangleF"/>.
/// </summary>
public float Left => this.X;
/// <summary>
/// Performs an implicit conversion from <see cref="Rectangle"/> to <see cref="RectangleF"/>.
/// </summary>
/// <param name="d">The d.</param>
/// <returns>
/// The result of the conversion.
/// </returns>
public static implicit operator RectangleF(Rectangle d)
{
return new RectangleF(d.Left, d.Top, d.Width, d.Height);
}
/// <summary>
/// Computes the sum of adding two rectangles.
/// </summary>
/// <param name="left">The rectangle on the left hand of the operand.</param>
/// <param name="right">The rectangle on the right hand of the operand.</param>
/// <returns>
/// The <see cref="RectangleF"/>
/// </returns>
public static RectangleF operator +(RectangleF left, RectangleF right)
{
return new RectangleF(left.backingVector + right.backingVector);
}
/// <summary>
/// Computes the difference left by subtracting one rectangle from another.
/// </summary>
/// <param name="left">The rectangle on the left hand of the operand.</param>
/// <param name="right">The rectangle on the right hand of the operand.</param>
/// <returns>
/// The <see cref="RectangleF"/>
/// </returns>
public static RectangleF operator -(RectangleF left, RectangleF right)
{
return new RectangleF(left.backingVector - right.backingVector);
}
/// <summary>
/// Compares two <see cref="RectangleF"/> objects for equality.
/// </summary>
/// <param name="left">
/// The <see cref="RectangleF"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="RectangleF"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator ==(RectangleF left, RectangleF right)
{
return left.Equals(right);
}
/// <summary>
/// Compares two <see cref="RectangleF"/> objects for inequality.
/// </summary>
/// <param name="left">
/// The <see cref="RectangleF"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="RectangleF"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator !=(RectangleF left, RectangleF right)
{
return !left.Equals(right);
}
/// <summary>
/// Returns the center point of the given <see cref="RectangleF"/>
/// </summary>
/// <param name="rectangle">The rectangle</param>
/// <returns><see cref="Point"/></returns>
public static Vector2 Center(RectangleF rectangle)
{
return new Vector2(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2));
}
/// <summary>
/// Rounds the points away from the center this into a <see cref="Rectangle"/>
/// by rounding the dimensions to the nerent integer ensuring that the new rectangle is
/// never smaller than the source <see cref="RectangleF"/>
/// </summary>
/// <param name="source">The source area to round out</param>
/// <returns>
/// The smallest <see cref="Rectangle"/> that the <see cref="RectangleF"/> will fit inside.
/// </returns>
public static Rectangle Ceiling(RectangleF source)
{
int y = (int)Math.Floor(source.Y);
int width = (int)Math.Ceiling(source.Width);
int x = (int)Math.Floor(source.X);
int height = (int)Math.Ceiling(source.Height);
return new Rectangle(x, y, width, height);
}
/// <summary>
/// Outsets the specified region.
/// </summary>
/// <param name="region">The region.</param>
/// <param name="width">The width.</param>
/// <returns>
/// The <see cref="RectangleF"/> with all dimensions move away from the center by the offset.
/// </returns>
public static RectangleF Outset(RectangleF region, float width)
{
var dblWidth = width * 2;
return new RectangleF(region.X - width, region.Y - width, region.Width + dblWidth, region.Height + dblWidth);
}
/// <summary>
/// Determines if the specfied point is contained within the rectangular region defined by
/// this <see cref="RectangleF"/>.
/// </summary>
/// <param name="x">The x-coordinate of the given point.</param>
/// <param name="y">The y-coordinate of the given point.</param>
/// <returns>The <see cref="bool"/></returns>
public bool Contains(float x, float y)
{
// TODO: SIMD?
return this.X <= x
&& x < this.Right
&& this.Y <= y
&& y < this.Bottom;
}
/// <summary>
/// Determines if the specfied <see cref="Rectangle"/> intersects the rectangular region defined by
/// this <see cref="Rectangle"/>.
/// </summary>
/// <param name="rect">The other Rectange </param>
/// <returns>The <see cref="bool"/></returns>
public bool Intersects(RectangleF rect)
{
return rect.Left <= this.Right && rect.Right >= this.Left
&&
rect.Top <= this.Bottom && rect.Bottom >= this.Top;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.GetHashCode(this);
}
/// <inheritdoc/>
public override string ToString()
{
if (this.IsEmpty)
{
return "Rectangle [ Empty ]";
}
return
$"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]";
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
if (obj is RectangleF)
{
return this.Equals((RectangleF)obj);
}
return false;
}
/// <inheritdoc/>
public bool Equals(RectangleF other)
{
return this.backingVector.Equals(other.backingVector);
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <param name="rectangle">
/// The instance of <see cref="RectangleF"/> to return the hash code for.
/// </param>
/// <returns>
/// A 32-bit signed integer that is the hash code for this instance.
/// </returns>
private int GetHashCode(RectangleF rectangle)
{
return rectangle.backingVector.GetHashCode();
}
}
}

64
tests/ImageSharp.Benchmarks/Drawing/DrawBeziers.cs

@ -0,0 +1,64 @@
// <copyright file="Crop.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Benchmarks
{
using System.Drawing;
using System.Drawing.Drawing2D;
using BenchmarkDotNet.Attributes;
using CoreImage = ImageSharp.Image;
using CorePoint = ImageSharp.Point;
using CorePointF= ImageSharp.PointF;
using CoreColor= ImageSharp.Color;
using System.IO;
public class DrawBeziers
{
[Benchmark(Baseline = true, Description = "System.Drawing Draw Beziers")]
public void DrawPathSystemDrawing()
{
using (Bitmap destination = new Bitmap(800, 800))
{
using (Graphics graphics = Graphics.FromImage(destination))
{
graphics.InterpolationMode = InterpolationMode.Default;
graphics.SmoothingMode = SmoothingMode.AntiAlias;
var pen = new Pen(Color.HotPink, 10);
graphics.DrawBeziers(pen, new[] {
new PointF(10, 500),
new PointF(30, 10),
new PointF(240, 30),
new PointF(300, 500)
});
}
using (MemoryStream ms = new MemoryStream())
{
destination.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
}
}
}
[Benchmark(Description = "ImageSharp Draw Beziers")]
public void DrawLinesCore()
{
CoreImage image = new CoreImage(800, 800);
image.DrawBeziers(CoreColor.HotPink, 10, new[] {
new CorePointF(10, 500),
new CorePointF(30, 10),
new CorePointF(240, 30),
new CorePointF(300, 500)
});
using (MemoryStream ms = new MemoryStream())
{
image.SaveAsBmp(ms);
}
}
}
}

62
tests/ImageSharp.Benchmarks/Drawing/DrawLines.cs

@ -0,0 +1,62 @@
// <copyright file="Crop.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Benchmarks
{
using System.Drawing;
using System.Drawing.Drawing2D;
using BenchmarkDotNet.Attributes;
using CoreImage = ImageSharp.Image;
using CorePoint = ImageSharp.Point;
using CorePointF= ImageSharp.PointF;
using CoreColor= ImageSharp.Color;
using System.IO;
public class DrawLines
{
[Benchmark(Baseline = true, Description = "System.Drawing Draw Lines")]
public void DrawPathSystemDrawing()
{
using (Bitmap destination = new Bitmap(800, 800))
{
using (Graphics graphics = Graphics.FromImage(destination))
{
graphics.InterpolationMode = InterpolationMode.Default;
graphics.SmoothingMode = SmoothingMode.AntiAlias;
var pen = new Pen(Color.HotPink, 10);
graphics.DrawLines(pen, new[] {
new PointF(10, 10),
new PointF(550, 50),
new PointF(200, 400)
});
}
using (MemoryStream ms = new MemoryStream())
{
destination.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
}
}
}
[Benchmark(Description = "ImageSharp Draw Lines")]
public void DrawLinesCore()
{
CoreImage image = new CoreImage(800, 800);
image.DrawLines(CoreColor.HotPink, 10, new[] {
new CorePointF(10, 10),
new CorePointF(550, 50),
new CorePointF(200, 400)
});
using (MemoryStream ms = new MemoryStream())
{
image.SaveAsBmp(ms);
}
}
}
}

62
tests/ImageSharp.Benchmarks/Drawing/DrawPolygon.cs

@ -0,0 +1,62 @@
// <copyright file="Crop.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Benchmarks
{
using System.Drawing;
using System.Drawing.Drawing2D;
using BenchmarkDotNet.Attributes;
using CoreImage = ImageSharp.Image;
using CorePoint = ImageSharp.Point;
using CorePointF= ImageSharp.PointF;
using CoreColor= ImageSharp.Color;
using System.IO;
public class DrawPolygon
{
[Benchmark(Baseline = true, Description = "System.Drawing Draw Polygon")]
public void DrawPolygonSystemDrawing()
{
using (Bitmap destination = new Bitmap(800, 800))
{
using (Graphics graphics = Graphics.FromImage(destination))
{
graphics.InterpolationMode = InterpolationMode.Default;
graphics.SmoothingMode = SmoothingMode.AntiAlias;
var pen = new Pen(Color.HotPink, 10);
graphics.DrawPolygon(pen, new[] {
new PointF(10, 10),
new PointF(550, 50),
new PointF(200, 400)
});
}
using (MemoryStream ms = new MemoryStream())
{
destination.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
}
}
}
[Benchmark(Description = "ImageSharp Draw Polygon")]
public void DrawPolygonCore()
{
CoreImage image = new CoreImage(800, 800);
image.DrawPolygon(CoreColor.HotPink, 10, new[] {
new CorePointF(10, 10),
new CorePointF(550, 50),
new CorePointF(200, 400)
});
using (MemoryStream ms = new MemoryStream())
{
image.SaveAsBmp(ms);
}
}
}
}

59
tests/ImageSharp.Benchmarks/Drawing/FillPolygon.cs

@ -0,0 +1,59 @@
// <copyright file="Crop.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Benchmarks
{
using System.Drawing;
using System.Drawing.Drawing2D;
using BenchmarkDotNet.Attributes;
using CoreImage = ImageSharp.Image;
using CorePoint = ImageSharp.Point;
using CoreColor = ImageSharp.Color;
using System.IO;
public class FillPolygon
{
[Benchmark(Baseline = true, Description = "System.Drawing Fill Polygon")]
public void DrawSolidPolygonSystemDrawing()
{
using (Bitmap destination = new Bitmap(800, 800))
{
using (Graphics graphics = Graphics.FromImage(destination))
{
graphics.SmoothingMode = SmoothingMode.AntiAlias;
graphics.FillPolygon(Brushes.HotPink, new[] {
new Point(10, 10),
new Point(550, 50),
new Point(200, 400)
});
}
using (MemoryStream ms = new MemoryStream())
{
destination.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
}
}
}
[Benchmark(Description = "ImageSharp Fill Polygon")]
public void DrawSolidPolygonCore()
{
CoreImage image = new CoreImage(800, 800);
image.FillPolygon(CoreColor.HotPink,
new[] {
new CorePoint(10, 10),
new CorePoint(550, 50),
new CorePoint(200, 400)
}
);
using (MemoryStream ms = new MemoryStream())
{
image.SaveAsBmp(ms);
}
}
}
}

103
tests/ImageSharp.Tests/Drawing/BeziersTests.cs

@ -0,0 +1,103 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using Drawing;
using ImageSharp.Drawing;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using Xunit;
public class Beziers : FileTestBase
{
[Fact]
public void ImageShouldBeOverlayedByBezierLine()
{
string path = CreateOutputDirectory("Drawing","BezierLine");
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawBeziers(Color.HotPink, 5, new[] {
new PointF(10, 400),
new PointF(30, 10),
new PointF(240, 30),
new PointF(300, 400)
})
.Save(output);
}
using (var sourcePixels = image.Lock())
{
//top of curve
Assert.Equal(Color.HotPink, sourcePixels[138,115]);
//start points
Assert.Equal(Color.HotPink, sourcePixels[10, 400]);
Assert.Equal(Color.HotPink, sourcePixels[300, 400]);
//curve points should not be never be set
Assert.Equal(Color.Blue, sourcePixels[30, 10]);
Assert.Equal(Color.Blue, sourcePixels[240, 30]);
// inside shape should be empty
Assert.Equal(Color.Blue, sourcePixels[200, 250]);
}
}
[Fact]
public void ImageShouldBeOverlayedBezierLineWithOpacity()
{
string path = CreateOutputDirectory("Drawing", "BezierLine");
var color = new Color(Color.HotPink.R, Color.HotPink.G, Color.HotPink.B, 150);
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Opacity.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawBeziers(color, 10, new[] {
new PointF(10, 400),
new PointF(30, 10),
new PointF(240, 30),
new PointF(300, 400)
})
.Save(output);
}
//shift background color towards forground color by the opacity amount
var mergedColor = new Color(Vector4.Lerp(Color.Blue.ToVector4(), Color.HotPink.ToVector4(), 150f/255f));
using (var sourcePixels = image.Lock())
{
//top of curve
Assert.Equal(mergedColor, sourcePixels[138, 115]);
//start points
Assert.Equal(mergedColor, sourcePixels[10, 400]);
Assert.Equal(mergedColor, sourcePixels[300, 400]);
//curve points should not be never be set
Assert.Equal(Color.Blue, sourcePixels[30, 10]);
Assert.Equal(Color.Blue, sourcePixels[240, 30]);
// inside shape should be empty
Assert.Equal(Color.Blue, sourcePixels[200, 250]);
}
}
}
}

105
tests/ImageSharp.Tests/Drawing/DrawPathTests.cs

@ -0,0 +1,105 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using Drawing;
using ImageSharp.Drawing;
using CorePath = ImageSharp.Drawing.Paths.Path;
using ImageSharp.Drawing.Paths;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using Xunit;
public class DrawPathTests : FileTestBase
{
[Fact]
public void ImageShouldBeOverlayedByPath()
{
string path = CreateOutputDirectory("Drawing", "Path");
var image = new Image(500, 500);
var linerSegemnt = new LinearLineSegment(
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
);
var bazierSegment = new BezierLineSegment(new Point(50, 300),
new Point(500, 500),
new Point(60, 10),
new Point(10, 400));
var p = new CorePath(linerSegemnt, bazierSegment);
using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPath(Color.HotPink, 5, p)
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[9, 9]);
Assert.Equal(Color.HotPink, sourcePixels[199, 149]);
Assert.Equal(Color.Blue, sourcePixels[50, 50]);
}
}
[Fact]
public void ImageShouldBeOverlayedPathWithOpacity()
{
string path = CreateOutputDirectory("Drawing", "Path");
var color = new Color(Color.HotPink.R, Color.HotPink.G, Color.HotPink.B, 150);
var linerSegemnt = new LinearLineSegment(
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
);
var bazierSegment = new BezierLineSegment(new Point(50, 300),
new Point(500, 500),
new Point(60, 10),
new Point(10, 400));
var p = new CorePath(linerSegemnt, bazierSegment);
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Opacity.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPath(color, 10, p)
.Save(output);
}
//shift background color towards forground color by the opacity amount
var mergedColor = new Color(Vector4.Lerp(Color.Blue.ToVector4(), Color.HotPink.ToVector4(), 150f/255f));
using (var sourcePixels = image.Lock())
{
Assert.Equal(mergedColor, sourcePixels[9, 9]);
Assert.Equal(mergedColor, sourcePixels[199, 149]);
Assert.Equal(Color.Blue, sourcePixels[50, 50]);
}
}
}
}

235
tests/ImageSharp.Tests/Drawing/FillPatternTests.cs

@ -0,0 +1,235 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using Drawing;
using ImageSharp.Drawing;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using Xunit;
using ImageSharp.Drawing.Brushes;
public class FillPatternBrushTests: FileTestBase
{
private Image Test(string name, Color background, IBrush<Color, uint> brush, Color[,] expectedPattern)
{
string path = CreateOutputDirectory("Fill", "PatternBrush");
var image = new Image(20, 20);
image
.Fill(background)
.Fill(brush);
using (FileStream output = File.OpenWrite($"{path}/{name}.png"))
{
image.Save(output);
}
using (var sourcePixels = image.Lock())
{
// lets pick random spots to start checking
var r = new Random();
var xStride = expectedPattern.GetLength(1);
var yStride = expectedPattern.GetLength(0);
var offsetX = r.Next(image.Width / xStride) * xStride;
var offsetY = r.Next(image.Height / yStride) * yStride;
for (var x = 0; x < xStride; x++)
{
for (var y = 0; y < yStride; y++)
{
var actualX = x + offsetX;
var actualY = y + offsetY;
var expected = expectedPattern[y, x]; // inverted pattern
var actual = sourcePixels[actualX, actualY];
if (expected != actual)
{
Assert.True(false, $"Expected {expected} but found {actual} at ({actualX},{actualY})");
}
}
}
}
using (FileStream output = File.OpenWrite($"{path}/{name}x4.png"))
{
image.Resize(80,80).Save(output);
}
return image;
}
[Fact]
public void ImageShouldBeFloodFilledWithPercent10()
{
Test("Percent10", Color.Blue, Brushes.Percent10(Color.HotPink, Color.LimeGreen), new Color[,] {
{ Color.HotPink , Color.LimeGreen, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.LimeGreen, Color.HotPink , Color.LimeGreen},
{ Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithPercent10Transparent()
{
Test("Percent10_Transparent", Color.Blue, Brushes.Percent10(Color.HotPink),
new Color[,] {
{ Color.HotPink , Color.Blue, Color.Blue, Color.Blue},
{ Color.Blue, Color.Blue, Color.Blue, Color.Blue},
{ Color.Blue, Color.Blue, Color.HotPink , Color.Blue},
{ Color.Blue, Color.Blue, Color.Blue, Color.Blue}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithPercent20()
{
Test("Percent20", Color.Blue, Brushes.Percent20(Color.HotPink, Color.LimeGreen),
new Color[,] {
{ Color.HotPink , Color.LimeGreen, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.LimeGreen, Color.HotPink , Color.LimeGreen},
{ Color.HotPink , Color.LimeGreen, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.LimeGreen, Color.HotPink , Color.LimeGreen}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithPercent20_transparent()
{
Test("Percent20_Transparent", Color.Blue, Brushes.Percent20(Color.HotPink),
new Color[,] {
{ Color.HotPink , Color.Blue, Color.Blue, Color.Blue},
{ Color.Blue, Color.Blue, Color.HotPink , Color.Blue},
{ Color.HotPink , Color.Blue, Color.Blue, Color.Blue},
{ Color.Blue, Color.Blue, Color.HotPink , Color.Blue}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithHorizontal()
{
Test("Horizontal", Color.Blue, Brushes.Horizontal(Color.HotPink, Color.LimeGreen),
new Color[,] {
{ Color.LimeGreen , Color.LimeGreen, Color.LimeGreen, Color.LimeGreen},
{ Color.HotPink, Color.HotPink, Color.HotPink , Color.HotPink},
{ Color.LimeGreen , Color.LimeGreen, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.LimeGreen, Color.LimeGreen , Color.LimeGreen}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithHorizontal_transparent()
{
Test("Horizontal_Transparent", Color.Blue, Brushes.Horizontal(Color.HotPink),
new Color[,] {
{ Color.Blue , Color.Blue, Color.Blue, Color.Blue},
{ Color.HotPink, Color.HotPink, Color.HotPink , Color.HotPink},
{ Color.Blue , Color.Blue, Color.Blue, Color.Blue},
{ Color.Blue, Color.Blue, Color.Blue , Color.Blue}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithMin()
{
Test("Min", Color.Blue, Brushes.Min(Color.HotPink, Color.LimeGreen),
new Color[,] {
{ Color.LimeGreen , Color.LimeGreen, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen , Color.LimeGreen, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.LimeGreen, Color.LimeGreen , Color.LimeGreen},
{ Color.HotPink, Color.HotPink, Color.HotPink , Color.HotPink}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithMin_transparent()
{
Test("Min_Transparent", Color.Blue, Brushes.Min(Color.HotPink),
new Color[,] {
{ Color.Blue , Color.Blue, Color.Blue, Color.Blue},
{ Color.Blue , Color.Blue, Color.Blue, Color.Blue},
{ Color.Blue, Color.Blue, Color.Blue , Color.Blue},
{ Color.HotPink, Color.HotPink, Color.HotPink , Color.HotPink},
});
}
[Fact]
public void ImageShouldBeFloodFilledWithVertical()
{
Test("Vertical", Color.Blue, Brushes.Vertical(Color.HotPink, Color.LimeGreen),
new Color[,] {
{ Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithVertical_transparent()
{
Test("Vertical_Transparent", Color.Blue, Brushes.Vertical(Color.HotPink),
new Color[,] {
{ Color.Blue, Color.HotPink, Color.Blue, Color.Blue},
{ Color.Blue, Color.HotPink, Color.Blue, Color.Blue},
{ Color.Blue, Color.HotPink, Color.Blue, Color.Blue},
{ Color.Blue, Color.HotPink, Color.Blue, Color.Blue}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithForwardDiagonal()
{
Test("ForwardDiagonal", Color.Blue, Brushes.ForwardDiagonal(Color.HotPink, Color.LimeGreen),
new Color[,] {
{ Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen},
{ Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen},
{ Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent()
{
Test("ForwardDiagonal_Transparent", Color.Blue, Brushes.ForwardDiagonal(Color.HotPink),
new Color[,] {
{ Color.HotPink, Color.Blue, Color.Blue, Color.Blue},
{ Color.Blue, Color.HotPink, Color.Blue, Color.Blue},
{ Color.Blue, Color.Blue, Color.HotPink, Color.Blue},
{ Color.Blue, Color.Blue, Color.Blue, Color.HotPink}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithBackwardDiagonal()
{
Test("BackwardDiagonal", Color.Blue, Brushes.BackwardDiagonal(Color.HotPink, Color.LimeGreen),
new Color[,] {
{ Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink},
{ Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen},
{ Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen},
{ Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen}
});
}
[Fact]
public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent()
{
Test("BackwardDiagonal_Transparent", Color.Blue, Brushes.BackwardDiagonal(Color.HotPink),
new Color[,] {
{ Color.Blue, Color.Blue, Color.Blue, Color.HotPink},
{ Color.Blue, Color.Blue, Color.HotPink, Color.Blue},
{ Color.Blue, Color.HotPink, Color.Blue, Color.Blue},
{ Color.HotPink, Color.Blue, Color.Blue, Color.Blue}
});
}
}
}

89
tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs

@ -0,0 +1,89 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using Drawing;
using ImageSharp.Drawing;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using Xunit;
public class FillSolidBrushTests: FileTestBase
{
[Fact]
public void ImageShouldBeFloodFilledWithColorOnDefaultBackground()
{
string path = CreateOutputDirectory("Fill", "SolidBrush");
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/DefaultBack.png"))
{
image
.Fill(Color.HotPink)
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[9, 9]);
Assert.Equal(Color.HotPink, sourcePixels[199, 149]);
}
}
[Fact]
public void ImageShouldBeFloodFilledWithColor()
{
string path = CreateOutputDirectory("Fill", "SolidBrush");
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
image
.BackgroundColor(Color.Blue)
.Fill(Color.HotPink)
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[9, 9]);
Assert.Equal(Color.HotPink, sourcePixels[199, 149]);
}
}
[Fact]
public void ImageShouldBeFloodFilledWithColorOpacity()
{
string path = CreateOutputDirectory("Fill", "SolidBrush");
var image = new Image(500, 500);
var color = new Color(Color.HotPink.R, Color.HotPink.G, Color.HotPink.B, 150);
using (FileStream output = File.OpenWrite($"{path}/Opacity.png"))
{
image
.BackgroundColor(Color.Blue)
.Fill(color)
.Save(output);
}
//shift background color towards forground color by the opacity amount
var mergedColor = new Color(Vector4.Lerp(Color.Blue.ToVector4(), Color.HotPink.ToVector4(), 150f / 255f));
using (var sourcePixels = image.Lock())
{
Assert.Equal(mergedColor, sourcePixels[9, 9]);
Assert.Equal(mergedColor, sourcePixels[199, 149]);
}
}
}
}

195
tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs

@ -0,0 +1,195 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Xunit;
using Drawing;
using ImageSharp.Drawing;
using System.Numerics;
using ImageSharp.Drawing.Shapes;
using ImageSharp.Drawing.Pens;
public class LineComplexPolygonTests : FileTestBase
{
[Fact]
public void ImageShouldBeOverlayedByPolygonOutline()
{
string path = CreateOutputDirectory("Drawing", "LineComplexPolygon");
var simplePath = new LinearPolygon(
new Point(10, 10),
new Point(200, 150),
new Point(50, 300));
var hole1 = new LinearPolygon(
new Point(37, 85),
new Point(93, 85),
new Point(65, 137));
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPolygon(Color.HotPink, 5, new ComplexPolygon(simplePath, hole1))
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[10, 10]);
Assert.Equal(Color.HotPink, sourcePixels[200, 150]);
Assert.Equal(Color.HotPink, sourcePixels[50, 300]);
Assert.Equal(Color.HotPink, sourcePixels[37, 85]);
Assert.Equal(Color.HotPink, sourcePixels[93, 85]);
Assert.Equal(Color.HotPink, sourcePixels[65, 137]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole
Assert.Equal(Color.Blue, sourcePixels[57, 99]);
//inside shape
Assert.Equal(Color.Blue, sourcePixels[100, 192]);
}
}
[Fact]
public void ImageShouldBeOverlayedByPolygonOutlineOverlapping()
{
string path = CreateOutputDirectory("Drawing", "LineComplexPolygon");
var simplePath = new LinearPolygon(
new Point(10, 10),
new Point(200, 150),
new Point(50, 300));
var hole1 = new LinearPolygon(
new Point(37, 85),
new Point(130, 40),
new Point(65, 137));
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/SimpleOverlapping.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPolygon(Color.HotPink, 5, new ComplexPolygon(simplePath, hole1))
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[10, 10]);
Assert.Equal(Color.HotPink, sourcePixels[200, 150]);
Assert.Equal(Color.HotPink, sourcePixels[50, 300]);
Assert.Equal(Color.Blue, sourcePixels[130, 41]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole
Assert.Equal(Color.Blue, sourcePixels[57, 99]);
//inside shape
Assert.Equal(Color.Blue, sourcePixels[100, 192]);
}
}
[Fact]
public void ImageShouldBeOverlayedByPolygonOutlineDashed()
{
string path = CreateOutputDirectory("Drawing", "LineComplexPolygon");
var simplePath = new LinearPolygon(
new Point(10, 10),
new Point(200, 150),
new Point(50, 300));
var hole1 = new LinearPolygon(
new Point(37, 85),
new Point(93, 85),
new Point(65, 137));
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Dashed.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPolygon(Pens.Dash(Color.HotPink, 5), new ComplexPolygon(simplePath, hole1))
.Save(output);
}
}
[Fact]
public void ImageShouldBeOverlayedPolygonOutlineWithOpacity()
{
string path = CreateOutputDirectory("Drawing", "LineComplexPolygon");
var simplePath = new LinearPolygon(
new Point(10, 10),
new Point(200, 150),
new Point(50, 300));
var hole1 = new LinearPolygon(
new Point(37, 85),
new Point(93, 85),
new Point(65, 137));
var color = new Color(Color.HotPink.R, Color.HotPink.G, Color.HotPink.B, 150);
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Opacity.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPolygon(color, 5, new ComplexPolygon(simplePath, hole1))
.Save(output);
}
//shift background color towards forground color by the opacity amount
var mergedColor = new Color(Vector4.Lerp(Color.Blue.ToVector4(), Color.HotPink.ToVector4(), 150f / 255f));
using (var sourcePixels = image.Lock())
{
Assert.Equal(mergedColor, sourcePixels[10, 10]);
Assert.Equal(mergedColor, sourcePixels[200, 150]);
Assert.Equal(mergedColor, sourcePixels[50, 300]);
Assert.Equal(mergedColor, sourcePixels[37, 85]);
Assert.Equal(mergedColor, sourcePixels[93, 85]);
Assert.Equal(mergedColor, sourcePixels[65, 137]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole
Assert.Equal(Color.Blue, sourcePixels[57, 99]);
//inside shape
Assert.Equal(Color.Blue, sourcePixels[100, 192]);
}
}
}
}

197
tests/ImageSharp.Tests/Drawing/LineTests.cs

@ -0,0 +1,197 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using Drawing;
using ImageSharp.Drawing;
using ImageSharp.Drawing.Pens;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using Xunit;
public class LineTests : FileTestBase
{
[Fact]
public void ImageShouldBeOverlayedByPath()
{
string path = CreateOutputDirectory("Drawing", "Lines");
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawLines(Color.HotPink, 5, new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
})
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[9, 9]);
Assert.Equal(Color.HotPink, sourcePixels[199, 149]);
Assert.Equal(Color.Blue, sourcePixels[50, 50]);
}
}
[Fact]
public void ImageShouldBeOverlayedByPathDashed()
{
string path = CreateOutputDirectory("Drawing", "Lines");
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Dashed.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawLines(Pens.Dash(Color.HotPink, 5), new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
})
.Save(output);
}
}
[Fact]
public void ImageShouldBeOverlayedByPathDotted()
{
string path = CreateOutputDirectory("Drawing", "Lines");
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Dot.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawLines(Pens.Dot(Color.HotPink, 5), new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
})
.Save(output);
}
}
[Fact]
public void ImageShouldBeOverlayedByPathDashDot()
{
string path = CreateOutputDirectory("Drawing", "Lines");
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/DashDot.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawLines(Pens.DashDot(Color.HotPink, 5), new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
})
.Save(output);
}
}
[Fact]
public void ImageShouldBeOverlayedByPathDashDotDot()
{
string path = CreateOutputDirectory("Drawing", "Lines");
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/DashDotDot.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawLines(Pens.DashDotDot(Color.HotPink, 5), new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
})
.Save(output);
}
}
[Fact]
public void ImageShouldBeOverlayedPathWithOpacity()
{
string path = CreateOutputDirectory("Drawing", "Lines");
var color = new Color(Color.HotPink.R, Color.HotPink.G, Color.HotPink.B, 150);
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Opacity.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawLines(color, 10, new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
})
.Save(output);
}
//shift background color towards forground color by the opacity amount
var mergedColor = new Color(Vector4.Lerp(Color.Blue.ToVector4(), Color.HotPink.ToVector4(), 150f/255f));
using (var sourcePixels = image.Lock())
{
Assert.Equal(mergedColor, sourcePixels[9, 9]);
Assert.Equal(mergedColor, sourcePixels[199, 149]);
Assert.Equal(Color.Blue, sourcePixels[50, 50]);
}
}
[Fact]
public void ImageShouldBeOverlayedByPathOutline()
{
string path = CreateOutputDirectory("Drawing", "Lines");
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Rectangle.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawLines(Color.HotPink, 10, new[] {
new Point(10, 10),
new Point(200, 10),
new Point(200, 150),
new Point(10, 150)
})
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[8, 8]);
Assert.Equal(Color.HotPink, sourcePixels[198, 10]);
Assert.Equal(Color.Blue, sourcePixels[10, 50]);
Assert.Equal(Color.Blue, sourcePixels[50, 50]);
}
}
}
}

132
tests/ImageSharp.Tests/Drawing/PolygonTests.cs

@ -0,0 +1,132 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Xunit;
using Drawing;
using ImageSharp.Drawing;
using System.Numerics;
public class PolygonTests : FileTestBase
{
[Fact]
public void ImageShouldBeOverlayedByPolygonOutline()
{
string path = CreateOutputDirectory("Drawing", "Polygons");
var simplePath = new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
};
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPolygon(Color.HotPink, 5, new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
})
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[9, 9]);
Assert.Equal(Color.HotPink, sourcePixels[199, 149]);
Assert.Equal(Color.Blue, sourcePixels[50, 50]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
}
}
[Fact]
public void ImageShouldBeOverlayedPolygonOutlineWithOpacity()
{
string path = CreateOutputDirectory("Drawing", "Polygons");
var simplePath = new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
};
var color = new Color(Color.HotPink.R, Color.HotPink.G, Color.HotPink.B, 150);
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Opacity.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPolygon(color, 10, simplePath)
.Save(output);
}
//shift background color towards forground color by the opacity amount
var mergedColor = new Color(Vector4.Lerp(Color.Blue.ToVector4(), Color.HotPink.ToVector4(), 150f / 255f));
using (var sourcePixels = image.Lock())
{
Assert.Equal(mergedColor, sourcePixels[9, 9]);
Assert.Equal(mergedColor, sourcePixels[199, 149]);
Assert.Equal(Color.Blue, sourcePixels[50, 50]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
}
}
[Fact]
public void ImageShouldBeOverlayedByRectangleOutline()
{
string path = CreateOutputDirectory("Drawing", "Polygons");
var simplePath = new[] {
new Point(10, 10),
new Point(200, 10),
new Point(200, 150),
new Point(10, 150)
};
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Rectangle.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPolygon(Color.HotPink, 10, new[] {
new Point(10, 10),
new Point(200, 10),
new Point(200, 150),
new Point(10, 150)
})
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[8, 8]);
Assert.Equal(Color.HotPink, sourcePixels[198, 10]);
Assert.Equal(Color.HotPink, sourcePixels[10, 50]);
Assert.Equal(Color.Blue, sourcePixels[50, 50]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
}
}
}
}

101
tests/ImageSharp.Tests/Drawing/SolidBezierTests.cs

@ -0,0 +1,101 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using Drawing;
using ImageSharp.Drawing;
using ImageSharp.Drawing.Shapes;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using Xunit;
public class SolidBezierTests : FileTestBase
{
[Fact]
public void ImageShouldBeOverlayedByFilledPolygon()
{
string path = CreateOutputDirectory("Drawing", "FilledBezier");
var simplePath = new[] {
new PointF(10, 400),
new PointF(30, 10),
new PointF(240, 30),
new PointF(300, 400)
};
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
image
.BackgroundColor(Color.Blue)
.Fill(Color.HotPink,new BezierPolygon(simplePath))
.Save(output);
}
using (var sourcePixels = image.Lock())
{
//top of curve
Assert.Equal(Color.HotPink, sourcePixels[138, 116]);
//start points
Assert.Equal(Color.HotPink, sourcePixels[10, 400]);
Assert.Equal(Color.HotPink, sourcePixels[300, 400]);
//curve points should not be never be set
Assert.Equal(Color.Blue, sourcePixels[30, 10]);
Assert.Equal(Color.Blue, sourcePixels[240, 30]);
// inside shape should not be empty
Assert.Equal(Color.HotPink, sourcePixels[200, 250]);
}
}
[Fact]
public void ImageShouldBeOverlayedByFilledPolygonOpacity()
{
string path = CreateOutputDirectory("Drawing", "FilledBezier");
var simplePath = new[] {
new PointF(10, 400),
new PointF(30, 10),
new PointF(240, 30),
new PointF(300, 400)
};
var color = new Color(Color.HotPink.R, Color.HotPink.G, Color.HotPink.B, 150);
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Opacity.png"))
{
image
.BackgroundColor(Color.Blue)
.Fill(color, new BezierPolygon(simplePath))
.Save(output);
}
//shift background color towards forground color by the opacity amount
var mergedColor = new Color(Vector4.Lerp(Color.Blue.ToVector4(), Color.HotPink.ToVector4(), 150f / 255f));
using (var sourcePixels = image.Lock())
{
//top of curve
Assert.Equal(mergedColor, sourcePixels[138, 116]);
//start points
Assert.Equal(mergedColor, sourcePixels[10, 400]);
Assert.Equal(mergedColor, sourcePixels[300, 400]);
//curve points should not be never be set
Assert.Equal(Color.Blue, sourcePixels[30, 10]);
Assert.Equal(Color.Blue, sourcePixels[240, 30]);
// inside shape should not be empty
Assert.Equal(mergedColor, sourcePixels[200, 250]);
}
}
}
}

141
tests/ImageSharp.Tests/Drawing/SolidComplexPolygonTests.cs

@ -0,0 +1,141 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Xunit;
using Drawing;
using ImageSharp.Drawing;
using System.Numerics;
using ImageSharp.Drawing.Shapes;
public class SolidComplexPolygonTests : FileTestBase
{
[Fact]
public void ImageShouldBeOverlayedByPolygonOutline()
{
string path = CreateOutputDirectory("Drawing", "ComplexPolygon");
var simplePath = new LinearPolygon(
new Point(10, 10),
new Point(200, 150),
new Point(50, 300));
var hole1 = new LinearPolygon(
new Point(37, 85),
new Point(93, 85),
new Point(65, 137));
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
image
.BackgroundColor(Color.Blue)
.Fill(Color.HotPink, new ComplexPolygon(simplePath, hole1))
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[11, 11]);
Assert.Equal(Color.HotPink, sourcePixels[200, 150]);
Assert.Equal(Color.HotPink, sourcePixels[50, 50]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole
Assert.Equal(Color.Blue, sourcePixels[57, 99]);
}
}
[Fact]
public void ImageShouldBeOverlayedPolygonOutlineWithOverlap()
{
string path = CreateOutputDirectory("Drawing", "ComplexPolygon");
var simplePath = new LinearPolygon(
new Point(10, 10),
new Point(200, 150),
new Point(50, 300));
var hole1 = new LinearPolygon(
new Point(37, 85),
new Point(130, 40),
new Point(65, 137));
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/SimpleOverlapping.png"))
{
image
.BackgroundColor(Color.Blue)
.Fill(Color.HotPink, new ComplexPolygon(simplePath, hole1))
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[11, 11]);
Assert.Equal(Color.HotPink, sourcePixels[200, 150]);
Assert.Equal(Color.HotPink, sourcePixels[50, 50]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole
Assert.Equal(Color.Blue, sourcePixels[57, 99]);
}
}
[Fact]
public void ImageShouldBeOverlayedPolygonOutlineWithOpacity()
{
string path = CreateOutputDirectory("Drawing", "ComplexPolygon");
var simplePath = new LinearPolygon(
new Point(10, 10),
new Point(200, 150),
new Point(50, 300));
var hole1 = new LinearPolygon(
new Point(37, 85),
new Point(93, 85),
new Point(65, 137));
var color = new Color(Color.HotPink.R, Color.HotPink.G, Color.HotPink.B, 150);
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Opacity.png"))
{
image
.BackgroundColor(Color.Blue)
.Fill(color, new ComplexPolygon(simplePath, hole1))
.Save(output);
}
//shift background color towards forground color by the opacity amount
var mergedColor = new Color(Vector4.Lerp(Color.Blue.ToVector4(), Color.HotPink.ToVector4(), 150f / 255f));
using (var sourcePixels = image.Lock())
{
Assert.Equal(mergedColor, sourcePixels[11, 11]);
Assert.Equal(mergedColor, sourcePixels[200, 150]);
Assert.Equal(mergedColor, sourcePixels[50, 50]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole
Assert.Equal(Color.Blue, sourcePixels[57, 99]);
}
}
}
}

120
tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs

@ -0,0 +1,120 @@
// <copyright file="ColorConversionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Drawing
{
using Drawing;
using ImageSharp.Drawing;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using Xunit;
public class SolidPolygonTests : FileTestBase
{
[Fact]
public void ImageShouldBeOverlayedByFilledPolygon()
{
string path = CreateOutputDirectory("Drawing", "FilledPolygons");
var simplePath = new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
};
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Simple.png"))
{
image
.BackgroundColor(Color.Blue)
.FillPolygon(Color.HotPink, simplePath)
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[11, 11]);
Assert.Equal(Color.HotPink, sourcePixels[200, 150]);
Assert.Equal(Color.HotPink, sourcePixels[50, 50]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
}
}
[Fact]
public void ImageShouldBeOverlayedByFilledPolygonOpacity()
{
string path = CreateOutputDirectory("Drawing", "FilledPolygons");
var simplePath = new[] {
new Point(10, 10),
new Point(200, 150),
new Point(50, 300)
};
var color = new Color(Color.HotPink.R, Color.HotPink.G, Color.HotPink.B, 150);
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Opacity.png"))
{
image
.BackgroundColor(Color.Blue)
.FillPolygon(color, simplePath)
.Save(output);
}
//shift background color towards forground color by the opacity amount
var mergedColor = new Color(Vector4.Lerp(Color.Blue.ToVector4(), Color.HotPink.ToVector4(), 150f / 255f));
using (var sourcePixels = image.Lock())
{
Assert.Equal(mergedColor, sourcePixels[11, 11]);
Assert.Equal(mergedColor, sourcePixels[200, 150]);
Assert.Equal(mergedColor, sourcePixels[50, 50]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
}
}
[Fact]
public void ImageShouldBeOverlayedByFilledRectangle()
{
string path = CreateOutputDirectory("Drawing", "FilledPolygons");
var simplePath = new[] {
new Point(10, 10),
new Point(200, 10),
new Point(200, 150),
new Point(10, 150)
};
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/Rectangle.png"))
{
image
.BackgroundColor(Color.Blue)
.FillPolygon(Color.HotPink, simplePath)
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[11, 11]);
Assert.Equal(Color.HotPink, sourcePixels[198, 10]);
Assert.Equal(Color.HotPink, sourcePixels[10, 50]);
Assert.Equal(Color.HotPink, sourcePixels[50, 50]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
}
}
}
}

10
tests/ImageSharp.Tests/FileTestBase.cs

@ -44,9 +44,15 @@ namespace ImageSharp.Tests
// new TestFile(TestImages.Gif.Giphy) // Perf: Enable for local testing only
};
protected string CreateOutputDirectory(string path)
protected string CreateOutputDirectory(string path, params string[] pathParts)
{
path = "TestOutput/" + path;
var postFix = "";
if (pathParts != null && pathParts.Length > 0)
{
postFix = "/" + string.Join("/", pathParts);
}
path = "TestOutput/" + path + postFix;
if (!Directory.Exists(path))
{

Loading…
Cancel
Save