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