diff --git a/src/ImageSharp/Drawing/Brushes/IBrush.cs b/src/ImageSharp/Drawing/Brushes/IBrush.cs
index 7c3f29335..0f090ebbb 100644
--- a/src/ImageSharp/Drawing/Brushes/IBrush.cs
+++ b/src/ImageSharp/Drawing/Brushes/IBrush.cs
@@ -23,12 +23,15 @@ namespace ImageSharp.Drawing
///
/// Creates the applicator for this brush.
///
+ /// The pixel source.
/// The region the brush will be applied to.
- /// The brush applicator for this brush
+ ///
+ /// The brush applicator for this brush
+ ///
///
/// The when being applied to things like shapes would usually be the
/// bounding box of the shape not necessarily the bounds of the whole image
///
- IBrushApplicator CreateApplicator(RectangleF region);
+ IBrushApplicator CreateApplicator(IReadonlyPixelAccessor pixelSource, RectangleF region);
}
}
\ No newline at end of file
diff --git a/src/ImageSharp/Drawing/Brushes/ImageBrush{TColor}.cs b/src/ImageSharp/Drawing/Brushes/ImageBrush{TColor}.cs
index ce15b4a2e..c8a601bbf 100644
--- a/src/ImageSharp/Drawing/Brushes/ImageBrush{TColor}.cs
+++ b/src/ImageSharp/Drawing/Brushes/ImageBrush{TColor}.cs
@@ -32,7 +32,7 @@ namespace ImageSharp.Drawing.Brushes
}
///
- public IBrushApplicator CreateApplicator(RectangleF region)
+ public IBrushApplicator CreateApplicator(IReadonlyPixelAccessor sourcePixels, RectangleF region)
{
return new ImageBrushApplicator(this.image, region);
}
diff --git a/src/ImageSharp/Drawing/Brushes/PatternBrush{TColor}.cs b/src/ImageSharp/Drawing/Brushes/PatternBrush{TColor}.cs
index 8ed5f8ae1..6373b51ad 100644
--- a/src/ImageSharp/Drawing/Brushes/PatternBrush{TColor}.cs
+++ b/src/ImageSharp/Drawing/Brushes/PatternBrush{TColor}.cs
@@ -95,7 +95,7 @@ namespace ImageSharp.Drawing.Brushes
}
///
- public IBrushApplicator CreateApplicator(RectangleF region)
+ public IBrushApplicator CreateApplicator(IReadonlyPixelAccessor sourcePixels, RectangleF region)
{
return new PatternBrushApplicator(this.pattern, this.stride);
}
diff --git a/src/ImageSharp/Drawing/Brushes/RecolorBrush.cs b/src/ImageSharp/Drawing/Brushes/RecolorBrush.cs
new file mode 100644
index 000000000..9e00e881f
--- /dev/null
+++ b/src/ImageSharp/Drawing/Brushes/RecolorBrush.cs
@@ -0,0 +1,24 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Drawing.Brushes
+{
+ ///
+ /// Provides an implementation of a recolor brush for painting color changes.
+ ///
+ public class RecolorBrush : RecolorBrush
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Color of the source.
+ /// Color of the target.
+ /// The threashold.
+ public RecolorBrush(Color sourceColor, Color targetColor, float threashold)
+ : base(sourceColor, targetColor, threashold)
+ {
+ }
+ }
+}
diff --git a/src/ImageSharp/Drawing/Brushes/RecolorBrush{TColor}.cs b/src/ImageSharp/Drawing/Brushes/RecolorBrush{TColor}.cs
new file mode 100644
index 000000000..09db4eaf0
--- /dev/null
+++ b/src/ImageSharp/Drawing/Brushes/RecolorBrush{TColor}.cs
@@ -0,0 +1,129 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Drawing.Brushes
+{
+ using System;
+ using System.Numerics;
+
+ using Processors;
+
+ ///
+ /// Provides an implementation of a brush that can recolor an image
+ ///
+ /// The pixel format.
+ public class RecolorBrush : IBrush
+ where TColor : struct, IPackedPixel, IEquatable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Color of the source.
+ /// Color of the target.
+ /// The threashold as a value between 0 and 1.
+ public RecolorBrush(TColor sourceColor, TColor targetColor, float threashold)
+ {
+ this.SourceColor = sourceColor;
+ this.Threashold = threashold;
+ this.TargetColor = targetColor;
+ }
+
+ ///
+ /// Gets the threashold.
+ ///
+ ///
+ /// The threashold.
+ ///
+ public float Threashold { get; }
+
+ ///
+ /// Gets the source color.
+ ///
+ ///
+ /// The color of the source.
+ ///
+ public TColor SourceColor { get; }
+
+ ///
+ /// Gets the target color.
+ ///
+ ///
+ /// The color of the target.
+ ///
+ public TColor TargetColor { get; }
+
+ ///
+ public IBrushApplicator CreateApplicator(IReadonlyPixelAccessor sourcePixels, RectangleF region)
+ {
+ return new RecolorBrushApplicator(sourcePixels, this.SourceColor, this.TargetColor, this.Threashold);
+ }
+
+ ///
+ /// The recolor brush applicator.
+ ///
+ private class RecolorBrushApplicator : IBrushApplicator
+ {
+ ///
+ /// The source pixel accessor.
+ ///
+ private readonly IReadonlyPixelAccessor source;
+ private readonly Vector4 sourceColor;
+ private readonly Vector4 targetColor;
+ private readonly float threashold;
+ private readonly float totalDistance;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source pixels.
+ /// Color of the source.
+ /// Color of the target.
+ /// The threashold .
+ public RecolorBrushApplicator(IReadonlyPixelAccessor sourcePixels, TColor sourceColor, TColor targetColor, float threashold)
+ {
+ this.source = sourcePixels;
+ this.sourceColor = sourceColor.ToVector4();
+ this.targetColor = targetColor.ToVector4();
+
+ // lets hack a min max extreams for a color space by letteing the IPackedPixle clamp our values to something in the correct spaces :)
+ TColor maxColor = default(TColor);
+ maxColor.PackFromVector4(new Vector4(float.MaxValue));
+ TColor minColor = default(TColor);
+ minColor.PackFromVector4(new Vector4(float.MinValue));
+ this.totalDistance = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4());
+ this.threashold = this.totalDistance * threashold;
+ }
+
+ ///
+ /// Gets the color for a single pixel.
+ ///
+ /// The point.
+ ///
+ /// The color
+ ///
+ public TColor GetColor(Vector2 point)
+ {
+ // Offset the requested pixel by the value in the rectangle (the shapes position)
+ TColor result = this.source[(int)point.X, (int)point.Y];
+ Vector4 background = result.ToVector4();
+ float distance = Vector4.DistanceSquared(background, this.sourceColor);
+ if (distance <= this.threashold)
+ {
+ var lerpAmount = (this.threashold - distance) / this.threashold;
+ Vector4 blended = Vector4BlendTransforms.PremultipliedLerp(background, this.targetColor, lerpAmount);
+ result.PackFromVector4(blended);
+ }
+
+ return result;
+ }
+
+ ///
+ public void Dispose()
+ {
+ // we didn't make the lock on the PixelAccessor we shouldn't release it.
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp/Drawing/Brushes/SolidBrush{TColor}.cs b/src/ImageSharp/Drawing/Brushes/SolidBrush{TColor}.cs
index 6b74580d8..2c277b5a7 100644
--- a/src/ImageSharp/Drawing/Brushes/SolidBrush{TColor}.cs
+++ b/src/ImageSharp/Drawing/Brushes/SolidBrush{TColor}.cs
@@ -40,7 +40,7 @@ namespace ImageSharp.Drawing.Brushes
public TColor Color => this.color;
///
- public IBrushApplicator CreateApplicator(RectangleF region)
+ public IBrushApplicator CreateApplicator(IReadonlyPixelAccessor sourcePixels, RectangleF region)
{
return new SolidBrushApplicator(this.color);
}
diff --git a/src/ImageSharp/Drawing/Pens/IPen.cs b/src/ImageSharp/Drawing/Pens/IPen.cs
index 0b4d525f2..8b23c1f0f 100644
--- a/src/ImageSharp/Drawing/Pens/IPen.cs
+++ b/src/ImageSharp/Drawing/Pens/IPen.cs
@@ -18,11 +18,14 @@ namespace ImageSharp.Drawing.Pens
///
/// Creates the applicator for applying this pen to an Image
///
+ /// The pixel source.
/// The region the pen will be applied to.
- /// Returns a the applicator for the pen.
+ ///
+ /// Returns a the applicator for the pen.
+ ///
///
/// The when being applied to things like shapes would usually be the bounding box of the shape not necessarily the shape of the whole image.
///
- IPenApplicator CreateApplicator(RectangleF region);
+ IPenApplicator CreateApplicator(IReadonlyPixelAccessor pixelSource, RectangleF region);
}
}
diff --git a/src/ImageSharp/Drawing/Pens/Pen{TColor}.cs b/src/ImageSharp/Drawing/Pens/Pen{TColor}.cs
index e89e7ba82..d674b1a56 100644
--- a/src/ImageSharp/Drawing/Pens/Pen{TColor}.cs
+++ b/src/ImageSharp/Drawing/Pens/Pen{TColor}.cs
@@ -103,6 +103,7 @@ namespace ImageSharp.Drawing.Pens
///
/// Creates the applicator for applying this pen to an Image
///
+ /// The source pixels.
/// The region the pen will be applied to.
///
/// Returns a the applicator for the pen.
@@ -111,16 +112,16 @@ namespace ImageSharp.Drawing.Pens
/// The when being applied to things like shapes would ussually be the
/// bounding box of the shape not necorserrally the shape of the whole image
///
- public IPenApplicator CreateApplicator(RectangleF region)
+ public IPenApplicator CreateApplicator(IReadonlyPixelAccessor sourcePixels, 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 SolidPenApplicator(sourcePixels, this.Brush, region, this.Width);
}
- return new PatternPenApplicator(this.Brush, region, this.Width, this.pattern);
+ return new PatternPenApplicator(sourcePixels, this.Brush, region, this.Width, this.pattern);
}
private class SolidPenApplicator : IPenApplicator
@@ -128,9 +129,9 @@ namespace ImageSharp.Drawing.Pens
private readonly IBrushApplicator brush;
private readonly float halfWidth;
- public SolidPenApplicator(IBrush brush, RectangleF region, float width)
+ public SolidPenApplicator(IReadonlyPixelAccessor sourcePixels, IBrush brush, RectangleF region, float width)
{
- this.brush = brush.CreateApplicator(region);
+ this.brush = brush.CreateApplicator(sourcePixels, region);
this.halfWidth = width / 2;
this.RequiredRegion = RectangleF.Outset(region, width);
}
@@ -171,9 +172,9 @@ namespace ImageSharp.Drawing.Pens
private readonly float[] pattern;
private readonly float totalLength;
- public PatternPenApplicator(IBrush brush, RectangleF region, float width, float[] pattern)
+ public PatternPenApplicator(IReadonlyPixelAccessor sourcePixels, IBrush brush, RectangleF region, float width, float[] pattern)
{
- this.brush = brush.CreateApplicator(region);
+ this.brush = brush.CreateApplicator(sourcePixels, region);
this.halfWidth = width / 2;
this.totalLength = 0;
diff --git a/src/ImageSharp/Drawing/Processors/DrawPathProcessor.cs b/src/ImageSharp/Drawing/Processors/DrawPathProcessor.cs
index 2bb7c350f..54ebc28ef 100644
--- a/src/ImageSharp/Drawing/Processors/DrawPathProcessor.cs
+++ b/src/ImageSharp/Drawing/Processors/DrawPathProcessor.cs
@@ -86,7 +86,8 @@ namespace ImageSharp.Drawing.Processors
///
protected override void OnApply(ImageBase source, Rectangle sourceRectangle)
{
- using (IPenApplicator applicator = this.pen.CreateApplicator(this.region))
+ using (PixelAccessor sourcePixels = source.Lock())
+ using (IPenApplicator applicator = this.pen.CreateApplicator(sourcePixels, this.region))
{
var rect = RectangleF.Ceiling(applicator.RequiredRegion);
@@ -117,45 +118,42 @@ namespace ImageSharp.Drawing.Processors
polyStartY = 0;
}
- using (PixelAccessor sourcePixels = source.Lock())
+ Parallel.For(
+ minY,
+ maxY,
+ this.ParallelOptions,
+ y =>
{
- Parallel.For(
- minY,
- maxY,
- this.ParallelOptions,
- y =>
+ int offsetY = y - polyStartY;
+ var currentPoint = default(Vector2);
+ for (int x = minX; x < maxX; x++)
{
- int offsetY = y - polyStartY;
- var currentPoint = default(Vector2);
- for (int x = minX; x < maxX; x++)
- {
- int offsetX = x - startX;
- currentPoint.X = offsetX;
- currentPoint.Y = offsetY;
+ int offsetX = x - startX;
+ currentPoint.X = offsetX;
+ currentPoint.Y = offsetY;
- var dist = this.Closest(currentPoint);
+ var dist = this.Closest(currentPoint);
- var color = applicator.GetColor(dist);
+ var color = applicator.GetColor(dist);
- var opacity = this.Opacity(color.DistanceFromElement);
+ var opacity = this.Opacity(color.DistanceFromElement);
- if (opacity > Epsilon)
- {
- int offsetColorX = x - minX;
+ if (opacity > Epsilon)
+ {
+ int offsetColorX = x - minX;
- Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
- Vector4 sourceVector = color.Color.ToVector4();
+ Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4();
+ Vector4 sourceVector = color.Color.ToVector4();
- var finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
- finalColor.W = backgroundVector.W;
+ var finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity);
+ finalColor.W = backgroundVector.W;
- TColor packed = default(TColor);
- packed.PackFromVector4(finalColor);
- sourcePixels[offsetX, offsetY] = packed;
- }
+ TColor packed = default(TColor);
+ packed.PackFromVector4(finalColor);
+ sourcePixels[offsetX, offsetY] = packed;
}
- });
- }
+ }
+ });
}
}
diff --git a/src/ImageSharp/Drawing/Processors/FillProcessor.cs b/src/ImageSharp/Drawing/Processors/FillProcessor.cs
index 7e773392b..a2cf12fdd 100644
--- a/src/ImageSharp/Drawing/Processors/FillProcessor.cs
+++ b/src/ImageSharp/Drawing/Processors/FillProcessor.cs
@@ -62,7 +62,7 @@ namespace ImageSharp.Drawing.Processors
// for example If brush is SolidBrush then we could just get the color upfront
// and skip using the IBrushApplicator?.
using (PixelAccessor sourcePixels = source.Lock())
- using (IBrushApplicator applicator = this.brush.CreateApplicator(sourceRectangle))
+ using (IBrushApplicator applicator = this.brush.CreateApplicator(sourcePixels, sourceRectangle))
{
Parallel.For(
minY,
diff --git a/src/ImageSharp/Drawing/Processors/FillShapeProcessor.cs b/src/ImageSharp/Drawing/Processors/FillShapeProcessor.cs
index 52d3b7a4b..df5cec71c 100644
--- a/src/ImageSharp/Drawing/Processors/FillShapeProcessor.cs
+++ b/src/ImageSharp/Drawing/Processors/FillShapeProcessor.cs
@@ -53,9 +53,9 @@ namespace ImageSharp.Drawing.Processors
int endX = rect.Right + DrawPadding;
int minX = Math.Max(sourceRectangle.Left, startX);
- int maxX = Math.Min(sourceRectangle.Right, endX);
+ int maxX = Math.Min(sourceRectangle.Right - 1, endX);
int minY = Math.Max(sourceRectangle.Top, polyStartY);
- int maxY = Math.Min(sourceRectangle.Bottom, polyEndY);
+ int maxY = Math.Min(sourceRectangle.Bottom - 1, polyEndY);
// Align start/end positions.
minX = Math.Max(0, minX);
@@ -75,7 +75,7 @@ namespace ImageSharp.Drawing.Processors
}
using (PixelAccessor sourcePixels = source.Lock())
- using (IBrushApplicator applicator = this.fillColor.CreateApplicator(rect))
+ using (IBrushApplicator applicator = this.fillColor.CreateApplicator(sourcePixels, rect))
{
Parallel.For(
minY,
diff --git a/src/ImageSharp/Image/IReadonlyPixelAccessor{TColor}.cs b/src/ImageSharp/Image/IReadonlyPixelAccessor{TColor}.cs
new file mode 100644
index 000000000..dbe17603a
--- /dev/null
+++ b/src/ImageSharp/Image/IReadonlyPixelAccessor{TColor}.cs
@@ -0,0 +1,50 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp
+{
+ using System;
+ using System.Diagnostics;
+ using System.Runtime.CompilerServices;
+ using System.Runtime.InteropServices;
+
+ ///
+ /// Provides per-pixel readonly access to generic pixels.
+ ///
+ /// The pixel format.
+ public interface IReadonlyPixelAccessor
+ {
+ ///
+ /// Gets the size of a single pixel in the number of bytes.
+ ///
+ int PixelSize { get; }
+
+ ///
+ /// Gets the width of one row in the number of bytes.
+ ///
+ int RowStride { get; }
+
+ ///
+ /// Gets the width of the image.
+ ///
+ int Width { get; }
+
+ ///
+ /// Gets the height of the image.
+ ///
+ int Height { get; }
+
+ ///
+ /// Gets or sets the pixel at the specified position.
+ ///
+ /// The x-coordinate of the pixel. Must be greater than zero and smaller than the width of the pixel.
+ /// The y-coordinate of the pixel. Must be greater than zero and smaller than the width of the pixel.
+ /// The at the specified position.
+ TColor this[int x, int y]
+ {
+ get;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp/Image/PixelAccessor{TColor}.cs b/src/ImageSharp/Image/PixelAccessor{TColor}.cs
index 3642d3942..8ce780563 100644
--- a/src/ImageSharp/Image/PixelAccessor{TColor}.cs
+++ b/src/ImageSharp/Image/PixelAccessor{TColor}.cs
@@ -14,7 +14,7 @@ namespace ImageSharp
/// Provides per-pixel access to generic pixels.
///
/// The pixel format.
- public unsafe class PixelAccessor : IDisposable
+ public unsafe class PixelAccessor : IReadonlyPixelAccessor, IDisposable
where TColor : struct, IPackedPixel, IEquatable
{
///
diff --git a/tests/ImageSharp.Tests/Drawing/RecolorImageTest.cs b/tests/ImageSharp.Tests/Drawing/RecolorImageTest.cs
new file mode 100644
index 000000000..2edd05be1
--- /dev/null
+++ b/tests/ImageSharp.Tests/Drawing/RecolorImageTest.cs
@@ -0,0 +1,55 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Tests
+{
+ using ImageSharp.Drawing.Brushes;
+ using System.IO;
+ using System.Linq;
+
+ using Xunit;
+
+ public class RecolorImageTest : FileTestBase
+ {
+ [Fact]
+ public void ImageShouldRecolorYellowToHotPink()
+ {
+ string path = CreateOutputDirectory("Drawing", "RecolorImage");
+
+ var brush = new RecolorBrush(Color.Yellow, Color.HotPink, 0.2f);
+
+ foreach (TestFile file in Files)
+ {
+ Image image = file.CreateImage();
+
+ using (FileStream output = File.OpenWrite($"{path}/{file.FileName}"))
+ {
+ image.Fill(brush)
+ .Save(output);
+ }
+ }
+ }
+
+ [Fact]
+ public void ImageShouldRecolorYellowToHotPinkInARectangle()
+ {
+ string path = CreateOutputDirectory("Drawing", "RecolorImage");
+
+ var brush = new RecolorBrush(Color.Yellow, Color.HotPink, 0.2f);
+
+ foreach (TestFile file in Files)
+ {
+ Image image = file.CreateImage();
+
+ using (FileStream output = File.OpenWrite($"{path}/Shaped_{file.FileName}"))
+ {
+ var imageHeight = image.Height;
+ image.Fill(brush, new Rectangle(0, imageHeight/2 - imageHeight/4, image.Width, imageHeight/2))
+ .Save(output);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file