diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/BrushApplicator.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/BrushApplicator.cs index f665e8408..c54666335 100644 --- a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/BrushApplicator.cs +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/BrushApplicator.cs @@ -79,6 +79,10 @@ namespace SixLabors.ImageSharp.Processing.Drawing.Brushes { amountSpan[i] = scanline[i] * this.Options.BlendPercentage; } + else + { + amountSpan[i] = scanline[i]; + } overlaySpan[i] = this[x + i, y]; } diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/ColorStop{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/ColorStop{TPixel}.cs new file mode 100644 index 000000000..298af5cb5 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/ColorStop{TPixel}.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes +{ + /// + /// A struct that defines a single color stop. + /// + /// The pixel format. + [DebuggerDisplay("ColorStop({Ratio} -> {Color}")] + public struct ColorStop + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the struct. + /// + /// Where should it be? 0 is at the start, 1 at the end of the Gradient. + /// What color should be used at that point? + public ColorStop(float ratio, TPixel color) + { + this.Ratio = ratio; + this.Color = color; + } + + /// + /// Gets the point along the defined gradient axis. + /// + public float Ratio { get; } + + /// + /// Gets the color to be used. + /// + public TPixel Color { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/EllipticGradientBrush{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/EllipticGradientBrush{TPixel}.cs new file mode 100644 index 000000000..43f7fe04e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/EllipticGradientBrush{TPixel}.cs @@ -0,0 +1,167 @@ +using System; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes +{ + /// + /// Gradient Brush with elliptic shape. + /// The ellipse is defined by a center point, + /// a point on the longest extension of the ellipse and + /// the ratio between longest and shortest extension. + /// + /// The Pixel format that is used. + public sealed class EllipticGradientBrush : GradientBrushBase + where TPixel : struct, IPixel + { + private readonly Point center; + + private readonly Point referenceAxisEnd; + + private readonly float axisRatio; + + /// + /// The center of the elliptical gradient and 0 for the color stops. + /// The end point of the reference axis of the ellipse. + /// + /// The ratio of the axis widths. + /// The second axis' is perpendicular to the reference axis and + /// it's length is the reference axis' length multiplied by this factor. + /// + /// Defines how the colors of the gradients are repeated. + /// the color stops as defined in base class. + public EllipticGradientBrush( + Point center, + Point referenceAxisEnd, + float axisRatio, + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + : base(repetitionMode, colorStops) + { + this.center = center; + this.referenceAxisEnd = referenceAxisEnd; + this.axisRatio = axisRatio; + } + + /// + public override BrushApplicator CreateApplicator( + ImageFrame source, + RectangleF region, + GraphicsOptions options) => + new RadialGradientBrushApplicator( + source, + options, + this.center, + this.referenceAxisEnd, + this.axisRatio, + this.ColorStops, + this.RepetitionMode); + + /// + private sealed class RadialGradientBrushApplicator : GradientBrushApplicatorBase + { + private readonly Point center; + + private readonly Point referenceAxisEnd; + + private readonly float axisRatio; + + private readonly double rotation; + + private readonly float referenceRadius; + + private readonly float secondRadius; + + private readonly float cosRotation; + + private readonly float sinRotation; + + private readonly float secondRadiusSquared; + + private readonly float referenceRadiusSquared; + + /// + /// Initializes a new instance of the class. + /// + /// The target image + /// The options + /// Center of the ellipse + /// Point on one angular points of the ellipse. + /// + /// Ratio of the axis length's. Used to determine the length of the second axis, + /// the first is defined by and . + /// Definition of colors + /// Defines how the gradient colors are repeated. + public RadialGradientBrushApplicator( + ImageFrame target, + GraphicsOptions options, + Point center, + Point referenceAxisEnd, + float axisRatio, + ColorStop[] colorStops, + GradientRepetitionMode repetitionMode) + : base(target, options, colorStops, repetitionMode) + { + this.center = center; + this.referenceAxisEnd = referenceAxisEnd; + this.axisRatio = axisRatio; + this.rotation = this.AngleBetween( + this.center, + new PointF(this.center.X + 1, this.center.Y), + this.referenceAxisEnd); + this.referenceRadius = this.DistanceBetween(this.center, this.referenceAxisEnd); + this.secondRadius = this.referenceRadius * this.axisRatio; + + this.referenceRadiusSquared = this.referenceRadius * this.referenceRadius; + this.secondRadiusSquared = this.secondRadius * this.secondRadius; + + this.sinRotation = (float)Math.Sin(this.rotation); + this.cosRotation = (float)Math.Cos(this.rotation); + } + + /// + public override void Dispose() + { + } + + /// + protected override float PositionOnGradient(int xt, int yt) + { + float x0 = xt - this.center.X; + float y0 = yt - this.center.Y; + + float x = (x0 * this.cosRotation) - (y0 * this.sinRotation); + float y = (x0 * this.sinRotation) + (y0 * this.cosRotation); + + float xSquared = x * x; + float ySquared = y * y; + + var inBoundaryChecker = (xSquared / this.referenceRadiusSquared) + + (ySquared / this.secondRadiusSquared); + + return inBoundaryChecker; + } + + private float AngleBetween(PointF junction, PointF a, PointF b) + { + var vA = a - junction; + var vB = b - junction; + return (float)(Math.Atan2(vB.Y, vB.X) + - Math.Atan2(vA.Y, vA.X)); + } + + private float DistanceBetween( + PointF p1, + PointF p2) + { + float dX = p1.X - p2.X; + float dXsquared = dX * dX; + + float dY = p1.Y - p2.Y; + float dYsquared = dY * dY; + return (float)Math.Sqrt(dXsquared + dYsquared); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/GradientBrushBase{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/GradientBrushBase{TPixel}.cs new file mode 100644 index 000000000..d0a1ef1c2 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/GradientBrushBase{TPixel}.cs @@ -0,0 +1,174 @@ +using System; +using System.Numerics; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.PixelFormats.PixelBlenders; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes +{ + /// + /// Base class for Gradient brushes + /// + /// The pixel format + public abstract class GradientBrushBase : IBrush + where TPixel : struct, IPixel + { + /// + /// Defines how the colors are repeated beyond the interval [0..1] + /// The gradient colors. + protected GradientBrushBase( + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + { + this.RepetitionMode = repetitionMode; + this.ColorStops = colorStops; + } + + /// + /// Gets how the colors are repeated beyond the interval [0..1]. + /// + protected GradientRepetitionMode RepetitionMode { get; } + + /// + /// Gets the list of color stops for this gradient. + /// + protected ColorStop[] ColorStops { get; } + + /// + public abstract BrushApplicator CreateApplicator( + ImageFrame source, + RectangleF region, + GraphicsOptions options); + + /// + /// Base class for gradient brush applicators + /// + protected abstract class GradientBrushApplicatorBase : BrushApplicator + { + private readonly ColorStop[] colorStops; + + private readonly GradientRepetitionMode repetitionMode; + + /// + /// Initializes a new instance of the class. + /// + /// The target. + /// The options. + /// An array of color stops sorted by their position. + /// Defines if and how the gradient should be repeated. + protected GradientBrushApplicatorBase( + ImageFrame target, + GraphicsOptions options, + ColorStop[] colorStops, + GradientRepetitionMode repetitionMode) + : base(target, options) + { + this.colorStops = colorStops; // TODO: requires colorStops to be sorted by position - should that be checked? + this.repetitionMode = repetitionMode; + } + + /// + /// Base implementation of the indexer for gradients + /// (follows the facade pattern, using abstract methods) + /// + /// X coordinate of the Pixel. + /// Y coordinate of the Pixel. + internal override TPixel this[int x, int y] + { + get + { + float positionOnCompleteGradient = this.PositionOnGradient(x, y); + + switch (this.repetitionMode) + { + case GradientRepetitionMode.None: + // do nothing. The following could be done, but is not necessary: + // onLocalGradient = Math.Min(0, Math.Max(1, onLocalGradient)); + break; + case GradientRepetitionMode.Repeat: + positionOnCompleteGradient = positionOnCompleteGradient % 1; + break; + case GradientRepetitionMode.Reflect: + positionOnCompleteGradient = positionOnCompleteGradient % 2; + if (positionOnCompleteGradient > 1) + { + positionOnCompleteGradient = 2 - positionOnCompleteGradient; + } + + break; + case GradientRepetitionMode.DontFill: + if (positionOnCompleteGradient > 1 || positionOnCompleteGradient < 0) + { + return NamedColors.Transparent; + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var (from, to) = this.GetGradientSegment(positionOnCompleteGradient); + + if (from.Color.Equals(to.Color)) + { + return from.Color; + } + else + { + var fromAsVector = from.Color.ToVector4(); + var toAsVector = to.Color.ToVector4(); + float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / to.Ratio; + + // TODO: this should be changeble for different gradienting functions + Vector4 result = PorterDuffFunctions.Normal( + fromAsVector, + toAsVector, + onLocalGradient); + + TPixel resultColor = default; + resultColor.PackFromVector4(result); + return resultColor; + } + } + } + + /// + /// calculates the position on the gradient for a given pixel. + /// This method is abstract as it's content depends on the shape of the gradient. + /// + /// The x coordinate of the pixel + /// The y coordinate of the pixel + /// + /// The position the given pixel has on the gradient. + /// The position is not bound to the [0..1] interval. + /// Values outside of that interval may be treated differently, + /// e.g. for the enum. + /// + protected abstract float PositionOnGradient(int x, int y); + + private (ColorStop from, ColorStop to) GetGradientSegment( + float positionOnCompleteGradient) + { + var localGradientFrom = this.colorStops[0]; + ColorStop localGradientTo = default; + + // TODO: ensure colorStops has at least 2 items (technically 1 would be okay, but that's no gradient) + foreach (var colorStop in this.colorStops) + { + localGradientTo = colorStop; + + if (colorStop.Ratio > positionOnCompleteGradient) + { + // we're done here, so break it! + break; + } + + localGradientFrom = localGradientTo; + } + + return (localGradientFrom, localGradientTo); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/GradientRepetitionMode.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/GradientRepetitionMode.cs new file mode 100644 index 000000000..adbc26ed4 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/GradientRepetitionMode.cs @@ -0,0 +1,34 @@ +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes +{ + /// + /// Modes to repeat a gradient. + /// + public enum GradientRepetitionMode + { + /// + /// don't repeat, keep the color of start and end beyond those points stable. + /// + None, + + /// + /// Repeat the gradient. + /// If it's a black-white gradient, with Repeat it will be Black->{gray}->White|Black->{gray}->White|... + /// + Repeat, + + /// + /// Reflect the gradient. + /// Similar to , but each other repetition uses inverse order of s. + /// Used on a Black-White gradient, Reflect leads to Black->{gray}->White->{gray}->White... + /// + Reflect, + + /// + /// With DontFill a gradient does not touch any pixel beyond it's borders. + /// For the this is beyond the orthogonal through start and end, + /// TODO For the cref="PolygonalGradientBrush" it's outside the polygon, + /// For and it's beyond 1.0. + /// + DontFill + } +} \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/LinearGradientBrush{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/LinearGradientBrush{TPixel}.cs new file mode 100644 index 000000000..09f816dd9 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/LinearGradientBrush{TPixel}.cs @@ -0,0 +1,152 @@ +using System; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes +{ + /// + /// Provides an implementation of a brush for painting linear gradients within areas. + /// Supported right now: + /// - a set of colors in relative distances to each other. + /// + /// The pixel format + public sealed class LinearGradientBrush : GradientBrushBase + where TPixel : struct, IPixel + { + private readonly Point p1; + + private readonly Point p2; + + /// + /// Initializes a new instance of the class. + /// + /// Start point + /// End point + /// defines how colors are repeated. + /// + public LinearGradientBrush( + Point p1, + Point p2, + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + : base(repetitionMode, colorStops) + { + this.p1 = p1; + this.p2 = p2; + } + + /// + public override BrushApplicator CreateApplicator(ImageFrame source, RectangleF region, GraphicsOptions options) + => new LinearGradientBrushApplicator(source, this.p1, this.p2, this.ColorStops, this.RepetitionMode, options); + + /// + /// The linear gradient brush applicator. + /// + private sealed class LinearGradientBrushApplicator : GradientBrushApplicatorBase + { + private readonly Point start; + + private readonly Point end; + + /// + /// the vector along the gradient, x component + /// + private readonly float alongX; + + /// + /// the vector along the gradient, y component + /// + private readonly float alongY; + + /// + /// the vector perpendicular to the gradient, y component + /// + private readonly float acrossY; + + /// + /// the vector perpendicular to the gradient, x component + /// + private readonly float acrossX; + + /// + /// the result of ^2 + ^2 + /// + private readonly float alongsSquared; + + /// + /// the length of the defined gradient (between source and end) + /// + private readonly float length; + + /// + /// Initializes a new instance of the class. + /// + /// The source + /// start point of the gradient + /// end point of the gradient + /// tuple list of colors and their respective position between 0 and 1 on the line + /// defines how the gradient colors are repeated. + /// the graphics options + public LinearGradientBrushApplicator( + ImageFrame source, + Point start, + Point end, + ColorStop[] colorStops, + GradientRepetitionMode repetitionMode, + GraphicsOptions options) + : base(source, options, colorStops, repetitionMode) + { + this.start = start; + this.end = end; + + // the along vector: + this.alongX = this.end.X - this.start.X; + this.alongY = this.end.Y - this.start.Y; + + // the cross vector: + this.acrossX = this.alongY; + this.acrossY = -this.alongX; + + // some helpers: + this.alongsSquared = (this.alongX * this.alongX) + (this.alongY * this.alongY); + this.length = (float)Math.Sqrt(this.alongsSquared); + } + + protected override float PositionOnGradient(int x, int y) + { + if (this.acrossX == 0) + { + return (x - this.start.X) / (float)(this.end.X - this.start.X); + } + else if (this.acrossY == 0) + { + return (y - this.start.Y) / (float)(this.end.Y - this.start.Y); + } + else + { + float deltaX = x - this.start.X; + float deltaY = y - this.start.Y; + float k = ((this.alongY * deltaX) - (this.alongX * deltaY)) / this.alongsSquared; + + // point on the line: + float x4 = x - (k * this.alongY); + float y4 = y + (k * this.alongX); + + // get distance from (x4,y4) to start + float distance = (float)Math.Sqrt( + Math.Pow(x4 - this.start.X, 2) + + Math.Pow(y4 - this.start.Y, 2)); + + // get and return ratio + float ratio = distance / this.length; + return ratio; + } + } + + public override void Dispose() + { + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/RadialGradientBrush{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/RadialGradientBrush{TPixel}.cs new file mode 100644 index 000000000..5c0d8051c --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Drawing/Brushes/GradientBrushes/RadialGradientBrush{TPixel}.cs @@ -0,0 +1,102 @@ +using System; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes +{ + /// + /// A Circular Gradient Brush, defined by center point and radius. + /// + /// The pixel format. + public sealed class RadialGradientBrush : GradientBrushBase + where TPixel : struct, IPixel + { + private readonly Point center; + + private readonly float radius; + + /// + /// The center of the circular gradient and 0 for the color stops. + /// The radius of the circular gradient and 1 for the color stops. + /// Defines how the colors in the gradient are repeated. + /// the color stops as defined in base class. + public RadialGradientBrush( + Point center, + float radius, + GradientRepetitionMode repetitionMode, + params ColorStop[] colorStops) + : base(repetitionMode, colorStops) + { + this.center = center; + this.radius = radius; + } + + /// + public override BrushApplicator CreateApplicator( + ImageFrame source, + RectangleF region, + GraphicsOptions options) => + new RadialGradientBrushApplicator( + source, + options, + this.center, + this.radius, + this.ColorStops, + this.RepetitionMode); + + /// + private sealed class RadialGradientBrushApplicator : GradientBrushApplicatorBase + { + private readonly Point center; + + private readonly float radius; + + /// + /// Initializes a new instance of the class. + /// + /// The target image + /// The options. + /// Center point of the gradient. + /// Radius of the gradient. + /// Definition of colors. + /// How the colors are repeated beyond the first gradient. + public RadialGradientBrushApplicator( + ImageFrame target, + GraphicsOptions options, + Point center, + float radius, + ColorStop[] colorStops, + GradientRepetitionMode repetitionMode) + : base(target, options, colorStops, repetitionMode) + { + this.center = center; + this.radius = radius; + } + + /// + public override void Dispose() + { + } + + /// + /// As this is a circular gradient, the position on the gradient is based on + /// the distance of the point to the center. + /// + /// The X coordinate of the target pixel. + /// The Y coordinate of the target pixel. + /// the position on the color gradient. + protected override float PositionOnGradient(int x, int y) + { + float distance = (float)Math.Sqrt(Math.Pow(this.center.X - x, 2) + Math.Pow(this.center.Y - y, 2)); + return distance / this.radius; + } + + internal override void Apply(Span scanline, int x, int y) + { + // TODO: each row is symmetric across center, so we can calculate half of it and mirror it to improve performance. + base.Apply(scanline, x, y); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/Drawing/FillPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Drawing/FillPathExtensions.cs index 36eef8d63..4273fd8be 100644 --- a/src/ImageSharp.Drawing/Processing/Drawing/FillPathExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/Drawing/FillPathExtensions.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Processing.Drawing public static class FillPathExtensions { /// - /// Flood fills the image in the shape of the provided polygon with the specified brush.. + /// Flood fills the image in the shape of the provided polygon with the specified brush. /// /// The type of the color. /// The image this method extends. diff --git a/src/ImageSharp/Advanced/IConfigurable.cs b/src/ImageSharp/Advanced/IConfigurable.cs index fd97ae921..38fc83ae1 100644 --- a/src/ImageSharp/Advanced/IConfigurable.cs +++ b/src/ImageSharp/Advanced/IConfigurable.cs @@ -4,7 +4,7 @@ namespace SixLabors.ImageSharp.Advanced { /// - /// Encapsulates the properties for configuration + /// Encapsulates the properties for configuration. /// internal interface IConfigurable { diff --git a/tests/ImageSharp.Tests/Drawing/FillEllipticGradientBrushTest.cs b/tests/ImageSharp.Tests/Drawing/FillEllipticGradientBrushTest.cs new file mode 100644 index 000000000..7c9fa2088 --- /dev/null +++ b/tests/ImageSharp.Tests/Drawing/FillEllipticGradientBrushTest.cs @@ -0,0 +1,149 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Drawing; +using SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Drawing +{ + using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + + [GroupOutput("Drawing/GradientBrushes")] + public class FillEllipticGradientBrushTests + { + public static ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); + + [Theory] + [WithBlankImages(10, 10, PixelTypes.Rgba32)] + public void WithEqualColorsReturnsUnicolorImage( + TestImageProvider provider) + where TPixel : struct, IPixel + { + TPixel red = NamedColors.Red; + + using (Image image = provider.GetImage()) + { + var unicolorLinearGradientBrush = + new EllipticGradientBrush( + new SixLabors.Primitives.Point(0, 0), + new SixLabors.Primitives.Point(10, 0), + 1.0f, + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, red)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + // no need for reference image in this test: + image.ComparePixelBufferTo(red); + } + } + + [Theory] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 1.2)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 1.6)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 2.0)] + public void AxisParallelEllipsesWithDifferentRatio( + TestImageProvider provider, + float ratio) + where TPixel : struct, IPixel + { + TPixel yellow = NamedColors.Yellow; + TPixel red = NamedColors.Red; + TPixel black = NamedColors.Black; + + provider.VerifyOperation( + TolerantComparer, + image => + { + var unicolorLinearGradientBrush = new EllipticGradientBrush( + new SixLabors.Primitives.Point(image.Width / 2, image.Height / 2), + new SixLabors.Primitives.Point(image.Width / 2, (image.Width * 2) / 3), + ratio, + GradientRepetitionMode.None, + new ColorStop(0, yellow), + new ColorStop(1, red), + new ColorStop(1, black)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + }, + $"{ratio:F2}", + false, + false); + } + + [Theory] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1, 0)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4, 0)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8, 0)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0, 0)] + + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1, 45)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4, 45)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8, 45)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0, 45)] + + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1, 90)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4, 90)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8, 90)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0, 90)] + + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.1, 30)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.4, 30)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0.8, 30)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 1.0, 30)] + public void RotatedEllipsesWithDifferentRatio( + TestImageProvider provider, + float ratio, + float rotationInDegree) + where TPixel: struct, IPixel + { + FormattableString variant = $"{ratio:F2}_AT_{rotationInDegree:00}deg"; + + provider.VerifyOperation( + TolerantComparer, + image => + { + TPixel yellow = NamedColors.Yellow; + TPixel red = NamedColors.Red; + TPixel black = NamedColors.Black; + + var center = new SixLabors.Primitives.Point(image.Width / 2, image.Height / 2); + + double rotation = (Math.PI * rotationInDegree) / 180.0; + double cos = Math.Cos(rotation); + double sin = Math.Sin(rotation); + + int offsetY = image.Height / 6; + int axisX = center.X + (int)-(offsetY * sin); + int axisY = center.Y + (int)(offsetY * cos); + + var unicolorLinearGradientBrush = new EllipticGradientBrush( + center, + new SixLabors.Primitives.Point(axisX, axisY), + ratio, + GradientRepetitionMode.None, + new ColorStop(0, yellow), + new ColorStop(1, red), + new ColorStop(1, black)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + }, + variant, + false, + false); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs b/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs new file mode 100644 index 000000000..9e7af1e57 --- /dev/null +++ b/tests/ImageSharp.Tests/Drawing/FillLinearGradientBrushTests.cs @@ -0,0 +1,351 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Globalization; +using System.Linq; +using System.Text; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Drawing; +using SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes; + +using Xunit; +// ReSharper disable InconsistentNaming + +namespace SixLabors.ImageSharp.Tests.Drawing +{ + using SixLabors.ImageSharp.Advanced; + using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + + [GroupOutput("Drawing/GradientBrushes")] + public class FillLinearGradientBrushTests + { + public static ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); + + [Theory] + [WithBlankImages(10, 10, PixelTypes.Rgba32)] + public void WithEqualColorsReturnsUnicolorImage(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + TPixel red = NamedColors.Red; + + var unicolorLinearGradientBrush = new LinearGradientBrush( + new SixLabors.Primitives.Point(0, 0), + new SixLabors.Primitives.Point(10, 0), + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, red)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + // no need for reference image in this test: + image.ComparePixelBufferTo(red); + } + } + + [Theory] + [WithBlankImages(20, 10, PixelTypes.Rgba32)] + [WithBlankImages(20, 10, PixelTypes.Argb32)] + [WithBlankImages(20, 10, PixelTypes.Rgb24)] + public void DoesNotDependOnSinglePixelType(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.VerifyOperation( + TolerantComparer, + image => + { + var unicolorLinearGradientBrush = new LinearGradientBrush( + new SixLabors.Primitives.Point(0, 0), + new SixLabors.Primitives.Point(image.Width, 0), + GradientRepetitionMode.None, + new ColorStop(0, NamedColors.Blue), + new ColorStop(1, NamedColors.Yellow)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + }, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImages(500, 10, PixelTypes.Rgba32)] + public void HorizontalReturnsUnicolorColumns(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.VerifyOperation( + TolerantComparer, + image => + { + TPixel red = NamedColors.Red; + TPixel yellow = NamedColors.Yellow; + + var unicolorLinearGradientBrush = new LinearGradientBrush( + new SixLabors.Primitives.Point(0, 0), + new SixLabors.Primitives.Point(image.Width, 0), + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, yellow)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + }, + false, + false); + } + + [Theory] + [WithBlankImages(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.DontFill)] + [WithBlankImages(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.None)] + [WithBlankImages(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Repeat)] + [WithBlankImages(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Reflect)] + public void HorizontalGradientWithRepMode( + TestImageProvider provider, + GradientRepetitionMode repetitionMode) + where TPixel : struct, IPixel + { + provider.VerifyOperation( + TolerantComparer, + image => + { + TPixel red = NamedColors.Red; + TPixel yellow = NamedColors.Yellow; + + var unicolorLinearGradientBrush = new LinearGradientBrush( + new SixLabors.Primitives.Point(0, 0), + new SixLabors.Primitives.Point(image.Width / 10, 0), + repetitionMode, + new ColorStop(0, red), + new ColorStop(1, yellow)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + }, + $"{repetitionMode}", + false, + false); + } + + [Theory] + [WithBlankImages(200, 100, PixelTypes.Rgba32, new[] { 0.5f })] + [WithBlankImages(200, 100, PixelTypes.Rgba32, new[] { 0.2f, 0.4f, 0.6f, 0.8f })] + [WithBlankImages(200, 100, PixelTypes.Rgba32, new[] { 0.1f, 0.3f, 0.6f })] + public void WithDoubledStopsProduceDashedPatterns( + TestImageProvider provider, + float[] pattern) + where TPixel : struct, IPixel + { + string variant = string.Join("_", pattern.Select(i => i.ToString(CultureInfo.InvariantCulture))); + + // ensure the input data is valid + Assert.True(pattern.Length > 0); + + TPixel black = NamedColors.Black; + TPixel white = NamedColors.White; + + // create the input pattern: 0, followed by each of the arguments twice, followed by 1.0 - toggling black and white. + ColorStop[] colorStops = + Enumerable.Repeat(new ColorStop(0, black), 1) + .Concat( + pattern + .SelectMany((f, index) => new[] + { + new ColorStop(f, index % 2 == 0 ? black : white), + new ColorStop(f, index % 2 == 0 ? white : black) + })) + .Concat(Enumerable.Repeat(new ColorStop(1, pattern.Length % 2 == 0 ? black : white), 1)) + .ToArray(); + + using (Image image = provider.GetImage()) + { + var unicolorLinearGradientBrush = + new LinearGradientBrush( + new SixLabors.Primitives.Point(0, 0), + new SixLabors.Primitives.Point(image.Width, 0), + GradientRepetitionMode.None, + colorStops); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + + image.DebugSave( + provider, + variant, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + // the result must be a black and white pattern, no other color should occur: + Assert.All( + Enumerable.Range(0, image.Width).Select(i => image[i, 0]), + color => Assert.True(color.Equals(black) || color.Equals(white))); + + image.CompareToReferenceOutput( + TolerantComparer, + provider, + variant, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + } + + [Theory] + [WithBlankImages(10, 500, PixelTypes.Rgba32)] + public void VerticalBrushReturnsUnicolorRows( + TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.VerifyOperation( + image => + { + TPixel red = NamedColors.Red; + TPixel yellow = NamedColors.Yellow; + + var unicolorLinearGradientBrush = new LinearGradientBrush( + new SixLabors.Primitives.Point(0, 0), + new SixLabors.Primitives.Point(0, image.Height), + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, yellow)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + + VerifyAllRowsAreUnicolor(image); + }, + false, + false); + + void VerifyAllRowsAreUnicolor(Image image) + { + for (int y = 0; y < image.Height; y++) + { + Span row = image.GetPixelRowSpan(y); + TPixel firstColorOfRow = row[0]; + foreach (TPixel p in row) + { + Assert.Equal(firstColorOfRow, p); + } + } + } + } + + public enum ImageCorner + { + TopLeft = 0, + TopRight = 1, + BottomLeft = 2, + BottomRight = 3 + } + + [Theory] + [WithBlankImages(200, 200, PixelTypes.Rgba32, ImageCorner.TopLeft)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, ImageCorner.TopRight)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, ImageCorner.BottomLeft)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, ImageCorner.BottomRight)] + public void DiagonalReturnsCorrectImages( + TestImageProvider provider, + ImageCorner startCorner) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + Assert.True(image.Height == image.Width, "For the math check block at the end the image must be squared, but it is not."); + + int startX = (int)startCorner % 2 == 0 ? 0 : image.Width - 1; + int startY = startCorner > ImageCorner.TopRight ? 0 : image.Height - 1; + int endX = image.Height - startX - 1; + int endY = image.Width - startY - 1; + + TPixel red = NamedColors.Red; + TPixel yellow = NamedColors.Yellow; + + var unicolorLinearGradientBrush = + new LinearGradientBrush( + new SixLabors.Primitives.Point(startX, startY), + new SixLabors.Primitives.Point(endX, endY), + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, yellow)); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + image.DebugSave( + provider, + startCorner, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + int verticalSign = startY == 0 ? 1 : -1; + int horizontalSign = startX == 0 ? 1 : -1; + + // check first and last pixel, these are known: + Assert.Equal(red, image[startX, startY]); + Assert.Equal(yellow, image[endX, endY]); + + for (int i = 0; i < image.Height; i++) + { + // it's diagonal, so for any (a, a) on the gradient line, for all (a-x, b+x) - +/- depending on the diagonal direction - must be the same color) + TPixel colorOnDiagonal = image[i, i]; + int orthoCount = 0; + for (int offset = -orthoCount; offset < orthoCount; offset++) + { + Assert.Equal(colorOnDiagonal, image[i + horizontalSign * offset, i + verticalSign * offset]); + } + } + + image.CompareToReferenceOutput( + TolerantComparer, + provider, + startCorner, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + } + + [Theory] + [WithBlankImages(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .2f, .5f, .9f }, new[] { 0, 0, 1, 1 })] + [WithBlankImages(500, 500, PixelTypes.Rgba32, 0, 499, 499, 0, new[] { 0f, 0.2f, 0.5f, 0.9f }, new[] { 0, 1, 2, 3 })] + [WithBlankImages(500, 500, PixelTypes.Rgba32, 499, 499, 0, 0, new[] { 0f, 0.7f, 0.8f, 0.9f}, new[] { 0, 1, 2, 0 })] + [WithBlankImages(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .5f, 1f}, new[]{0, 1, 3})] + public void ArbitraryGradients( + TestImageProvider provider, + int startX, int startY, + int endX, int endY, + float[] stopPositions, + int[] stopColorCodes) + where TPixel : struct, IPixel + { + TPixel[] colors = { + NamedColors.Navy, NamedColors.LightGreen, NamedColors.Yellow, + NamedColors.Red + }; + + var coloringVariant = new StringBuilder(); + ColorStop[] colorStops = new ColorStop[stopPositions.Length]; + for (int i = 0; i < stopPositions.Length; i++) + { + TPixel color = colors[stopColorCodes[i % colors.Length]]; + float position = stopPositions[i]; + + colorStops[i] = new ColorStop(position, color); + coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color, position); + } + + FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; + + provider.VerifyOperation( + image => + { + var unicolorLinearGradientBrush = new LinearGradientBrush( + new SixLabors.Primitives.Point(startX, startY), + new SixLabors.Primitives.Point(endX, endY), + GradientRepetitionMode.None, + colorStops); + + image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); + }, + variant, + false, + false); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Drawing/FillRadialGradientBrushTests.cs b/tests/ImageSharp.Tests/Drawing/FillRadialGradientBrushTests.cs new file mode 100644 index 000000000..eafbf3df1 --- /dev/null +++ b/tests/ImageSharp.Tests/Drawing/FillRadialGradientBrushTests.cs @@ -0,0 +1,76 @@ +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Drawing; +using SixLabors.ImageSharp.Processing.Drawing.Brushes.GradientBrushes; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Drawing +{ + using System; + + using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + + [GroupOutput("Drawing/GradientBrushes")] + public class FillRadialGradientBrushTests + { + public static ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); + + [Theory] + [WithBlankImages(200, 200, PixelTypes.Rgba32)] + public void WithEqualColorsReturnsUnicolorImage( + TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + TPixel red = NamedColors.Red; + + var unicolorRadialGradientBrush = + new RadialGradientBrush( + new SixLabors.Primitives.Point(0, 0), + 100, + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, red)); + + image.Mutate(x => x.Fill(unicolorRadialGradientBrush)); + + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + // no need for reference image in this test: + image.ComparePixelBufferTo(red); + } + } + + [Theory] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 100, 100)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0, 0)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 100, 0)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, 0, 100)] + [WithBlankImages(200, 200, PixelTypes.Rgba32, -40, 100)] + public void WithDifferentCentersReturnsImage( + TestImageProvider provider, + int centerX, + int centerY) + where TPixel : struct, IPixel + { + provider.VerifyOperation( + TolerantComparer, + image => + { + var brush = new RadialGradientBrush( + new SixLabors.Primitives.Point(centerX, centerY), + image.Width / 2f, + GradientRepetitionMode.None, + new ColorStop(0, NamedColors.Red), + new ColorStop(1, NamedColors.Yellow)); + + image.Mutate(x => x.Fill(brush)); + }, + $"center({centerX},{centerY})", + false, + false); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index 2177551af..bfd120fff 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -92,7 +92,7 @@ namespace SixLabors.ImageSharp.Tests details = '_' + details; } - return $"{this.GetTestOutputDir()}/{this.TestName}{pixName}{fn}{details}{extension}"; + return TestUtils.AsInvariantString($"{this.GetTestOutputDir()}/{this.TestName}{pixName}{fn}{details}{extension}"); } /// diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 34c93a7c1..b8c0489c8 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -173,7 +173,8 @@ namespace SixLabors.ImageSharp.Tests FormattableString testOutputDetails, string extension = "png", bool grayscale = false, - bool appendPixelTypeToFileName = true) + bool appendPixelTypeToFileName = true, + bool appendSourceFileOrDescription = true) where TPixel : struct, IPixel { return image.CompareToReferenceOutput( @@ -181,7 +182,8 @@ namespace SixLabors.ImageSharp.Tests (object)testOutputDetails, extension, grayscale, - appendPixelTypeToFileName); + appendPixelTypeToFileName, + appendSourceFileOrDescription); } /// @@ -195,6 +197,7 @@ namespace SixLabors.ImageSharp.Tests /// The extension /// A boolean indicating whether we should debug save + compare against a grayscale image, smaller in size. /// A boolean indicating whether to append the pixel type to the output file name. + /// A boolean indicating whether to append to the test output file name. /// public static Image CompareToReferenceOutput( this Image image, @@ -202,7 +205,8 @@ namespace SixLabors.ImageSharp.Tests object testOutputDetails = null, string extension = "png", bool grayscale = false, - bool appendPixelTypeToFileName = true) + bool appendPixelTypeToFileName = true, + bool appendSourceFileOrDescription = true) where TPixel : struct, IPixel { return CompareToReferenceOutput( @@ -212,7 +216,8 @@ namespace SixLabors.ImageSharp.Tests testOutputDetails, extension, grayscale, - appendPixelTypeToFileName); + appendPixelTypeToFileName, + appendSourceFileOrDescription); } public static Image CompareToReferenceOutput( @@ -246,6 +251,7 @@ namespace SixLabors.ImageSharp.Tests /// The extension /// A boolean indicating whether we should debug save + compare against a grayscale image, smaller in size. /// A boolean indicating whether to append the pixel type to the output file name. + /// A boolean indicating whether to append to the test output file name. /// public static Image CompareToReferenceOutput( this Image image, @@ -254,14 +260,16 @@ namespace SixLabors.ImageSharp.Tests object testOutputDetails = null, string extension = "png", bool grayscale = false, - bool appendPixelTypeToFileName = true) + bool appendPixelTypeToFileName = true, + bool appendSourceFileOrDescription = true) where TPixel : struct, IPixel { using (Image referenceImage = GetReferenceOutputImage( provider, testOutputDetails, extension, - appendPixelTypeToFileName)) + appendPixelTypeToFileName, + appendSourceFileOrDescription)) { comparer.VerifySimilarity(referenceImage, image); } @@ -442,6 +450,9 @@ namespace SixLabors.ImageSharp.Tests return image; } + /// + /// All pixels in all frames should be exactly equal to 'expectedPixel'. + /// public static Image ComparePixelBufferTo(this Image image, TPixel expectedPixel) where TPixel : struct, IPixel { @@ -453,6 +464,9 @@ namespace SixLabors.ImageSharp.Tests return image; } + /// + /// All pixels in the frame should be exactly equal to 'expectedPixel'. + /// public static ImageFrame ComparePixelBufferTo(this ImageFrame imageFrame, TPixel expectedPixel) where TPixel : struct, IPixel { @@ -465,7 +479,7 @@ namespace SixLabors.ImageSharp.Tests return imageFrame; } - + public static ImageFrame ComparePixelBufferTo( this ImageFrame image, Span expectedPixels) @@ -522,22 +536,120 @@ namespace SixLabors.ImageSharp.Tests return image; } + /// + /// Utility method for doing the following in one step: + /// 1. Executing an operation (taken as a delegate) + /// 2. Executing DebugSave() + /// 3. Executing CopareToReferenceOutput() + /// + internal static void VerifyOperation( + this TestImageProvider provider, + ImageComparer comparer, + Action> operation, + FormattableString testOutputDetails, + bool appendPixelTypeToFileName = true, + bool appendSourceFileOrDescription = true) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + operation(image); + + image.DebugSave( + provider, + testOutputDetails, + appendPixelTypeToFileName: appendPixelTypeToFileName, + appendSourceFileOrDescription: appendSourceFileOrDescription); + + image.CompareToReferenceOutput(comparer, + provider, + testOutputDetails, + appendPixelTypeToFileName: appendPixelTypeToFileName, + appendSourceFileOrDescription: appendSourceFileOrDescription); + } + } + + /// + /// Utility method for doing the following in one step: + /// 1. Executing an operation (taken as a delegate) + /// 2. Executing DebugSave() + /// 3. Executing CopareToReferenceOutput() + /// + internal static void VerifyOperation( + this TestImageProvider provider, + Action> operation, + FormattableString testOutputDetails, + bool appendPixelTypeToFileName = true, + bool appendSourceFileOrDescription = true) + where TPixel : struct, IPixel + { + provider.VerifyOperation( + ImageComparer.Tolerant(), + operation, + testOutputDetails, + appendPixelTypeToFileName, + appendSourceFileOrDescription); + } + + /// + /// Utility method for doing the following in one step: + /// 1. Executing an operation (taken as a delegate) + /// 2. Executing DebugSave() + /// 3. Executing CopareToReferenceOutput() + /// + internal static void VerifyOperation( + this TestImageProvider provider, + ImageComparer comparer, + Action> operation, + bool appendPixelTypeToFileName = true, + bool appendSourceFileOrDescription = true) + where TPixel : struct, IPixel + { + provider.VerifyOperation( + comparer, + operation, + $"", + appendPixelTypeToFileName, + appendSourceFileOrDescription); + } + + /// + /// Utility method for doing the following in one step: + /// 1. Executing an operation (taken as a delegate) + /// 2. Executing DebugSave() + /// 3. Executing CopareToReferenceOutput() + /// + internal static void VerifyOperation( + this TestImageProvider provider, + Action> operation, + bool appendPixelTypeToFileName = true, + bool appendSourceFileOrDescription = true) + where TPixel : struct, IPixel + { + provider.VerifyOperation(operation, $"", appendPixelTypeToFileName, appendSourceFileOrDescription); + } + /// /// Loads the expected image with a reference decoder + compares it to . /// Also performs a debug save using . /// - internal static void VerifyEncoder(this Image image, - ITestImageProvider provider, - string extension, - object testOutputDetails, - IImageEncoder encoder, - ImageComparer customComparer = null, - bool appendPixelTypeToFileName = true, - string referenceImageExtension = null - ) + internal static void VerifyEncoder( + this Image image, + ITestImageProvider provider, + string extension, + object testOutputDetails, + IImageEncoder encoder, + ImageComparer customComparer = null, + bool appendPixelTypeToFileName = true, + string referenceImageExtension = null) where TPixel : struct, IPixel { - string actualOutputFile = provider.Utility.SaveTestOutputFile(image, extension, encoder, testOutputDetails, appendPixelTypeToFileName); + string actualOutputFile = provider.Utility.SaveTestOutputFile( + image, + extension, + encoder, + testOutputDetails, + appendPixelTypeToFileName); IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); using (var actualImage = Image.Load(actualOutputFile, referenceDecoder)) diff --git a/tests/Images/External b/tests/Images/External index f641620eb..8ab54f800 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit f641620eb5378db49d6153bbf1443ad13bda2379 +Subproject commit 8ab54f8003aff94b3a9662b0be46b0062cad6b74