diff --git a/src/ImageSharp.Drawing/DrawImage.cs b/src/ImageSharp.Drawing/DrawImage.cs index 6a4f49337c..52c275595b 100644 --- a/src/ImageSharp.Drawing/DrawImage.cs +++ b/src/ImageSharp.Drawing/DrawImage.cs @@ -1,57 +1,105 @@ -// -// Copyright (c) James Jackson-South and contributors. -// Licensed under the Apache License, Version 2.0. -// - -namespace ImageSharp -{ - using Drawing.Processors; - using ImageSharp.PixelFormats; - - /// - /// Extension methods for the type. - /// - public static partial class ImageExtensions - { - /// - /// Draws the given image together with the current one by blending their pixels. - /// - /// The pixel format. - /// The image this method extends. - /// The image to blend with the currently processing image. - /// The opacity of the image image to blend. Must be between 0 and 100. - /// The . - public static Image Blend(this Image source, Image image, int percent = 50) - where TPixel : struct, IPixel - { - return DrawImage(source, image, percent, default(Size), default(Point)); - } - - /// - /// Draws the given image together with the current one by blending their pixels. - /// - /// The image this method extends. - /// The image to blend with the currently processing image. - /// The pixel format. - /// The opacity of the image image to blend. Must be between 0 and 100. - /// The size to draw the blended image. - /// The location to draw the blended image. - /// The . - public static Image DrawImage(this Image source, Image image, int percent, Size size, Point location) - where TPixel : struct, IPixel - { - if (size == default(Size)) - { - size = new Size(image.Width, image.Height); - } - - if (location == default(Point)) - { - location = Point.Empty; - } - - source.ApplyProcessor(new DrawImageProcessor(image, size, location, percent), source.Bounds); - return source; - } - } +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp +{ + using System; + using Drawing.Processors; + using ImageSharp.PixelFormats; + + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Draws the given image together with the current one by blending their pixels. + /// + /// The pixel format. + /// The image this method extends. + /// The image to blend with the currently processing image. + /// The opacity of the image image to blend. Must be between 0 and 100. + /// The . + public static Image Blend(this Image source, Image image, int percent = 50) + where TPixel : struct, IPixel + { + return DrawImage(source, image, percent, default(Size), default(Point)); + } + + /// + /// Draws the given image together with the current one by blending their pixels. + /// + /// The image this method extends. + /// The image to blend with the currently processing image. + /// The pixel format. + /// The opacity of the image image to blend. Must be between 0 and 100. + /// The size to draw the blended image. + /// The location to draw the blended image. + /// The . + public static Image DrawImage(this Image source, Image image, int percent, Size size, Point location) + where TPixel : struct, IPixel + { + if (size == default(Size)) + { + size = new Size(image.Width, image.Height); + } + + if (location == default(Point)) + { + location = Point.Empty; + } + + source.ApplyProcessor(new DrawImageProcessor(image, size, location, percent), source.Bounds); + return source; + } + + /// + /// Draws the given image together with the current one by blending their pixels. + /// + /// The image this method extends. + /// The image to blend with the currently processing image. + /// Pixel function effect to apply on every pixel + /// The pixel format. + /// The opacity of the image image to blend. Must be between 0 and 100. + /// The size to draw the blended image. + /// The location to draw the blended image. + /// The . + public static Image DrawImage(this Image source, Image image, PixelTransformMode mode, int percent, Size size, Point location) + where TPixel : struct, IPixel + { + Func pixelFunc = mode.GetPixelFunction(); + + return DrawImage(source, image, pixelFunc, percent, size, location); + } + + /// + /// Draws the given image together with the current one by blending their pixels. + /// + /// The image this method extends. + /// The image to blend with the currently processing image. + /// Pixel function effect to apply on every pixel + /// The pixel format. + /// The opacity of the image image to blend. Must be between 0 and 100. + /// The size to draw the blended image. + /// The location to draw the blended image. + /// The . + public static Image DrawImage(this Image source, Image image, Func pixelFunc, int percent, Size size, Point location) + where TPixel : struct, IPixel + { + if (size == default(Size)) + { + size = new Size(image.Width, image.Height); + } + + if (location == default(Point)) + { + location = Point.Empty; + } + + source.ApplyProcessor(new DrawImageEffectProcessor(image, size, location, pixelFunc, percent), source.Bounds); + return source; + } + } } \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processors/DrawImageEffectProcessor.cs b/src/ImageSharp.Drawing/Processors/DrawImageEffectProcessor.cs new file mode 100644 index 0000000000..4ef8800f92 --- /dev/null +++ b/src/ImageSharp.Drawing/Processors/DrawImageEffectProcessor.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Drawing.Processors +{ + using System; + using System.Numerics; + using System.Threading.Tasks; + using ImageSharp.PixelFormats; + using ImageSharp.Processing; + + /// + /// Combines two images together by blending the pixels. + /// + /// The pixel format. + internal class DrawImageEffectProcessor : ImageProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The image to blend with the currently processing image. + /// The size to draw the blended image. + /// The location to draw the blended image. + /// Pixel function effect to apply on every pixel + /// The opacity of the image to blend. Between 0 and 100. + public DrawImageEffectProcessor(Image image, Size size, Point location, Func pixelFunction, int alpha = 100) + { + Guard.MustBeBetweenOrEqualTo(alpha, 0, 100, nameof(alpha)); + this.Image = image; + this.PixelFunction = pixelFunction; + this.Size = size; + this.Location = location; + this.Alpha = alpha; + } + + /// + /// Gets the image to blend. + /// + public Image Image { get; private set; } + + /// + /// Gets The function effect to apply on a per pixel basis + /// + public Func PixelFunction { get; private set; } + + /// + /// Gets the alpha percentage value. + /// + public int Alpha { get; } + + /// + /// Gets the size to draw the blended image. + /// + public Size Size { get; } + + /// + /// Gets the location to draw the blended image. + /// + public Point Location { get; } + + /// + protected override void OnApply(ImageBase target, Rectangle sourceRectangle) + { + if (this.Image.Bounds.Size != this.Size) + { + // should Resize be moved to core? + this.Image = this.Image.Resize(this.Size.Width, this.Size.Height); + } + + // Align start/end positions. + Rectangle bounds = this.Image.Bounds; + int minX = Math.Max(this.Location.X, sourceRectangle.X); + int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width); + int minY = Math.Max(this.Location.Y, sourceRectangle.Y); + int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom); + + float alpha = this.Alpha / 100F; + + using (PixelAccessor sourcePixels = this.Image.Lock()) + using (PixelAccessor targetPixels = target.Lock()) + { + Parallel.For( + minY, + maxY, + this.ParallelOptions, + y => + { + for (int x = minX; x < maxX; x++) + { + TPixel targetColor = targetPixels[x, y]; + TPixel sourceColor = sourcePixels[x - minX, y - minY]; + + targetPixels[x, y] = this.PixelFunction(targetColor, sourceColor, alpha); + } + }); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/PixelFormats/PixelTransformMode.cs b/src/ImageSharp/PixelFormats/PixelTransformMode.cs new file mode 100644 index 0000000000..159b275158 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelTransformMode.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.PixelFormats +{ + /// + /// Porter Duff Blending composition modes + /// + public enum PixelTransformMode + { + /// + /// Default blending mode, also known as "Normal" or "Alpha Blending" + /// + Normal, + + /// + /// Backdrop + Source + /// + Multiply, + + /// + /// Backdrop + Source + /// + Add, + + /// + /// Backdrop - Source + /// + Substract, + + /// + /// Screen effect + /// + Screen, + + /// + /// Darken effect + /// + Darken, + + /// + /// Lighten effect + /// + Lighten, + + /// + /// Overlay effect + /// + Overlay, + + /// + /// Hard light effect + /// + HardLight + } +} diff --git a/src/ImageSharp/PixelFormats/PixelTransformModeExtensions.cs b/src/ImageSharp/PixelFormats/PixelTransformModeExtensions.cs new file mode 100644 index 0000000000..4ecddfded0 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PixelTransformModeExtensions.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp +{ + using System; + using System.Collections.Generic; + using System.Text; + using PixelFormats; + + /// + /// Extensions to retrieve the appropiate pixel transformation functions for + /// + public static class PixelTransformModeExtensions + { + /// + /// Gets a pixel transformation function + /// + /// The pixel format used for both Backdrop and Source + /// The Duff Porter mode + /// A function that transforms a Backdrop and Source colors into a final color + public static Func GetPixelFunction(this PixelTransformMode mode) + where TPixel : IPixel + { + return mode.GetPixelFunction(); + } + + /// + /// Gets a pixel transformation function + /// + /// The pixel format used for Backdrop and Output + /// The pixel format used for Source + /// The Duff Porter mode + /// A function that transforms a Backdrop and Source colors into a final color + public static Func GetPixelFunction(this PixelTransformMode mode) + where TBckPixel : IPixel + where TSrcPixel : IPixel + { + switch (mode) + { + case PixelTransformMode.Normal: return PorterDuffFunctions.NormalBlendFunction; + case PixelTransformMode.Multiply: return PorterDuffFunctions.MultiplyFunction; + case PixelTransformMode.Add: return PorterDuffFunctions.AddFunction; + case PixelTransformMode.Substract: return PorterDuffFunctions.SubstractFunction; + case PixelTransformMode.Screen: return PorterDuffFunctions.ScreenFunction; + case PixelTransformMode.Darken: return PorterDuffFunctions.DarkenFunction; + case PixelTransformMode.Lighten: return PorterDuffFunctions.LightenFunction; + case PixelTransformMode.Overlay: return PorterDuffFunctions.OverlayFunction; + case PixelTransformMode.HardLight: return PorterDuffFunctions.HardLightFunction; + + default: throw new NotImplementedException(nameof(mode)); + } + } + } +} diff --git a/src/ImageSharp/PixelFormats/PorterDuffFunctions.cs b/src/ImageSharp/PixelFormats/PorterDuffFunctions.cs new file mode 100644 index 0000000000..1c9f6b8425 --- /dev/null +++ b/src/ImageSharp/PixelFormats/PorterDuffFunctions.cs @@ -0,0 +1,267 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.PixelFormats +{ + using System.Numerics; + using System.Runtime.CompilerServices; + + /// + /// Collection of Porter Duff alpha blending functions + /// + /// Backdrop Pixel Format + /// Source Pixel Format + /// + /// These functions are designed to be a general solution for all color cases, + /// that is, they take in account the alpha value of both the backdrop + /// and source, and there's no need to alpha-premultiply neither the backdrop + /// nor the source. + /// Note there are faster functions for when the backdrop color is known + /// to be opaque + /// + internal static class PorterDuffFunctions + where TBckPixel : IPixel + where TsrcPixel : IPixel + { + /// + /// Source over backdrop + /// + /// Backgrop color + /// Source color + /// Opacity applied to Source Alpha + /// Output color + public static TBckPixel NormalBlendFunction(TBckPixel backdrop, TsrcPixel source, float opacity) + { + Vector4 l = source.ToVector4(); + l.W *= opacity; + if (l.W == 0) + { + return backdrop; + } + + Vector4 b = backdrop.ToVector4(); + + return Compose(b, l, l); + } + + /// + /// Source multiplied by backdrop + /// + /// Backgrop color + /// Source color + /// Opacity applied to Source Alpha + /// Output color + public static TBckPixel MultiplyFunction(TBckPixel backdrop, TsrcPixel source, float opacity) + { + Vector4 l = source.ToVector4(); + l.W *= opacity; + if (l.W == 0) + { + return backdrop; + } + + Vector4 b = backdrop.ToVector4(); + + return Compose(b, l, b * l); + } + + /// + /// Source added to backdrop + /// + /// Backgrop color + /// Source color + /// Opacity applied to Source Alpha + /// Output color + public static TBckPixel AddFunction(TBckPixel backdrop, TsrcPixel source, float opacity) + { + Vector4 l = source.ToVector4(); + l.W *= opacity; + if (l.W == 0) + { + return backdrop; + } + + Vector4 b = backdrop.ToVector4(); + + return Compose(b, l, Vector4.Min(Vector4.One, b + l)); + } + + /// + /// Source substracted from backdrop + /// + /// Backgrop color + /// Source color + /// Opacity applied to Source Alpha + /// Output color + public static TBckPixel SubstractFunction(TBckPixel backdrop, TsrcPixel source, float opacity) + { + Vector4 l = source.ToVector4(); + l.W *= opacity; + if (l.W == 0) + { + return backdrop; + } + + Vector4 b = backdrop.ToVector4(); + + return Compose(b, l, Vector4.Max(Vector4.Zero, b - l)); + } + + /// + /// Complement of source multiplied by the complement of backdrop + /// + /// Backgrop color + /// Source color + /// Opacity applied to Source Alpha + /// Output color + public static TBckPixel ScreenFunction(TBckPixel backdrop, TsrcPixel source, float opacity) + { + Vector4 l = source.ToVector4(); + l.W *= opacity; + if (l.W == 0) + { + return backdrop; + } + + Vector4 b = backdrop.ToVector4(); + + return Compose(b, l, Vector4.One - ((Vector4.One - b) * (Vector4.One - l))); + } + + /// + /// Per element, chooses the smallest value of source and backdrop + /// + /// Backgrop color + /// Source color + /// Opacity applied to Source Alpha + /// Output color + public static TBckPixel DarkenFunction(TBckPixel backdrop, TsrcPixel source, float opacity) + { + Vector4 l = source.ToVector4(); + l.W *= opacity; + if (l.W == 0) + { + return backdrop; + } + + Vector4 b = backdrop.ToVector4(); + + return Compose(b, l, Vector4.Min(b, l)); + } + + /// + /// Per element, chooses the largest value of source and backdrop + /// + /// Backgrop color + /// Source color + /// Opacity applied to Source Alpha + /// Output color + public static TBckPixel LightenFunction(TBckPixel backdrop, TsrcPixel source, float opacity) + { + Vector4 l = source.ToVector4(); + l.W *= opacity; + if (l.W == 0) + { + return backdrop; + } + + Vector4 b = backdrop.ToVector4(); + + return Compose(b, l, Vector4.Max(b, l)); + } + + /// + /// Overlays source over backdrop + /// + /// Backgrop color + /// Source color + /// Opacity applied to Source Alpha + /// Output color + public static TBckPixel OverlayFunction(TBckPixel backdrop, TsrcPixel source, float opacity) + { + Vector4 l = source.ToVector4(); + l.W *= opacity; + if (l.W == 0) + { + return backdrop; + } + + Vector4 b = backdrop.ToVector4(); + + float cr = OverlayValueFunction(b.X, l.X); + float cg = OverlayValueFunction(b.Y, l.Y); + float cb = OverlayValueFunction(b.Z, l.Z); + + return Compose(b, l, Vector4.Min(Vector4.One, new Vector4(cr, cg, cb, 0))); + } + + /// + /// Hard light effect + /// + /// Backgrop color + /// Source color + /// Opacity applied to Source Alpha + /// Output color + public static TBckPixel HardLightFunction(TBckPixel backdrop, TsrcPixel source, float opacity) + { + Vector4 l = source.ToVector4(); + l.W *= opacity; + if (l.W == 0) + { + return backdrop; + } + + Vector4 b = backdrop.ToVector4(); + + float cr = OverlayValueFunction(l.X, b.X); + float cg = OverlayValueFunction(l.Y, b.Y); + float cb = OverlayValueFunction(l.Z, b.Z); + + return Compose(b, l, Vector4.Min(Vector4.One, new Vector4(cr, cg, cb, 0))); + } + + /// + /// Helper function for Overlay andHardLight modes + /// + /// Backdrop color element + /// Source color element + /// Overlay value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float OverlayValueFunction(float backdrop, float source) + { + return backdrop <= 0.5f ? (2 * backdrop * source) : 1 - ((2 * (1 - source)) * (1 - backdrop)); + } + + /// + /// General composition function for all modes, with a general solution for alpha channel + /// + /// Original backgrop color + /// Original source color + /// Desired transformed color, without taking Alpha channel in account + /// The final color + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TBckPixel Compose(Vector4 backdrop, Vector4 source, Vector4 xform) + { + DebugGuard.MustBeGreaterThan(source.W, 0, nameof(source.W)); + + // calculate weights + float xw = backdrop.W * source.W; + float bw = backdrop.W - xw; + float sw = source.W - xw; + + // calculate final alpha + float a = xw + bw + sw; + + // calculate final value + xform = ((xform * xw) + (backdrop * bw) + (source * sw)) / a; + xform.W = a; + + TBckPixel packed = default(TBckPixel); + packed.PackFromVector4(xform); + + return packed; + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageEffectTest.cs b/tests/ImageSharp.Tests/Drawing/DrawImageEffectTest.cs new file mode 100644 index 0000000000..78f0a08702 --- /dev/null +++ b/tests/ImageSharp.Tests/Drawing/DrawImageEffectTest.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System.IO; + using ImageSharp.PixelFormats; + using Xunit; + + public class DrawImageEffectTest : FileTestBase + { + [Fact] + public void ImageShouldApplyDrawImageFilter() + { + string path = this.CreateOutputDirectory("Drawing", "DrawImageEffect"); + + PixelTransformMode[] modes = (PixelTransformMode[])System.Enum.GetValues(typeof(PixelTransformMode)); + + using (Image blend = TestFile.Create(TestImages.Png.Blur).CreateImage()) + { + foreach (TestFile file in Files) + { + using (Image image = file.CreateImage()) + { + foreach (PixelTransformMode mode in modes) + { + using (FileStream output = File.OpenWrite($"{path}/{mode}.{file.FileName}")) + { + Size size = new Size(image.Width / 2, image.Height / 2); + Point loc = new Point(image.Width / 4, image.Height / 4); + + image.DrawImage(blend, mode, 75, size, loc).Save(output); + } + } + } + } + } + } + } +} \ No newline at end of file