diff --git a/README.md b/README.md index a420c07c8e..66dc0dcf43 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,9 @@ Our API is designed to be simple to consume. Here's an example of the code requi On platforms supporting netstandard 1.3+ ```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + // Image.Load(string path) is a shortcut for our default type. // Other pixel formats use Image.Load(string path)) using (Image image = Image.Load("foo.jpg")) @@ -83,6 +86,9 @@ using (Image image = Image.Load("foo.jpg")) On netstandard 1.1 - 1.2 ```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + // Image.Load(Stream stream) is a shortcut for our default type. // Other pixel formats use Image.Load(Stream stream)) using (FileStream stream = File.OpenRead("foo.jpg")) @@ -99,6 +105,9 @@ using (Image image = Image.Load(stream)) Setting individual pixel values can be performed as follows: ```csharp +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + // Individual pixels using (Image image = new Image(400, 400)) { diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs index 4e6018e07a..dc73420f30 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs @@ -1,95 +1,101 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors.Drawing -{ - /// - /// Combines two images together by blending the pixels. - /// +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Drawing +{ + /// + /// Combines two images together by blending the pixels. + /// /// The pixel format of destination image. - /// The pixel format os source image. - internal class DrawImageProcessor : ImageProcessor + /// The pixel format of source image. + internal class DrawImageProcessor : ImageProcessor where TPixelDst : struct, IPixel - where TPixelSrc : struct, IPixel + where TPixelSrc : struct, IPixel { - /// - /// Initializes a new instance of the class. - /// - /// The image to blend with the currently processing image. + /// + /// Initializes a new instance of the class. + /// + /// The image to blend with the currently processing image. /// The location to draw the blended image. /// The blending mode to use when drawing the image. /// The Alpha blending mode to use when drawing the image. - /// The opacity of the image to blend. Must be between 0 and 1. - public DrawImageProcessor(Image image, Point location, PixelColorBlendingMode colorBlendingMode, PixelAlphaCompositionMode alphaCompositionMode, float opacity) - { - Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); - - this.Image = image; - this.Opacity = opacity; - this.Blender = PixelOperations.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode); - this.Location = location; - } - - /// - /// Gets the image to blend - /// - public Image Image { get; } - - /// - /// Gets the opacity of the image to blend - /// - public float Opacity { get; } - - /// - /// Gets the pixel blender - /// - public PixelBlender Blender { get; } - - /// - /// Gets the location to draw the blended image - /// - public Point Location { get; } - - /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - Image targetImage = this.Image; - PixelBlender blender = this.Blender; - int locationY = this.Location.Y; - - // Align start/end positions. - Rectangle bounds = targetImage.Bounds(); - - int minX = Math.Max(this.Location.X, sourceRectangle.X); - int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width); - int targetX = minX - this.Location.X; - - int minY = Math.Max(this.Location.Y, sourceRectangle.Y); - int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom); - - int width = maxX - minX; - + /// The opacity of the image to blend. Must be between 0 and 1. + public DrawImageProcessor(Image image, Point location, PixelColorBlendingMode colorBlendingMode, PixelAlphaCompositionMode alphaCompositionMode, float opacity) + { + Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); + + this.Image = image; + this.Opacity = opacity; + this.Blender = PixelOperations.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode); + this.Location = location; + } + + /// + /// Gets the image to blend + /// + public Image Image { get; } + + /// + /// Gets the opacity of the image to blend + /// + public float Opacity { get; } + + /// + /// Gets the pixel blender + /// + public PixelBlender Blender { get; } + + /// + /// Gets the location to draw the blended image + /// + public Point Location { get; } + + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + Image targetImage = this.Image; + PixelBlender blender = this.Blender; + int locationY = this.Location.Y; + + // Align start/end positions. + Rectangle bounds = targetImage.Bounds(); + + int minX = Math.Max(this.Location.X, sourceRectangle.X); + int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width); + int targetX = minX - this.Location.X; + + int minY = Math.Max(this.Location.Y, sourceRectangle.Y); + int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom); + + int width = maxX - minX; + MemoryAllocator memoryAllocator = this.Image.GetConfiguration().MemoryAllocator; - ParallelFor.WithConfiguration( - minY, - maxY, - configuration, - y => - { - Span background = source.GetPixelRowSpan(y).Slice(minX, width); - Span foreground = targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width); - blender.Blend(memoryAllocator, background, background, foreground, this.Opacity); - }); - } - } + var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); + + ParallelHelper.IterateRows( + workingRect, + configuration, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span background = source.GetPixelRowSpan(y).Slice(minX, width); + Span foreground = + targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width); + blender.Blend(memoryAllocator, background, background, foreground, this.Opacity); + } + }); + } + } } \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs index 3285e75a7b..ed6c869511 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs @@ -1,107 +1,116 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors.Drawing -{ - /// - /// Using the brush as a source of pixels colors blends the brush color with source. - /// - /// The pixel format. - internal class FillProcessor : ImageProcessor - where TPixel : struct, IPixel - { - /// - /// The brush. - /// - private readonly IBrush brush; - private readonly GraphicsOptions options; - - /// - /// Initializes a new instance of the class. - /// - /// The brush to source pixel colors from. - /// The options - public FillProcessor(IBrush brush, GraphicsOptions options) - { - this.brush = brush; - this.options = options; - } - - /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - int startX = sourceRectangle.X; - int endX = sourceRectangle.Right; - int startY = sourceRectangle.Y; - int endY = sourceRectangle.Bottom; - - // Align start/end positions. - int minX = Math.Max(0, startX); - int maxX = Math.Min(source.Width, endX); - int minY = Math.Max(0, startY); - int maxY = Math.Min(source.Height, endY); - - int width = maxX - minX; - - // If there's no reason for blending, then avoid it. - if (this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush)) - { - ParallelFor.WithConfiguration( - minY, - maxY, - configuration, - y => - { - source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color); - }); - } - else - { - // Reset offset if necessary. - if (minX > 0) - { - startX = 0; - } - - if (minY > 0) - { - startY = 0; - } - - using (IMemoryOwner amount = source.MemoryAllocator.Allocate(width)) - using (BrushApplicator applicator = this.brush.CreateApplicator( - source, - sourceRectangle, - this.options)) - { - amount.GetSpan().Fill(1f); - - ParallelFor.WithConfiguration( - minY, - maxY, - configuration, - y => - { - int offsetY = y - startY; - int offsetX = minX - startX; - - applicator.Apply(amount.GetSpan(), offsetX, offsetY); - }); - } - } - } - - private bool IsSolidBrushWithoutBlending(out SolidBrush solidBrush) - { +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Drawing +{ + /// + /// Using the brush as a source of pixels colors blends the brush color with source. + /// + /// The pixel format. + internal class FillProcessor : ImageProcessor + where TPixel : struct, IPixel + { + /// + /// The brush. + /// + private readonly IBrush brush; + private readonly GraphicsOptions options; + + /// + /// Initializes a new instance of the class. + /// + /// The brush to source pixel colors from. + /// The options + public FillProcessor(IBrush brush, GraphicsOptions options) + { + this.brush = brush; + this.options = options; + } + + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + int startX = sourceRectangle.X; + int endX = sourceRectangle.Right; + int startY = sourceRectangle.Y; + int endY = sourceRectangle.Bottom; + + // Align start/end positions. + int minX = Math.Max(0, startX); + int maxX = Math.Min(source.Width, endX); + int minY = Math.Max(0, startY); + int maxY = Math.Min(source.Height, endY); + + int width = maxX - minX; + + var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); + + // If there's no reason for blending, then avoid it. + if (this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush)) + { + ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4); + + ParallelHelper.IterateRows( + workingRect, + parallelSettings, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color); + } + }); + } + else + { + // Reset offset if necessary. + if (minX > 0) + { + startX = 0; + } + + if (minY > 0) + { + startY = 0; + } + + using (IMemoryOwner amount = source.MemoryAllocator.Allocate(width)) + using (BrushApplicator applicator = this.brush.CreateApplicator( + source, + sourceRectangle, + this.options)) + { + amount.GetSpan().Fill(1f); + + ParallelHelper.IterateRows( + workingRect, + configuration, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + int offsetY = y - startY; + int offsetX = minX - startX; + + applicator.Apply(amount.GetSpan(), offsetX, offsetY); + } + }); + } + } + } + + private bool IsSolidBrushWithoutBlending(out SolidBrush solidBrush) + { solidBrush = this.brush as SolidBrush; if (solidBrush == null) @@ -109,7 +118,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing return false; } - return this.options.IsOpaqueColorWithoutBlending(solidBrush.Color); - } - } + return this.options.IsOpaqueColorWithoutBlending(solidBrush.Color); + } + } } \ No newline at end of file diff --git a/src/ImageSharp/Common/Extensions/EncoderExtensions.cs b/src/ImageSharp/Common/Extensions/EncoderExtensions.cs new file mode 100644 index 0000000000..e6b800e86a --- /dev/null +++ b/src/ImageSharp/Common/Extensions/EncoderExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +#if !NETCOREAPP2_1 +using System; +using System.Text; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + internal static unsafe class EncoderExtensions + { + /// + /// Gets a string from the provided buffer data. + /// + /// The encoding. + /// The buffer. + /// The string. + public static string GetString(this Encoding encoding, ReadOnlySpan buffer) + { +#if NETSTANDARD1_1 + return encoding.GetString(buffer.ToArray()); +#else + fixed (byte* bytes = buffer) + { + return encoding.GetString(bytes, buffer.Length); + } +#endif + } + } +} +#endif \ No newline at end of file diff --git a/src/ImageSharp/Common/Helpers/ParallelFor.cs b/src/ImageSharp/Common/Helpers/ParallelFor.cs deleted file mode 100644 index 4c14bb6e3d..0000000000 --- a/src/ImageSharp/Common/Helpers/ParallelFor.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Buffers; -using System.Threading.Tasks; -using SixLabors.Memory; - -namespace SixLabors.ImageSharp -{ - /// - /// Utility methods for Parallel.For() execution. Use this instead of raw calls! - /// - internal static class ParallelFor - { - /// - /// Helper method to execute Parallel.For using the settings in - /// - public static void WithConfiguration(int fromInclusive, int toExclusive, Configuration configuration, Action body) - { - Parallel.For(fromInclusive, toExclusive, configuration.GetParallelOptions(), body); - } - - /// - /// Helper method to execute Parallel.For with temporary worker buffer shared between executing tasks. - /// The buffer is not guaranteed to be clean! - /// - /// The value type of the buffer - /// The start index, inclusive. - /// The end index, exclusive. - /// The used for getting the and - /// The length of the requested parallel buffer - /// The delegate that is invoked once per iteration. - public static void WithTemporaryBuffer( - int fromInclusive, - int toExclusive, - Configuration configuration, - int bufferLength, - Action> body) - where T : struct - { - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - ParallelOptions parallelOptions = configuration.GetParallelOptions(); - - IMemoryOwner InitBuffer() - { - return memoryAllocator.Allocate(bufferLength); - } - - void CleanUpBuffer(IMemoryOwner buffer) - { - buffer.Dispose(); - } - - IMemoryOwner BodyFunc(int i, ParallelLoopState state, IMemoryOwner buffer) - { - body(i, buffer); - return buffer; - } - - Parallel.For(fromInclusive, toExclusive, parallelOptions, InitBuffer, BodyFunc, CleanUpBuffer); - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Common/ParallelUtils/ParallelExecutionSettings.cs b/src/ImageSharp/Common/ParallelUtils/ParallelExecutionSettings.cs new file mode 100644 index 0000000000..0b45719c38 --- /dev/null +++ b/src/ImageSharp/Common/ParallelUtils/ParallelExecutionSettings.cs @@ -0,0 +1,71 @@ +// Copyright(c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Threading.Tasks; + +using SixLabors.Memory; + +namespace SixLabors.ImageSharp.ParallelUtils +{ + /// + /// Defines execution settings for methods in . + /// + internal readonly struct ParallelExecutionSettings + { + /// + /// Default value for . + /// + public const int DefaultMinimumPixelsProcessedPerTask = 4096; + + /// + /// Initializes a new instance of the struct. + /// + public ParallelExecutionSettings( + int maxDegreeOfParallelism, + int minimumPixelsProcessedPerTask, + MemoryAllocator memoryAllocator) + { + this.MaxDegreeOfParallelism = maxDegreeOfParallelism; + this.MinimumPixelsProcessedPerTask = minimumPixelsProcessedPerTask; + this.MemoryAllocator = memoryAllocator; + } + + /// + /// Initializes a new instance of the struct. + /// + public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator) + : this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator) + { + } + + /// + /// Gets the MemoryAllocator + /// + public MemoryAllocator MemoryAllocator { get; } + + /// + /// Gets the value used for initializing when using TPL. + /// + public int MaxDegreeOfParallelism { get; } + + /// + /// Gets the minimum number of pixels being processed by a single task when parallelizing operations with TPL. + /// Launching tasks for pixel regions below this limit is not worth the overhead. + /// Initialized with by default, + /// the optimum value is operation specific. (The cheaper the operation, the larger the value is.) + /// + public int MinimumPixelsProcessedPerTask { get; } + + /// + /// Creates a new instance of + /// having multiplied by + /// + public ParallelExecutionSettings MultiplyMinimumPixelsPerTask(int multiplier) + { + return new ParallelExecutionSettings( + this.MaxDegreeOfParallelism, + this.MinimumPixelsProcessedPerTask * multiplier, + this.MemoryAllocator); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Common/ParallelUtils/ParallelHelper.cs b/src/ImageSharp/Common/ParallelUtils/ParallelHelper.cs new file mode 100644 index 0000000000..1d1734a863 --- /dev/null +++ b/src/ImageSharp/Common/ParallelUtils/ParallelHelper.cs @@ -0,0 +1,146 @@ +// Copyright(c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +using SixLabors.ImageSharp.Memory; +using SixLabors.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.ParallelUtils +{ + /// + /// Utility methods for batched processing of pixel row intervals. + /// Parallel execution is optimized for image processing. + /// Use this instead of direct calls! + /// + internal static class ParallelHelper + { + /// + /// Get the default for a + /// + public static ParallelExecutionSettings GetParallelSettings(this Configuration configuration) + { + return new ParallelExecutionSettings(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches defined by -s. + /// + public static void IterateRows(Rectangle rectangle, Configuration configuration, Action body) + { + ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings(); + + IterateRows(rectangle, parallelSettings, body); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches defined by -s. + /// + public static void IterateRows( + Rectangle rectangle, + in ParallelExecutionSettings parallelSettings, + Action body) + { + DebugGuard.MustBeGreaterThan(rectangle.Width, 0, nameof(rectangle)); + + int maxSteps = DivideCeil(rectangle.Width * rectangle.Height, parallelSettings.MinimumPixelsProcessedPerTask); + + int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps); + + // Avoid TPL overhead in this trivial case: + if (numOfSteps == 1) + { + var rows = new RowInterval(rectangle.Top, rectangle.Bottom); + body(rows); + return; + } + + int verticalStep = DivideCeil(rectangle.Height, numOfSteps); + + var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = numOfSteps }; + + Parallel.For( + 0, + numOfSteps, + parallelOptions, + i => + { + int yMin = rectangle.Top + (i * verticalStep); + int yMax = Math.Min(yMin + verticalStep, rectangle.Bottom); + + var rows = new RowInterval(yMin, yMax); + body(rows); + }); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches defined by -s + /// instantiating a temporary buffer for each invocation. + /// + public static void IterateRowsWithTempBuffer( + Rectangle rectangle, + in ParallelExecutionSettings parallelSettings, + Action> body) + where T : struct + { + int maxSteps = DivideCeil(rectangle.Width * rectangle.Height, parallelSettings.MinimumPixelsProcessedPerTask); + + int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps); + + MemoryAllocator memoryAllocator = parallelSettings.MemoryAllocator; + + // Avoid TPL overhead in this trivial case: + if (numOfSteps == 1) + { + var rows = new RowInterval(rectangle.Top, rectangle.Bottom); + using (IMemoryOwner buffer = memoryAllocator.Allocate(rectangle.Width)) + { + body(rows, buffer.Memory); + } + + return; + } + + int verticalStep = DivideCeil(rectangle.Height, numOfSteps); + + var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = numOfSteps }; + + Parallel.For( + 0, + numOfSteps, + parallelOptions, + i => + { + int yMin = rectangle.Top + (i * verticalStep); + int yMax = Math.Min(yMin + verticalStep, rectangle.Bottom); + + var rows = new RowInterval(yMin, yMax); + + using (IMemoryOwner buffer = memoryAllocator.Allocate(rectangle.Width)) + { + body(rows, buffer.Memory); + } + }); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches defined by -s + /// instantiating a temporary buffer for each invocation. + /// + public static void IterateRowsWithTempBuffer( + Rectangle rectangle, + Configuration configuration, + Action> body) + where T : struct + { + IterateRowsWithTempBuffer(rectangle, configuration.GetParallelSettings(), body); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 1b009bfedd..576f7bf3d0 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -59,6 +59,7 @@ namespace SixLabors.ImageSharp /// /// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms /// configured with this instance. + /// Initialized with by default. /// public int MaxDegreeOfParallelism { @@ -125,7 +126,7 @@ namespace SixLabors.ImageSharp /// Creates a shallow copy of the /// /// A new configuration instance - public Configuration ShallowCopy() + public Configuration Clone() { return new Configuration { diff --git a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs index 3d678c13e1..8b33e30fa6 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs @@ -6,13 +6,29 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Provides Bmp specific metadata information for the image. /// - public class BmpMetaData + public class BmpMetaData : IDeepCloneable { + /// + /// Initializes a new instance of the class. + /// + public BmpMetaData() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private BmpMetaData(BmpMetaData other) => this.BitsPerPixel = other.BitsPerPixel; + /// /// Gets or sets the number of bits per pixel. /// public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24; + /// + public IDeepCloneable DeepClone() => new BmpMetaData(this); + // TODO: Colors used once we support encoding palette bmps. } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs b/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs index cc04d48314..0042c6a108 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs @@ -6,8 +6,26 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Provides Gif specific metadata information for the image frame. /// - public class GifFrameMetaData + public class GifFrameMetaData : IDeepCloneable { + /// + /// Initializes a new instance of the class. + /// + public GifFrameMetaData() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private GifFrameMetaData(GifFrameMetaData other) + { + this.ColorTableLength = other.ColorTableLength; + this.FrameDelay = other.FrameDelay; + this.DisposalMethod = other.DisposalMethod; + } + /// /// Gets or sets the length of the color table for paletted images. /// If not 0, then this field indicates the maximum number of colors to use when quantizing the @@ -29,5 +47,8 @@ namespace SixLabors.ImageSharp.Formats.Gif /// be treated after being displayed. /// public GifDisposalMethod DisposalMethod { get; set; } + + /// + public IDeepCloneable DeepClone() => new GifFrameMetaData(this); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifMetaData.cs b/src/ImageSharp/Formats/Gif/GifMetaData.cs index f58f5dff3e..bb7fb50518 100644 --- a/src/ImageSharp/Formats/Gif/GifMetaData.cs +++ b/src/ImageSharp/Formats/Gif/GifMetaData.cs @@ -6,8 +6,26 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Provides Gif specific metadata information for the image. /// - public class GifMetaData + public class GifMetaData : IDeepCloneable { + /// + /// Initializes a new instance of the class. + /// + public GifMetaData() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private GifMetaData(GifMetaData other) + { + this.RepeatCount = other.RepeatCount; + this.ColorTableMode = other.ColorTableMode; + this.GlobalColorTableLength = other.GlobalColorTableLength; + } + /// /// Gets or sets the number of times any animation is repeated. /// @@ -25,5 +43,8 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Gets or sets the length of the global color table if present. /// public int GlobalColorTableLength { get; set; } + + /// + public IDeepCloneable DeepClone() => new GifMetaData(this); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs b/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs index bd7232e602..fcad29e5d0 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs @@ -6,11 +6,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Provides Jpeg specific metadata information for the image. /// - public class JpegMetaData + public class JpegMetaData : IDeepCloneable { + /// + /// Initializes a new instance of the class. + /// + public JpegMetaData() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private JpegMetaData(JpegMetaData other) => this.Quality = other.Quality; + /// /// Gets or sets the encoded quality. /// public int Quality { get; set; } = 75; + + /// + public IDeepCloneable DeepClone() => new JpegMetaData(this); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/Adam7.cs b/src/ImageSharp/Formats/Png/Adam7.cs new file mode 100644 index 0000000000..4e6485b55f --- /dev/null +++ b/src/ImageSharp/Formats/Png/Adam7.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Png +{ + /// + /// Constants and helper methods for the Adam7 interlacing algorithm. + /// + internal static class Adam7 + { + /// + /// The amount to increment when processing each column per scanline for each interlaced pass. + /// + public static readonly int[] ColumnIncrement = { 8, 8, 4, 4, 2, 2, 1 }; + + /// + /// The index to start at when processing each column per scanline for each interlaced pass. + /// + public static readonly int[] FirstColumn = { 0, 4, 0, 2, 0, 1, 0 }; + + /// + /// The index to start at when processing each row per scanline for each interlaced pass. + /// + public static readonly int[] FirstRow = { 0, 0, 4, 0, 2, 0, 1 }; + + /// + /// The amount to increment when processing each row per scanline for each interlaced pass. + /// + public static readonly int[] RowIncrement = { 8, 8, 8, 4, 4, 2, 2 }; + + /// + /// Returns the correct number of columns for each interlaced pass. + /// + /// The line width. + /// The current pass index. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ComputeColumns(int width, int passIndex) + { + switch (passIndex) + { + case 0: return (width + 7) / 8; + case 1: return (width + 3) / 8; + case 2: return (width + 3) / 4; + case 3: return (width + 1) / 4; + case 4: return (width + 1) / 2; + case 5: return width / 2; + case 6: return width; + default: throw new ArgumentException($"Not a valid pass index: {passIndex}"); + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs b/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs new file mode 100644 index 0000000000..07fc688d50 --- /dev/null +++ b/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs @@ -0,0 +1,107 @@ +using System; +using System.Buffers.Binary; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.MetaData; + +namespace SixLabors.ImageSharp.Formats.Png.Chunks +{ + /// + /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. + /// + internal readonly struct PhysicalChunkData + { + public const int Size = 9; + + public PhysicalChunkData(uint x, uint y, byte unitSpecifier) + { + this.XAxisPixelsPerUnit = x; + this.YAxisPixelsPerUnit = y; + this.UnitSpecifier = unitSpecifier; + } + + /// + /// Gets the number of pixels per unit on the X axis. + /// + public uint XAxisPixelsPerUnit { get; } + + /// + /// Gets the number of pixels per unit on the Y axis. + /// + public uint YAxisPixelsPerUnit { get; } + + /// + /// Gets the unit specifier. + /// 0: unit is unknown + /// 1: unit is the meter + /// When the unit specifier is 0, the pHYs chunk defines pixel aspect ratio only; the actual size of the pixels remains unspecified. + /// + public byte UnitSpecifier { get; } + + /// + /// Parses the PhysicalChunkData from the given buffer. + /// + /// The data buffer. + /// The parsed PhysicalChunkData. + public static PhysicalChunkData Parse(ReadOnlySpan data) + { + uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(0, 4)); + uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4)); + byte unit = data[8]; + + return new PhysicalChunkData(hResolution, vResolution, unit); + } + + /// + /// Constructs the PngPhysicalChunkData from the provided metadata. + /// If the resolution units are not in meters, they are automatically convereted. + /// + /// The metadata. + /// The constructed PngPhysicalChunkData instance. + public static PhysicalChunkData FromMetadata(ImageMetaData meta) + { + byte unitSpecifier = 0; + uint x; + uint y; + + switch (meta.ResolutionUnits) + { + case PixelResolutionUnit.AspectRatio: + unitSpecifier = 0; // Unspecified + x = (uint)Math.Round(meta.HorizontalResolution); + y = (uint)Math.Round(meta.VerticalResolution); + break; + + case PixelResolutionUnit.PixelsPerInch: + unitSpecifier = 1; // Per meter + x = (uint)Math.Round(UnitConverter.InchToMeter(meta.HorizontalResolution)); + y = (uint)Math.Round(UnitConverter.InchToMeter(meta.VerticalResolution)); + break; + + case PixelResolutionUnit.PixelsPerCentimeter: + unitSpecifier = 1; // Per meter + x = (uint)Math.Round(UnitConverter.CmToMeter(meta.HorizontalResolution)); + y = (uint)Math.Round(UnitConverter.CmToMeter(meta.VerticalResolution)); + break; + + default: + unitSpecifier = 1; // Per meter + x = (uint)Math.Round(meta.HorizontalResolution); + y = (uint)Math.Round(meta.VerticalResolution); + break; + } + + return new PhysicalChunkData(x, y, unitSpecifier); + } + + /// + /// Writes the data to the given buffer. + /// + /// The buffer. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(0, 4), this.XAxisPixelsPerUnit); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), this.YAxisPixelsPerUnit); + buffer[8] = this.UnitSpecifier; + } + } +} diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index ffcf9b0f30..bc5a54e8b9 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -30,7 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) int x = 1; - for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) { + for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) + { ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); byte above = Unsafe.Add(ref prevBaseRef, x); scan = (byte)(scan + (above >> 1)); @@ -68,7 +69,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters resultBaseRef = 3; int x = 0; - for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) { + for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) + { byte scan = Unsafe.Add(ref scanBaseRef, x); byte above = Unsafe.Add(ref prevBaseRef, x); ++x; @@ -77,7 +79,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters sum += ImageMaths.FastAbs(unchecked((sbyte)res)); } - for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) { + for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) + { byte scan = Unsafe.Add(ref scanBaseRef, x); byte left = Unsafe.Add(ref scanBaseRef, xLeft); byte above = Unsafe.Add(ref prevBaseRef, x); @@ -97,9 +100,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters /// The above byte /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Average(byte left, byte above) - { - return (left + above) >> 1; - } + private static int Average(byte left, byte above) => (left + above) >> 1; } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index 0d3df079c9..4ffc39bdbd 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -72,7 +72,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters resultBaseRef = 4; int x = 0; - for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) { + for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) + { byte scan = Unsafe.Add(ref scanBaseRef, x); byte above = Unsafe.Add(ref prevBaseRef, x); ++x; @@ -81,7 +82,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters sum += ImageMaths.FastAbs(unchecked((sbyte)res)); } - for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) { + for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) + { byte scan = Unsafe.Add(ref scanBaseRef, x); byte left = Unsafe.Add(ref scanBaseRef, xLeft); byte above = Unsafe.Add(ref prevBaseRef, x); diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs index cfb7781be4..6af5f0b648 100644 --- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -59,7 +59,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters resultBaseRef = 1; int x = 0; - for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) { + for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) + { byte scan = Unsafe.Add(ref scanBaseRef, x); ++x; ref byte res = ref Unsafe.Add(ref resultBaseRef, x); @@ -67,7 +68,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters sum += ImageMaths.FastAbs(unchecked((sbyte)res)); } - for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) { + for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) + { byte scan = Unsafe.Add(ref scanBaseRef, x); byte prev = Unsafe.Add(ref scanBaseRef, xLeft); ++x; diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index 77bc9f7a05..7e5a9fa6b8 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Gets the filter method. /// - PngFilterMethod FilterMethod { get; } + PngFilterMethod? FilterMethod { get; } /// /// Gets the compression level 1-9. diff --git a/src/ImageSharp/Formats/Png/PngBitDepth.cs b/src/ImageSharp/Formats/Png/PngBitDepth.cs index 396f2c1608..0321b532ab 100644 --- a/src/ImageSharp/Formats/Png/PngBitDepth.cs +++ b/src/ImageSharp/Formats/Png/PngBitDepth.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Provides enumeration for the available PNG bit depths. /// - public enum PngBitDepth + public enum PngBitDepth : byte { /// /// 1 bit per sample or per palette index (not per pixel). diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index ff25e26b7a..48c866f671 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/src/ImageSharp/Formats/Png/PngConstants.cs @@ -41,5 +41,17 @@ namespace SixLabors.ImageSharp.Formats.Png /// The header bytes as a big endian coded ulong. /// public const ulong HeaderValue = 0x89504E470D0A1A0AUL; + + /// + /// The dictionary of available color types. + /// + public static readonly Dictionary ColorTypes = new Dictionary() + { + [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 }, + [PngColorType.Rgb] = new byte[] { 8, 16 }, + [PngColorType.Palette] = new byte[] { 1, 2, 4, 8 }, + [PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 }, + [PngColorType.RgbWithAlpha] = new byte[] { 8, 16 } + }; } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 9281927012..8401f4e98f 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -3,13 +3,12 @@ using System; using System.Buffers.Binary; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Memory; @@ -26,51 +25,9 @@ namespace SixLabors.ImageSharp.Formats.Png internal sealed class PngDecoderCore { /// - /// The dictionary of available color types. + /// Reusable buffer. /// - private static readonly Dictionary ColorTypes = new Dictionary() - { - [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 }, - [PngColorType.Rgb] = new byte[] { 8, 16 }, - [PngColorType.Palette] = new byte[] { 1, 2, 4, 8 }, - [PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 }, - [PngColorType.RgbWithAlpha] = new byte[] { 8, 16 } - }; - - /// - /// The amount to increment when processing each column per scanline for each interlaced pass - /// - private static readonly int[] Adam7ColumnIncrement = { 8, 8, 4, 4, 2, 2, 1 }; - - /// - /// The index to start at when processing each column per scanline for each interlaced pass - /// - private static readonly int[] Adam7FirstColumn = { 0, 4, 0, 2, 0, 1, 0 }; - - /// - /// The index to start at when processing each row per scanline for each interlaced pass - /// - private static readonly int[] Adam7FirstRow = { 0, 0, 4, 0, 2, 0, 1 }; - - /// - /// The amount to increment when processing each row per scanline for each interlaced pass - /// - private static readonly int[] Adam7RowIncrement = { 8, 8, 8, 4, 4, 2, 2 }; - - /// - /// Reusable buffer for reading chunk types. - /// - private readonly byte[] chunkTypeBuffer = new byte[4]; - - /// - /// Reusable buffer for reading chunk lengths. - /// - private readonly byte[] chunkLengthBuffer = new byte[4]; - - /// - /// Reusable buffer for reading crc values. - /// - private readonly byte[] crcBuffer = new byte[4]; + private readonly byte[] buffer = new byte[4]; /// /// Reusable crc for validating chunks. @@ -92,6 +49,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// private readonly bool ignoreMetadata; + /// + /// Used the manage memory allocations. + /// + private readonly MemoryAllocator memoryAllocator; + /// /// The stream to decode from. /// @@ -145,7 +107,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The index of the current scanline being processed /// - private int currentRow = Adam7FirstRow[0]; + private int currentRow = Adam7.FirstRow[0]; /// /// The current pass for an interlaced PNG @@ -187,6 +149,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// private bool hasTrans; + /// + /// The next chunk of data to return + /// + private PngChunk? nextChunk; + /// /// Initializes a new instance of the class. /// @@ -195,12 +162,11 @@ namespace SixLabors.ImageSharp.Formats.Png public PngDecoderCore(Configuration configuration, IPngDecoderOptions options) { this.configuration = configuration ?? Configuration.Default; + this.memoryAllocator = this.configuration.MemoryAllocator; this.textEncoding = options.TextEncoding ?? PngConstants.DefaultEncoding; this.ignoreMetadata = options.IgnoreMetadata; } - private MemoryAllocator MemoryAllocator => this.configuration.MemoryAllocator; - /// /// Decodes the stream to the image. /// @@ -223,67 +189,66 @@ namespace SixLabors.ImageSharp.Formats.Png Image image = null; try { - using (var deframeStream = new ZlibInflateStream(this.currentStream)) + while (!this.isEndChunkReached && this.TryReadChunk(out PngChunk chunk)) { - while (!this.isEndChunkReached && this.TryReadChunk(out PngChunk chunk)) + try { - try + switch (chunk.Type) { - switch (chunk.Type) - { - case PngChunkType.Header: - this.ReadHeaderChunk(pngMetaData, chunk.Data.Array); - this.ValidateHeader(); - break; - case PngChunkType.Physical: - this.ReadPhysicalChunk(metaData, chunk.Data.GetSpan()); - break; - case PngChunkType.Gamma: - this.ReadGammaChunk(pngMetaData, chunk.Data.GetSpan()); - break; - case PngChunkType.Data: - if (image is null) - { - this.InitializeImage(metaData, out image); - } + case PngChunkType.Header: + this.ReadHeaderChunk(pngMetaData, chunk.Data.Array); + break; + case PngChunkType.Physical: + this.ReadPhysicalChunk(metaData, chunk.Data.GetSpan()); + break; + case PngChunkType.Gamma: + this.ReadGammaChunk(pngMetaData, chunk.Data.GetSpan()); + break; + case PngChunkType.Data: + if (image is null) + { + this.InitializeImage(metaData, out image); + } + using (var deframeStream = new ZlibInflateStream(this.currentStream, this.ReadNextDataChunk)) + { deframeStream.AllocateNewBytes(chunk.Length); this.ReadScanlines(deframeStream.CompressedStream, image.Frames.RootFrame); - this.currentStream.Read(this.crcBuffer, 0, 4); - break; - case PngChunkType.Palette: - byte[] pal = new byte[chunk.Length]; - Buffer.BlockCopy(chunk.Data.Array, 0, pal, 0, chunk.Length); - this.palette = pal; - break; - case PngChunkType.PaletteAlpha: - byte[] alpha = new byte[chunk.Length]; - Buffer.BlockCopy(chunk.Data.Array, 0, alpha, 0, chunk.Length); - this.paletteAlpha = alpha; - this.AssignTransparentMarkers(alpha); - break; - case PngChunkType.Text: - this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length); - break; - case PngChunkType.Exif: - if (!this.ignoreMetadata) - { - byte[] exifData = new byte[chunk.Length]; - Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length); - metaData.ExifProfile = new ExifProfile(exifData); - } - - break; - case PngChunkType.End: - this.isEndChunkReached = true; - break; - } - } - finally - { - chunk.Data?.Dispose(); // Data is rented in ReadChunkData() + } + + break; + case PngChunkType.Palette: + byte[] pal = new byte[chunk.Length]; + Buffer.BlockCopy(chunk.Data.Array, 0, pal, 0, chunk.Length); + this.palette = pal; + break; + case PngChunkType.PaletteAlpha: + byte[] alpha = new byte[chunk.Length]; + Buffer.BlockCopy(chunk.Data.Array, 0, alpha, 0, chunk.Length); + this.paletteAlpha = alpha; + this.AssignTransparentMarkers(alpha); + break; + case PngChunkType.Text: + this.ReadTextChunk(metaData, chunk.Data.Array.AsSpan(0, chunk.Length)); + break; + case PngChunkType.Exif: + if (!this.ignoreMetadata) + { + byte[] exifData = new byte[chunk.Length]; + Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length); + metaData.ExifProfile = new ExifProfile(exifData); + } + + break; + case PngChunkType.End: + this.isEndChunkReached = true; + break; } } + finally + { + chunk.Data?.Dispose(); // Data is rented in ReadChunkData() + } } if (image is null) @@ -320,7 +285,6 @@ namespace SixLabors.ImageSharp.Formats.Png { case PngChunkType.Header: this.ReadHeaderChunk(pngMetaData, chunk.Data.Array); - this.ValidateHeader(); break; case PngChunkType.Physical: this.ReadPhysicalChunk(metaData, chunk.Data.GetSpan()); @@ -332,7 +296,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.SkipChunkDataAndCrc(chunk); break; case PngChunkType.Text: - this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length); + this.ReadTextChunk(metaData, chunk.Data.Array.AsSpan(0, chunk.Length)); break; case PngChunkType.End: this.isEndChunkReached = true; @@ -386,19 +350,19 @@ namespace SixLabors.ImageSharp.Formats.Png return false; } - buffer = this.MemoryAllocator.AllocateManagedByteBuffer(bytesPerScanline * 8 / bits, AllocationOptions.Clean); - byte[] result = buffer.Array; + buffer = this.memoryAllocator.AllocateManagedByteBuffer(bytesPerScanline * 8 / bits, AllocationOptions.Clean); + ref byte sourceRef = ref MemoryMarshal.GetReference(source); + ref byte resultRef = ref buffer.Array[0]; int mask = 0xFF >> (8 - bits); int resultOffset = 0; - for (int i = 0; i < bytesPerScanline - 1; i++) + for (int i = 0; i < bytesPerScanline; i++) { - byte b = source[i]; + byte b = Unsafe.Add(ref sourceRef, i); for (int shift = 0; shift < 8; shift += bits) { int colorIndex = (b >> (8 - bits - shift)) & mask; - result[resultOffset] = (byte)colorIndex; - + Unsafe.Add(ref resultRef, resultOffset) = (byte)colorIndex; resultOffset++; } } @@ -413,26 +377,14 @@ namespace SixLabors.ImageSharp.Formats.Png /// The data containing physical data. private void ReadPhysicalChunk(ImageMetaData metadata, ReadOnlySpan data) { - // The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. It contains: - // Pixels per unit, X axis: 4 bytes (unsigned integer) - // Pixels per unit, Y axis: 4 bytes (unsigned integer) - // Unit specifier: 1 byte - // - // The following values are legal for the unit specifier: - // 0: unit is unknown - // 1: unit is the meter - // - // When the unit specifier is 0, the pHYs chunk defines pixel aspect ratio only; the actual size of the pixels remains unspecified. - int hResolution = BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)); - int vResolution = BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)); - byte unit = data[8]; - - metadata.ResolutionUnits = unit == byte.MinValue + var physicalChunk = PhysicalChunkData.Parse(data); + + metadata.ResolutionUnits = physicalChunk.UnitSpecifier == byte.MinValue ? PixelResolutionUnit.AspectRatio : PixelResolutionUnit.PixelsPerMeter; - metadata.HorizontalResolution = hResolution; - metadata.VerticalResolution = vResolution; + metadata.HorizontalResolution = physicalChunk.XAxisPixelsPerUnit; + metadata.VerticalResolution = physicalChunk.YAxisPixelsPerUnit; } /// @@ -465,7 +417,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.bytesPerSample = this.header.BitDepth / 8; } - this.previousScanline = this.MemoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean); + this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean); this.scanline = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean); } @@ -584,22 +536,18 @@ namespace SixLabors.ImageSharp.Formats.Png break; case FilterType.Sub: - SubFilter.Decode(scanlineSpan, this.bytesPerPixel); break; case FilterType.Up: - UpFilter.Decode(scanlineSpan, this.previousScanline.GetSpan()); break; case FilterType.Average: - AverageFilter.Decode(scanlineSpan, this.previousScanline.GetSpan(), this.bytesPerPixel); break; case FilterType.Paeth: - PaethFilter.Decode(scanlineSpan, this.previousScanline.GetSpan(), this.bytesPerPixel); break; @@ -607,7 +555,7 @@ namespace SixLabors.ImageSharp.Formats.Png throw new ImageFormatException("Unknown filter type."); } - this.ProcessDefilteredScanline(this.scanline.Array, image); + this.ProcessDefilteredScanline(scanlineSpan, image); this.SwapBuffers(); this.currentRow++; @@ -626,7 +574,7 @@ namespace SixLabors.ImageSharp.Formats.Png { while (true) { - int numColumns = this.ComputeColumnsAdam7(this.pass); + int numColumns = Adam7.ComputeColumns(this.header.Width, this.pass); if (numColumns == 0) { @@ -658,22 +606,18 @@ namespace SixLabors.ImageSharp.Formats.Png break; case FilterType.Sub: - SubFilter.Decode(scanSpan, this.bytesPerPixel); break; case FilterType.Up: - UpFilter.Decode(scanSpan, prevSpan); break; case FilterType.Average: - AverageFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel); break; case FilterType.Paeth: - PaethFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel); break; @@ -682,11 +626,11 @@ namespace SixLabors.ImageSharp.Formats.Png } Span rowSpan = image.GetPixelRowSpan(this.currentRow); - this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, Adam7FirstColumn[this.pass], Adam7ColumnIncrement[this.pass]); + this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, Adam7.FirstColumn[this.pass], Adam7.ColumnIncrement[this.pass]); this.SwapBuffers(); - this.currentRow += Adam7RowIncrement[this.pass]; + this.currentRow += Adam7.RowIncrement[this.pass]; } this.pass++; @@ -694,7 +638,7 @@ namespace SixLabors.ImageSharp.Formats.Png if (this.pass < 7) { - this.currentRow = Adam7FirstRow[this.pass]; + this.currentRow = Adam7.FirstRow[this.pass]; } else { @@ -713,211 +657,69 @@ namespace SixLabors.ImageSharp.Formats.Png private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels) where TPixel : struct, IPixel { - TPixel pixel = default; Span rowSpan = pixels.GetPixelRowSpan(this.currentRow); // Trim the first marker byte from the buffer ReadOnlySpan trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1); // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. - ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(trimmed, this.bytesPerScanline, this.header.BitDepth, out IManagedByteBuffer buffer) + ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(trimmed, this.bytesPerScanline - 1, this.header.BitDepth, out IManagedByteBuffer buffer) ? buffer.GetSpan() : trimmed; switch (this.pngColorType) { case PngColorType.Grayscale: - - int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1); - - if (!this.hasTrans) - { - if (this.header.BitDepth == 16) - { - Rgb48 rgb48 = default; - for (int x = 0, o = 0; x < this.header.Width; x++, o += 2) - { - ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgb48.R = luminance; - rgb48.G = luminance; - rgb48.B = luminance; - pixel.PackFromRgb48(rgb48); - rowSpan[x] = pixel; - } - } - else - { - // TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method. - var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue); - for (int x = 0; x < this.header.Width; x++) - { - byte luminance = (byte)(scanlineSpan[x] * factor); - rgba32.R = luminance; - rgba32.G = luminance; - rgba32.B = luminance; - pixel.PackFromRgba32(rgba32); - rowSpan[x] = pixel; - } - } - } - else - { - if (this.header.BitDepth == 16) - { - Rgba64 rgba64 = default; - for (int x = 0, o = 0; x < this.header.Width; x++, o += 2) - { - ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgba64.R = luminance; - rgba64.G = luminance; - rgba64.B = luminance; - rgba64.A = luminance.Equals(this.luminance16Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.PackFromRgba64(rgba64); - rowSpan[x] = pixel; - } - } - else - { - Rgba32 rgba32 = default; - for (int x = 0; x < this.header.Width; x++) - { - byte luminance = (byte)(scanlineSpan[x] * factor); - rgba32.R = luminance; - rgba32.G = luminance; - rgba32.B = luminance; - rgba32.A = luminance.Equals(this.luminanceTrans) ? byte.MinValue : byte.MaxValue; - - pixel.PackFromRgba32(rgba32); - rowSpan[x] = pixel; - } - } - } + PngScanlineProcessor.ProcessGrayscaleScanline( + this.header, + scanlineSpan, + rowSpan, + this.hasTrans, + this.luminance16Trans, + this.luminanceTrans); break; case PngColorType.GrayscaleWithAlpha: - - if (this.header.BitDepth == 16) - { - Rgba64 rgba64 = default; - for (int x = 0, o = 0; x < this.header.Width; x++, o += 4) - { - ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); - rgba64.R = luminance; - rgba64.G = luminance; - rgba64.B = luminance; - rgba64.A = alpha; - - pixel.PackFromRgba64(rgba64); - rowSpan[x] = pixel; - } - } - else - { - Rgba32 rgba32 = default; - for (int x = 0; x < this.header.Width; x++) - { - int offset = x * this.bytesPerPixel; - byte luminance = scanlineSpan[offset]; - byte alpha = scanlineSpan[offset + this.bytesPerSample]; - - rgba32.R = luminance; - rgba32.G = luminance; - rgba32.B = luminance; - rgba32.A = alpha; - - pixel.PackFromRgba32(rgba32); - rowSpan[x] = pixel; - } - } + PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline( + this.header, + scanlineSpan, + rowSpan, + this.bytesPerPixel, + this.bytesPerSample); break; case PngColorType.Palette: - - this.ProcessScanlineFromPalette(scanlineSpan, rowSpan); + PngScanlineProcessor.ProcessPaletteScanline( + this.header, + scanlineSpan, + rowSpan, + this.palette, + this.paletteAlpha); break; case PngColorType.Rgb: - - if (!this.hasTrans) - { - if (this.header.BitDepth == 16) - { - Rgb48 rgb48 = default; - for (int x = 0, o = 0; x < this.header.Width; x++, o += 6) - { - rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); - rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2)); - pixel.PackFromRgb48(rgb48); - rowSpan[x] = pixel; - } - } - else - { - PixelOperations.Instance.PackFromRgb24Bytes(scanlineSpan, rowSpan, this.header.Width); - } - } - else - { - if (this.header.BitDepth == 16) - { - Rgb48 rgb48 = default; - Rgba64 rgba64 = default; - for (int x = 0, o = 0; x < this.header.Width; x++, o += 6) - { - rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); - rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2)); - - rgba64.Rgb = rgb48; - rgba64.A = rgb48.Equals(this.rgb48Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.PackFromRgba64(rgba64); - rowSpan[x] = pixel; - } - } - else - { - ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan); - for (int x = 0; x < this.header.Width; x++) - { - ref readonly Rgb24 rgb24 = ref rgb24Span[x]; - Rgba32 rgba32 = default; - rgba32.Rgb = rgb24; - rgba32.A = rgb24.Equals(this.rgb24Trans) ? byte.MinValue : byte.MaxValue; - - pixel.PackFromRgba32(rgba32); - rowSpan[x] = pixel; - } - } - } + PngScanlineProcessor.ProcessRgbScanline( + this.header, + scanlineSpan, + rowSpan, + this.bytesPerPixel, + this.bytesPerSample, + this.hasTrans, + this.rgb48Trans, + this.rgb24Trans); break; case PngColorType.RgbWithAlpha: - - if (this.header.BitDepth == 16) - { - Rgba64 rgba64 = default; - for (int x = 0, o = 0; x < this.header.Width; x++, o += 8) - { - rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); - rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2)); - rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 6, 2)); - pixel.PackFromRgba64(rgba64); - rowSpan[x] = pixel; - } - } - else - { - PixelOperations.Instance.PackFromRgba32Bytes(scanlineSpan, rowSpan, this.header.Width); - } + PngScanlineProcessor.ProcessRgbaScanline( + this.header, + scanlineSpan, + rowSpan, + this.bytesPerPixel, + this.bytesPerSample); break; } @@ -936,8 +738,6 @@ namespace SixLabors.ImageSharp.Formats.Png private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defilteredScanline, Span rowSpan, int pixelOffset = 0, int increment = 1) where TPixel : struct, IPixel { - TPixel pixel = default; - // Trim the first marker byte from the buffer ReadOnlySpan trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1); @@ -949,245 +749,66 @@ namespace SixLabors.ImageSharp.Formats.Png switch (this.pngColorType) { case PngColorType.Grayscale: - - int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1); - - if (!this.hasTrans) - { - if (this.header.BitDepth == 16) - { - Rgb48 rgb48 = default; - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 2) - { - ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgb48.R = luminance; - rgb48.G = luminance; - rgb48.B = luminance; - - pixel.PackFromRgb48(rgb48); - rowSpan[x] = pixel; - } - } - else - { - // TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method. - var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue); - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o++) - { - byte luminance = (byte)(scanlineSpan[o] * factor); - rgba32.R = luminance; - rgba32.G = luminance; - rgba32.B = luminance; - - pixel.PackFromRgba32(rgba32); - rowSpan[x] = pixel; - } - } - } - else - { - if (this.header.BitDepth == 16) - { - Rgba64 rgba64 = default; - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 2) - { - ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgba64.R = luminance; - rgba64.G = luminance; - rgba64.B = luminance; - rgba64.A = luminance.Equals(this.luminance16Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.PackFromRgba64(rgba64); - rowSpan[x] = pixel; - } - } - else - { - Rgba32 rgba32 = default; - for (int x = pixelOffset; x < this.header.Width; x += increment) - { - byte luminance = (byte)(scanlineSpan[x] * factor); - rgba32.R = luminance; - rgba32.G = luminance; - rgba32.B = luminance; - rgba32.A = luminance.Equals(this.luminanceTrans) ? byte.MinValue : byte.MaxValue; - - pixel.PackFromRgba32(rgba32); - rowSpan[x] = pixel; - } - } - } + PngScanlineProcessor.ProcessInterlacedGrayscaleScanline( + this.header, + scanlineSpan, + rowSpan, + pixelOffset, + increment, + this.hasTrans, + this.luminance16Trans, + this.luminanceTrans); break; case PngColorType.GrayscaleWithAlpha: - - if (this.header.BitDepth == 16) - { - Rgba64 rgba64 = default; - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 4) - { - ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); - rgba64.R = luminance; - rgba64.G = luminance; - rgba64.B = luminance; - rgba64.A = alpha; - - pixel.PackFromRgba64(rgba64); - rowSpan[x] = pixel; - } - } - else - { - Rgba32 rgba32 = default; - for (int x = pixelOffset; x < this.header.Width; x += increment) - { - int offset = x * this.bytesPerPixel; - byte luminance = scanlineSpan[offset]; - byte alpha = scanlineSpan[offset + this.bytesPerSample]; - rgba32.R = luminance; - rgba32.G = luminance; - rgba32.B = luminance; - rgba32.A = alpha; - - pixel.PackFromRgba32(rgba32); - rowSpan[x] = pixel; - } - } + PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline( + this.header, + scanlineSpan, + rowSpan, + pixelOffset, + increment, + this.bytesPerPixel, + this.bytesPerSample); break; case PngColorType.Palette: - - Span palettePixels = MemoryMarshal.Cast(this.palette); - - if (this.paletteAlpha?.Length > 0) - { - // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha - // channel and we should try to read it. - Rgba32 rgba = default; - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o++) - { - int index = scanlineSpan[o]; - rgba.A = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : byte.MaxValue; - rgba.Rgb = palettePixels[index]; - - pixel.PackFromRgba32(rgba); - rowSpan[x] = pixel; - } - } - else - { - var rgba = new Rgba32(0, 0, 0, byte.MaxValue); - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o++) - { - int index = scanlineSpan[o]; - rgba.Rgb = palettePixels[index]; - - pixel.PackFromRgba32(rgba); - rowSpan[x] = pixel; - } - } + PngScanlineProcessor.ProcessInterlacedPaletteScanline( + this.header, + scanlineSpan, + rowSpan, + pixelOffset, + increment, + this.palette, + this.paletteAlpha); break; case PngColorType.Rgb: - - if (this.header.BitDepth == 16) - { - if (this.hasTrans) - { - Rgb48 rgb48 = default; - Rgba64 rgba64 = default; - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 6) - { - rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); - rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2)); - - rgba64.Rgb = rgb48; - rgba64.A = rgb48.Equals(this.rgb48Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.PackFromRgba64(rgba64); - rowSpan[x] = pixel; - } - } - else - { - Rgb48 rgb48 = default; - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 6) - { - rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); - rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2)); - pixel.PackFromRgb48(rgb48); - rowSpan[x] = pixel; - } - } - } - else - { - if (this.hasTrans) - { - Rgba32 rgba = default; - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += this.bytesPerPixel) - { - rgba.R = scanlineSpan[o]; - rgba.G = scanlineSpan[o + this.bytesPerSample]; - rgba.B = scanlineSpan[o + (2 * this.bytesPerSample)]; - rgba.A = this.rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue; - - pixel.PackFromRgba32(rgba); - rowSpan[x] = pixel; - } - } - else - { - var rgba = new Rgba32(0, 0, 0, byte.MaxValue); - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += this.bytesPerPixel) - { - rgba.R = scanlineSpan[o]; - rgba.G = scanlineSpan[o + this.bytesPerSample]; - rgba.B = scanlineSpan[o + (2 * this.bytesPerSample)]; - - pixel.PackFromRgba32(rgba); - rowSpan[x] = pixel; - } - } - } + PngScanlineProcessor.ProcessInterlacedRgbScanline( + this.header, + scanlineSpan, + rowSpan, + pixelOffset, + increment, + this.bytesPerPixel, + this.bytesPerSample, + this.hasTrans, + this.rgb48Trans, + this.rgb24Trans); break; case PngColorType.RgbWithAlpha: - - if (this.header.BitDepth == 16) - { - Rgba64 rgba64 = default; - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 8) - { - rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); - rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); - rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2)); - rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 6, 2)); - pixel.PackFromRgba64(rgba64); - rowSpan[x] = pixel; - } - } - else - { - Rgba32 rgba = default; - for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += this.bytesPerPixel) - { - rgba.R = scanlineSpan[o]; - rgba.G = scanlineSpan[o + this.bytesPerSample]; - rgba.B = scanlineSpan[o + (2 * this.bytesPerSample)]; - rgba.A = scanlineSpan[o + (3 * this.bytesPerSample)]; - - pixel.PackFromRgba32(rgba); - rowSpan[x] = pixel; - } - } + PngScanlineProcessor.ProcessInterlacedRgbaScanline( + this.header, + scanlineSpan, + rowSpan, + pixelOffset, + increment, + this.bytesPerPixel, + this.bytesPerSample); break; } @@ -1210,6 +831,7 @@ namespace SixLabors.ImageSharp.Formats.Png ushort rc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(0, 2)); ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2)); ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); + this.rgb48Trans = new Rgb48(rc, gc, bc); this.hasTrans = true; return; @@ -1240,50 +862,6 @@ namespace SixLabors.ImageSharp.Formats.Png } } - /// - /// Processes a scanline that uses a palette - /// - /// The type of pixel we are expanding to - /// The defiltered scanline - /// The current output image row - private void ProcessScanlineFromPalette(ReadOnlySpan scanline, Span row) - where TPixel : struct, IPixel - { - ReadOnlySpan palettePixels = MemoryMarshal.Cast(this.palette); - var color = default(TPixel); - - if (this.paletteAlpha?.Length > 0) - { - Rgba32 rgba = default; - - // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha - // channel and we should try to read it. - for (int x = 0; x < this.header.Width; x++) - { - int index = scanline[x]; - rgba.A = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : byte.MaxValue; - rgba.Rgb = palettePixels[index]; - - color.PackFromRgba32(rgba); - row[x] = color; - } - } - else - { - // TODO: We should have PackFromRgb24. - var rgba = new Rgba32(0, 0, 0, byte.MaxValue); - for (int x = 0; x < this.header.Width; x++) - { - int index = scanline[x]; - - rgba.Rgb = palettePixels[index]; - - color.PackFromRgba32(rgba); - row[x] = color; - } - } - } - /// /// Reads a header chunk from the data. /// @@ -1291,79 +869,60 @@ namespace SixLabors.ImageSharp.Formats.Png /// The containing data. private void ReadHeaderChunk(PngMetaData pngMetaData, ReadOnlySpan data) { - byte bitDepth = data[8]; - this.header = new PngHeader( - width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)), - height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)), - bitDepth: bitDepth, - colorType: (PngColorType)data[9], - compressionMethod: data[10], - filterMethod: data[11], - interlaceMethod: (PngInterlaceMode)data[12]); - - pngMetaData.BitDepth = (PngBitDepth)bitDepth; + this.header = PngHeader.Parse(data); + + this.header.Validate(); + + pngMetaData.BitDepth = (PngBitDepth)this.header.BitDepth; pngMetaData.ColorType = this.header.ColorType; + + this.pngColorType = this.header.ColorType; } /// - /// Validates the png header. + /// Reads a text chunk containing image properties from the data. /// - /// - /// Thrown if the image does pass validation. - /// - private void ValidateHeader() + /// The metadata to decode to. + /// The containing the data. + private void ReadTextChunk(ImageMetaData metadata, ReadOnlySpan data) { - if (!ColorTypes.ContainsKey(this.header.ColorType)) - { - throw new NotSupportedException("Color type is not supported or not valid."); - } - - if (!ColorTypes[this.header.ColorType].Contains(this.header.BitDepth)) + if (this.ignoreMetadata) { - throw new NotSupportedException("Bit depth is not supported or not valid."); + return; } - if (this.header.FilterMethod != 0) - { - throw new NotSupportedException("The png specification only defines 0 as filter method."); - } + int zeroIndex = data.IndexOf((byte)0); - if (this.header.InterlaceMethod != PngInterlaceMode.None && this.header.InterlaceMethod != PngInterlaceMode.Adam7) - { - throw new NotSupportedException("The png specification only defines 'None' and 'Adam7' as interlaced methods."); - } + string name = this.textEncoding.GetString(data.Slice(0, zeroIndex)); + string value = this.textEncoding.GetString(data.Slice(zeroIndex + 1)); - this.pngColorType = this.header.ColorType; + metadata.Properties.Add(new ImageProperty(name, value)); } /// - /// Reads a text chunk containing image properties from the data. + /// Reads the next data chunk. /// - /// The metadata to decode to. - /// The containing data. - /// The maximum length to read. - private void ReadTextChunk(ImageMetaData metadata, byte[] data, int length) + /// Count of bytes in the next data chunk, or 0 if there are no more data chunks left. + private int ReadNextDataChunk() { - if (this.ignoreMetadata) + if (this.nextChunk != null) { - return; + return 0; } - int zeroIndex = 0; + this.currentStream.Read(this.buffer, 0, 4); - for (int i = 0; i < length; i++) + if (this.TryReadChunk(out PngChunk chunk)) { - if (data[i] == 0) + if (chunk.Type == PngChunkType.Data) { - zeroIndex = i; - break; + return chunk.Length; } - } - string name = this.textEncoding.GetString(data, 0, zeroIndex); - string value = this.textEncoding.GetString(data, zeroIndex + 1, length - zeroIndex - 1); + this.nextChunk = chunk; + } - metadata.Properties.Add(new ImageProperty(name, value)); + return 0; } /// @@ -1375,9 +934,16 @@ namespace SixLabors.ImageSharp.Formats.Png /// private bool TryReadChunk(out PngChunk chunk) { - int length = this.ReadChunkLength(); + if (this.nextChunk != null) + { + chunk = this.nextChunk.Value; + + this.nextChunk = null; - if (length == -1) + return true; + } + + if (!this.TryReadChunkLength(out int length)) { chunk = default; @@ -1391,9 +957,7 @@ namespace SixLabors.ImageSharp.Formats.Png // That lets us read one byte at a time until we reach a known chunk. this.currentStream.Position -= 3; - length = this.ReadChunkLength(); - - if (length == -1) + if (!this.TryReadChunkLength(out length)) { chunk = default; @@ -1431,13 +995,17 @@ namespace SixLabors.ImageSharp.Formats.Png /// The . private void ValidateChunk(in PngChunk chunk) { + Span chunkType = stackalloc byte[4]; + + BinaryPrimitives.WriteUInt32BigEndian(chunkType, (uint)chunk.Type); + this.crc.Reset(); - this.crc.Update(this.chunkTypeBuffer); + this.crc.Update(chunkType); this.crc.Update(chunk.Data.GetSpan()); if (this.crc.Value != chunk.Crc) { - string chunkTypeName = Encoding.UTF8.GetString(this.chunkTypeBuffer, 0, 4); + string chunkTypeName = Encoding.UTF8.GetString(chunkType); throw new ImageFormatException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); } @@ -1451,14 +1019,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// private uint ReadChunkCrc() { - int numBytes = this.currentStream.Read(this.crcBuffer, 0, 4); - - if (numBytes >= 1 && numBytes <= 3) - { - throw new ImageFormatException("Image stream is not valid!"); - } - - return BinaryPrimitives.ReadUInt32BigEndian(this.crcBuffer); + return this.currentStream.Read(this.buffer, 0, 4) == 4 + ? BinaryPrimitives.ReadUInt32BigEndian(this.buffer) + : throw new ImageFormatException("Image stream is not valid!"); } /// @@ -1493,54 +1056,29 @@ namespace SixLabors.ImageSharp.Formats.Png /// private PngChunkType ReadChunkType() { - int numBytes = this.currentStream.Read(this.chunkTypeBuffer, 0, 4); - - if (numBytes >= 1 && numBytes <= 3) - { - throw new ImageFormatException("Image stream is not valid!"); - } - - return (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.chunkTypeBuffer.AsSpan()); + return this.currentStream.Read(this.buffer, 0, 4) == 4 + ? (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer) + : throw new ImageFormatException("Invalid PNG data."); } /// - /// Calculates the length of the given chunk. + /// Attempts to read the length of the next chunk. /// - /// - /// Thrown if the input stream is not valid. - /// - private int ReadChunkLength() + /// + /// Whether the the length was read. + /// + private bool TryReadChunkLength(out int result) { - int numBytes = this.currentStream.Read(this.chunkLengthBuffer, 0, 4); - - if (numBytes < 4) + if (this.currentStream.Read(this.buffer, 0, 4) == 4) { - return -1; + result = BinaryPrimitives.ReadInt32BigEndian(this.buffer); + + return true; } - return BinaryPrimitives.ReadInt32BigEndian(this.chunkLengthBuffer); - } + result = default; - /// - /// Returns the correct number of columns for each interlaced pass. - /// - /// Th current pass index - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int ComputeColumnsAdam7(int passIndex) - { - int width = this.header.Width; - switch (passIndex) - { - case 0: return (width + 7) / 8; - case 1: return (width + 3) / 8; - case 2: return (width + 3) / 4; - case 3: return (width + 1) / 4; - case 4: return (width + 1) / 2; - case 5: return width / 2; - case 6: return width; - default: throw new ArgumentException($"Not a valid pass index: {passIndex}"); - } + return false; } private void SwapBuffers() diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index f47a6518f0..96e97a305f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Gets or sets the filter method. /// - public PngFilterMethod FilterMethod { get; set; } = PngFilterMethod.Paeth; + public PngFilterMethod? FilterMethod { get; set; } /// /// Gets or sets the compression level 1-9. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index e4d2fc510d..a46d83707e 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -3,9 +3,13 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Memory; @@ -21,6 +25,21 @@ namespace SixLabors.ImageSharp.Formats.Png /// internal sealed class PngEncoderCore : IDisposable { + /// + /// The dictionary of available color types. + /// + private static readonly Dictionary ColorTypes = new Dictionary() + { + [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 }, + [PngColorType.Rgb] = new byte[] { 8, 16 }, + [PngColorType.Palette] = new byte[] { 1, 2, 4, 8 }, + [PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 }, + [PngColorType.RgbWithAlpha] = new byte[] { 8, 16 } + }; + + /// + /// Used the manage memory allocations. + /// private readonly MemoryAllocator memoryAllocator; /// @@ -158,7 +177,11 @@ namespace SixLabors.ImageSharp.Formats.Png this.memoryAllocator = memoryAllocator; this.pngBitDepth = options.BitDepth; this.pngColorType = options.ColorType; - this.pngFilterMethod = options.FilterMethod; + + // Specification recommends default filter method None for paletted images and Paeth for others. + this.pngFilterMethod = options.FilterMethod ?? (options.ColorType.Equals(PngColorType.Palette) + ? PngFilterMethod.None + : PngFilterMethod.Paeth); this.compressionLevel = options.CompressionLevel; this.gamma = options.Gamma; this.quantizer = options.Quantizer; @@ -189,28 +212,37 @@ namespace SixLabors.ImageSharp.Formats.Png this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth; this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16); + // Ensure we are not allowing impossible combinations. + if (!ColorTypes.ContainsKey(this.pngColorType.Value)) + { + throw new NotSupportedException("Color type is not supported or not valid."); + } + stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); QuantizedFrame quantized = null; - ReadOnlySpan quantizedPixelsSpan = default; if (this.pngColorType == PngColorType.Palette) { - byte bits; + byte bits = (byte)this.pngBitDepth; + if (!ColorTypes[this.pngColorType.Value].Contains(bits)) + { + throw new NotSupportedException("Bit depth is not supported or not valid."); + } // Use the metadata to determine what quantization depth to use if no quantizer has been set. if (this.quantizer == null) { - bits = (byte)Math.Min(8u, (short)this.pngBitDepth); - int colorSize = ImageMaths.GetColorCountForBitDepth(bits); - this.quantizer = new WuQuantizer(colorSize); + this.quantizer = new WuQuantizer(ImageMaths.GetColorCountForBitDepth(bits)); } // Create quantized frame returning the palette and set the bit depth. quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(image.Frames.RootFrame); - quantizedPixelsSpan = quantized.GetPixelSpan(); - bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); + byte quantizedBits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); + bits = Math.Max(bits, quantizedBits); // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk + // We check again for the bit depth as the bit depth of the color palette from a given quantizer might not + // be within the acceptable range. if (bits == 3) { bits = 4; @@ -224,7 +256,11 @@ namespace SixLabors.ImageSharp.Formats.Png } else { - this.bitDepth = (byte)(this.use16Bit ? 16 : 8); + this.bitDepth = (byte)this.pngBitDepth; + if (!ColorTypes[this.pngColorType.Value].Contains(this.bitDepth)) + { + throw new NotSupportedException("Bit depth is not supported or not valid."); + } } this.bytesPerPixel = this.CalculateBytesPerPixel(); @@ -249,7 +285,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.WritePhysicalChunk(stream, metaData); this.WriteGammaChunk(stream); this.WriteExifChunk(stream, metaData); - this.WriteDataChunks(image.Frames.RootFrame, quantizedPixelsSpan, stream); + this.WriteDataChunks(image.Frames.RootFrame, quantized, stream); this.WriteEndChunk(stream); stream.Flush(); @@ -280,32 +316,55 @@ namespace SixLabors.ImageSharp.Formats.Png const float RX = .2126F; const float GX = .7152F; const float BX = .0722F; + + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); Span rawScanlineSpan = this.rawScanline.GetSpan(); + ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan); if (this.pngColorType.Equals(PngColorType.Grayscale)) { - // TODO: Realistically we should support 1, 2, 4, 8, and 16 bit grayscale images. - // we currently do the other types via palette. Maybe RC as I don't understand how the data is packed yet - // for 1, 2, and 4 bit grayscale images. + // TODO: Research and add support for grayscale plus tRNS if (this.use16Bit) { // 16 bit grayscale Rgb48 rgb = default; for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 2) { - rowSpan[x].ToRgb48(ref rgb); + Unsafe.Add(ref rowSpanRef, x).ToRgb48(ref rgb); ushort luminance = (ushort)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B)); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance); } } else { - // 8 bit grayscale - Rgb24 rgb = default; - for (int x = 0; x < rowSpan.Length; x++) + if (this.bitDepth == 8) + { + // 8 bit grayscale + Rgb24 rgb = default; + for (int x = 0; x < rowSpan.Length; x++) + { + Unsafe.Add(ref rowSpanRef, x).ToRgb24(ref rgb); + Unsafe.Add(ref rawScanlineSpanRef, x) = (byte)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B)); + } + } + else { - rowSpan[x].ToRgb24(ref rgb); - rawScanlineSpan[x] = (byte)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B)); + // 1, 2, and 4 bit grayscale + using (IManagedByteBuffer temp = this.memoryAllocator.AllocateManagedByteBuffer(rowSpan.Length, AllocationOptions.Clean)) + { + int scaleFactor = 255 / (ImageMaths.GetColorCountForBitDepth(this.bitDepth) - 1); + Span tempSpan = temp.GetSpan(); + ref byte tempSpanRef = ref MemoryMarshal.GetReference(tempSpan); + + Rgb24 rgb = default; + for (int x = 0; x < rowSpan.Length; x++) + { + Unsafe.Add(ref rowSpanRef, x).ToRgb24(ref rgb); + float luminance = ((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B)) / scaleFactor; + Unsafe.Add(ref tempSpanRef, x) = (byte)luminance; + this.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth); + } + } } } } @@ -317,7 +376,7 @@ namespace SixLabors.ImageSharp.Formats.Png Rgba64 rgba = default; for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 4) { - rowSpan[x].ToRgba64(ref rgba); + Unsafe.Add(ref rowSpanRef, x).ToRgba64(ref rgba); ushort luminance = (ushort)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B)); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.A); @@ -329,9 +388,9 @@ namespace SixLabors.ImageSharp.Formats.Png Rgba32 rgba = default; for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 2) { - rowSpan[x].ToRgba32(ref rgba); - rawScanlineSpan[o] = (byte)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B)); - rawScanlineSpan[o + 1] = rgba.A; + Unsafe.Add(ref rowSpanRef, x).ToRgba32(ref rgba); + Unsafe.Add(ref rawScanlineSpanRef, o) = (byte)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B)); + Unsafe.Add(ref rawScanlineSpanRef, o + 1) = rgba.A; } } } @@ -367,9 +426,10 @@ namespace SixLabors.ImageSharp.Formats.Png { // 16 bit Rgba Rgba64 rgba = default; + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 8) { - rowSpan[x].ToRgba64(ref rgba); + Unsafe.Add(ref rowSpanRef, x).ToRgba64(ref rgba); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgba.R); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.G); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgba.B); @@ -383,9 +443,10 @@ namespace SixLabors.ImageSharp.Formats.Png { // 16 bit Rgb Rgb48 rgb = default; + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 6) { - rowSpan[x].ToRgb48(ref rgb); + Unsafe.Add(ref rowSpanRef, x).ToRgb48(ref rgb); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgb.R); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgb.G); BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgb.B); @@ -402,18 +463,25 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The pixel format. /// The row span. - /// The span of quantized pixels. Can be null. + /// The quantized pixels. Can be null. /// The row. /// The - private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, ReadOnlySpan quantizedPixelsSpan, int row) + private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row) where TPixel : struct, IPixel { switch (this.pngColorType) { case PngColorType.Palette: - int stride = this.rawScanline.Length(); - quantizedPixelsSpan.Slice(row * stride, stride).CopyTo(this.rawScanline.GetSpan()); + if (this.bitDepth < 8) + { + this.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.rawScanline.GetSpan(), this.bitDepth); + } + else + { + int stride = this.rawScanline.Length(); + quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.rawScanline.GetSpan()); + } break; case PngColorType.Grayscale: @@ -538,16 +606,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// The . private void WriteHeaderChunk(Stream stream, in PngHeader header) { - BinaryPrimitives.WriteInt32BigEndian(this.chunkDataBuffer.AsSpan(0, 4), header.Width); - BinaryPrimitives.WriteInt32BigEndian(this.chunkDataBuffer.AsSpan(4, 4), header.Height); - - this.chunkDataBuffer[8] = header.BitDepth; - this.chunkDataBuffer[9] = (byte)header.ColorType; - this.chunkDataBuffer[10] = header.CompressionMethod; - this.chunkDataBuffer[11] = header.FilterMethod; - this.chunkDataBuffer[12] = (byte)header.InterlaceMethod; + header.WriteTo(this.chunkDataBuffer); - this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, 13); + this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, PngHeader.Size); } /// @@ -569,8 +630,8 @@ namespace SixLabors.ImageSharp.Formats.Png using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength)) { - Span colorTableSpan = colorTable.GetSpan(); - Span alphaTableSpan = alphaTable.GetSpan(); + ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan()); + ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan()); Span quantizedSpan = quantized.GetPixelSpan(); for (int i = 0; i < paletteLength; i++) @@ -582,9 +643,9 @@ namespace SixLabors.ImageSharp.Formats.Png byte alpha = rgba.A; - colorTableSpan[offset] = rgba.R; - colorTableSpan[offset + 1] = rgba.G; - colorTableSpan[offset + 2] = rgba.B; + Unsafe.Add(ref colorTableRef, offset) = rgba.R; + Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G; + Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B; if (alpha > this.threshold) { @@ -592,7 +653,7 @@ namespace SixLabors.ImageSharp.Formats.Png } anyAlpha = anyAlpha || alpha < byte.MaxValue; - alphaTableSpan[i] = alpha; + Unsafe.Add(ref alphaTableRef, i) = alpha; } } @@ -613,51 +674,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// The image meta data. private void WritePhysicalChunk(Stream stream, ImageMetaData meta) { - // The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. It contains: - // Pixels per unit, X axis: 4 bytes (unsigned integer) - // Pixels per unit, Y axis: 4 bytes (unsigned integer) - // Unit specifier: 1 byte - // - // The following values are legal for the unit specifier: - // 0: unit is unknown - // 1: unit is the meter - // - // When the unit specifier is 0, the pHYs chunk defines pixel aspect ratio only; the actual size of the pixels remains unspecified. - Span hResolution = this.chunkDataBuffer.AsSpan(0, 4); - Span vResolution = this.chunkDataBuffer.AsSpan(4, 4); - - switch (meta.ResolutionUnits) - { - case PixelResolutionUnit.AspectRatio: - - this.chunkDataBuffer[8] = 0; - BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(meta.HorizontalResolution)); - BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(meta.VerticalResolution)); - break; - - case PixelResolutionUnit.PixelsPerInch: - - this.chunkDataBuffer[8] = 1; // Per meter - BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(UnitConverter.InchToMeter(meta.HorizontalResolution))); - BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(UnitConverter.InchToMeter(meta.VerticalResolution))); - break; + PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer); - case PixelResolutionUnit.PixelsPerCentimeter: - - this.chunkDataBuffer[8] = 1; // Per meter - BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(UnitConverter.CmToMeter(meta.HorizontalResolution))); - BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(UnitConverter.CmToMeter(meta.VerticalResolution))); - break; - - default: - - this.chunkDataBuffer[8] = 1; // Per meter - BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(meta.HorizontalResolution)); - BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(meta.VerticalResolution)); - break; - } - - this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, 9); + this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, PhysicalChunkData.Size); } /// @@ -696,9 +715,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The pixel format. /// The image. - /// The span of quantized pixel data. Can be null. + /// The quantized pixel data. Can be null. /// The stream. - private void WriteDataChunks(ImageFrame pixels, ReadOnlySpan quantizedPixelsSpan, Stream stream) + private void WriteDataChunks(ImageFrame pixels, QuantizedFrame quantized, Stream stream) where TPixel : struct, IPixel { this.bytesPerScanline = this.CalculateScanlineLength(this.width); @@ -714,26 +733,22 @@ namespace SixLabors.ImageSharp.Formats.Png break; case PngFilterMethod.Sub: - this.sub = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean); break; case PngFilterMethod.Up: - this.up = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean); break; case PngFilterMethod.Average: - this.average = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean); break; case PngFilterMethod.Paeth: - this.paeth = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean); break; - case PngFilterMethod.Adaptive: + case PngFilterMethod.Adaptive: this.sub = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean); this.up = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean); this.average = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean); @@ -750,7 +765,7 @@ namespace SixLabors.ImageSharp.Formats.Png { for (int y = 0; y < this.height; y++) { - IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan)pixels.GetPixelRowSpan(y), quantizedPixelsSpan, y); + IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan)pixels.GetPixelRowSpan(y), quantized, y); deflateStream.Write(r.Array, 0, resultLength); IManagedByteBuffer temp = this.rawScanline; @@ -830,6 +845,47 @@ namespace SixLabors.ImageSharp.Formats.Png stream.Write(this.buffer, 0, 4); // write the crc } + /// + /// Packs the given 8 bit array into and array of depths. + /// + /// The source span in 8 bits. + /// The resultant span in . + /// The bit depth. + private void ScaleDownFrom8BitArray(ReadOnlySpan source, Span result, int bits) + { + ref byte sourceRef = ref MemoryMarshal.GetReference(source); + ref byte resultRef = ref MemoryMarshal.GetReference(result); + + byte mask = (byte)(0xFF >> (8 - bits)); + byte shift0 = (byte)(8 - bits); + int shift = 8 - bits; + int v = 0; + int resultOffset = 0; + + for (int i = 0; i < source.Length; i++) + { + int value = Unsafe.Add(ref sourceRef, i) & mask; + v |= value << shift; + + if (shift == 0) + { + shift = shift0; + Unsafe.Add(ref resultRef, resultOffset) = (byte)v; + resultOffset++; + v = 0; + } + else + { + shift -= bits; + } + } + + if (shift != shift0) + { + Unsafe.Add(ref resultRef, resultOffset) = (byte)v; + } + } + /// /// Calculates the scanline length. /// diff --git a/src/ImageSharp/Formats/Png/PngHeader.cs b/src/ImageSharp/Formats/Png/PngHeader.cs index df85642bed..ea43ba96a5 100644 --- a/src/ImageSharp/Formats/Png/PngHeader.cs +++ b/src/ImageSharp/Formats/Png/PngHeader.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Buffers.Binary; + namespace SixLabors.ImageSharp.Formats.Png { /// @@ -8,6 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Png /// internal readonly struct PngHeader { + public const int Size = 13; + public PngHeader( int width, int height, @@ -74,5 +79,68 @@ namespace SixLabors.ImageSharp.Formats.Png /// Two values are currently defined: 0 (no interlace) or 1 (Adam7 interlace). /// public PngInterlaceMode InterlaceMethod { get; } + + /// + /// Validates the png header. + /// + /// + /// Thrown if the image does pass validation. + /// + public void Validate() + { + if (!PngConstants.ColorTypes.TryGetValue(this.ColorType, out byte[] supportedBitDepths)) + { + throw new NotSupportedException($"Invalid or unsupported color type. Was '{this.ColorType}'."); + } + + if (supportedBitDepths.AsSpan().IndexOf(this.BitDepth) == -1) + { + throw new NotSupportedException($"Invalid or unsupported bit depth. Was '{this.BitDepth}'."); + } + + if (this.FilterMethod != 0) + { + throw new NotSupportedException($"Invalid filter method. Expected 0. Was '{this.FilterMethod}'."); + } + + // The png specification only defines 'None' and 'Adam7' as interlaced methods. + if (this.InterlaceMethod != PngInterlaceMode.None && this.InterlaceMethod != PngInterlaceMode.Adam7) + { + throw new NotSupportedException($"Invalid interlace method. Expected 'None' or 'Adam7'. Was '{this.InterlaceMethod}'."); + } + } + + /// + /// Writes the header to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(0, 4), this.Width); + BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(4, 4), this.Height); + + buffer[8] = this.BitDepth; + buffer[9] = (byte)this.ColorType; + buffer[10] = this.CompressionMethod; + buffer[11] = this.FilterMethod; + buffer[12] = (byte)this.InterlaceMethod; + } + + /// + /// Parses the PngHeader from the given data buffer. + /// + /// The data to parse. + /// The parsed PngHeader. + public static PngHeader Parse(ReadOnlySpan data) + { + return new PngHeader( + width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)), + height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)), + bitDepth: data[8], + colorType: (PngColorType)data[9], + compressionMethod: data[10], + filterMethod: data[11], + interlaceMethod: (PngInterlaceMode)data[12]); + } } } diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs index 1eb3cdad6a..9c76765146 100644 --- a/src/ImageSharp/Formats/Png/PngMetaData.cs +++ b/src/ImageSharp/Formats/Png/PngMetaData.cs @@ -6,8 +6,26 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Provides Png specific metadata information for the image. /// - public class PngMetaData + public class PngMetaData : IDeepCloneable { + /// + /// Initializes a new instance of the class. + /// + public PngMetaData() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private PngMetaData(PngMetaData other) + { + this.BitDepth = other.BitDepth; + this.ColorType = other.ColorType; + this.Gamma = other.Gamma; + } + /// /// Gets or sets the number of bits per sample or per palette index (not per pixel). /// Not all values are allowed for all values. @@ -23,5 +41,8 @@ namespace SixLabors.ImageSharp.Formats.Png /// Gets or sets the gamma value for the image. /// public float Gamma { get; set; } + + /// + public IDeepCloneable DeepClone() => new PngMetaData(this); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs new file mode 100644 index 0000000000..6c81ba76c2 --- /dev/null +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -0,0 +1,600 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Png +{ + /// + /// Provides methods to allow the decoding of raw scanlines to image rows of different pixel formats. + /// + internal static class PngScanlineProcessor + { + public static void ProcessGrayscaleScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + bool hasTrans, + ushort luminance16Trans, + byte luminanceTrans) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + int scaleFactor = 255 / (ImageMaths.GetColorCountForBitDepth(header.BitDepth) - 1); + + if (!hasTrans) + { + if (header.BitDepth == 16) + { + Rgb48 rgb48 = default; + for (int x = 0, o = 0; x < header.Width; x++, o += 2) + { + ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); + rgb48.R = luminance; + rgb48.G = luminance; + rgb48.B = luminance; + + pixel.PackFromRgb48(rgb48); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + // TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method. + var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue); + for (int x = 0; x < header.Width; x++) + { + byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor); + rgba32.R = luminance; + rgba32.G = luminance; + rgba32.B = luminance; + + pixel.PackFromRgba32(rgba32); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + + return; + } + + if (header.BitDepth == 16) + { + Rgba64 rgba64 = default; + for (int x = 0, o = 0; x < header.Width; x++, o += 2) + { + ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); + rgba64.R = luminance; + rgba64.G = luminance; + rgba64.B = luminance; + rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue; + + pixel.PackFromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + Rgba32 rgba32 = default; + for (int x = 0; x < header.Width; x++) + { + byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor); + rgba32.R = luminance; + rgba32.G = luminance; + rgba32.B = luminance; + rgba32.A = luminance.Equals(luminanceTrans) ? byte.MinValue : byte.MaxValue; + + pixel.PackFromRgba32(rgba32); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + } + + public static void ProcessInterlacedGrayscaleScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + bool hasTrans, + ushort luminance16Trans, + byte luminanceTrans) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + int scaleFactor = 255 / (ImageMaths.GetColorCountForBitDepth(header.BitDepth) - 1); + + if (!hasTrans) + { + if (header.BitDepth == 16) + { + Rgb48 rgb48 = default; + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += 2) + { + ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); + rgb48.R = luminance; + rgb48.G = luminance; + rgb48.B = luminance; + + pixel.PackFromRgb48(rgb48); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + // TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method. + var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue); + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o++) + { + byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); + rgba32.R = luminance; + rgba32.G = luminance; + rgba32.B = luminance; + + pixel.PackFromRgba32(rgba32); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + + return; + } + + if (header.BitDepth == 16) + { + Rgba64 rgba64 = default; + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += 2) + { + ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); + rgba64.R = luminance; + rgba64.G = luminance; + rgba64.B = luminance; + rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue; + + pixel.PackFromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + Rgba32 rgba32 = default; + for (int x = pixelOffset; x < header.Width; x += increment) + { + byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor); + rgba32.R = luminance; + rgba32.G = luminance; + rgba32.B = luminance; + rgba32.A = luminance.Equals(luminanceTrans) ? byte.MinValue : byte.MaxValue; + + pixel.PackFromRgba32(rgba32); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + } + + public static void ProcessGrayscaleWithAlphaScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int bytesPerPixel, + int bytesPerSample) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + + if (header.BitDepth == 16) + { + Rgba64 rgba64 = default; + for (int x = 0, o = 0; x < header.Width; x++, o += 4) + { + ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); + ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); + rgba64.R = luminance; + rgba64.G = luminance; + rgba64.B = luminance; + rgba64.A = alpha; + + pixel.PackFromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + Rgba32 rgba32 = default; + int bps = bytesPerSample; + for (int x = 0; x < header.Width; x++) + { + int offset = x * bytesPerPixel; + byte luminance = Unsafe.Add(ref scanlineSpanRef, offset); + byte alpha = Unsafe.Add(ref scanlineSpanRef, offset + bps); + + rgba32.R = luminance; + rgba32.G = luminance; + rgba32.B = luminance; + rgba32.A = alpha; + + pixel.PackFromRgba32(rgba32); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + } + + public static void ProcessInterlacedGrayscaleWithAlphaScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + int bytesPerPixel, + int bytesPerSample) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + + if (header.BitDepth == 16) + { + Rgba64 rgba64 = default; + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += 4) + { + ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); + ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); + rgba64.R = luminance; + rgba64.G = luminance; + rgba64.B = luminance; + rgba64.A = alpha; + + pixel.PackFromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + Rgba32 rgba32 = default; + for (int x = pixelOffset; x < header.Width; x += increment) + { + int offset = x * bytesPerPixel; + byte luminance = Unsafe.Add(ref scanlineSpanRef, offset); + byte alpha = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample); + rgba32.R = luminance; + rgba32.G = luminance; + rgba32.B = luminance; + rgba32.A = alpha; + + pixel.PackFromRgba32(rgba32); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + } + + public static void ProcessPaletteScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + ReadOnlySpan palette, + byte[] paletteAlpha) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette); + ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels); + + if (paletteAlpha?.Length > 0) + { + // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha + // channel and we should try to read it. + Rgba32 rgba = default; + ref byte paletteAlphaRef = ref paletteAlpha[0]; + + for (int x = 0; x < header.Width; x++) + { + int index = Unsafe.Add(ref scanlineSpanRef, x); + rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); + rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; + + pixel.PackFromRgba32(rgba); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + // TODO: We should have PackFromRgb24. + var rgba = new Rgba32(0, 0, 0, byte.MaxValue); + for (int x = 0; x < header.Width; x++) + { + int index = Unsafe.Add(ref scanlineSpanRef, x); + rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); + + pixel.PackFromRgba32(rgba); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + } + + public static void ProcessInterlacedPaletteScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + ReadOnlySpan palette, + byte[] paletteAlpha) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette); + ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels); + + if (paletteAlpha?.Length > 0) + { + // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha + // channel and we should try to read it. + Rgba32 rgba = default; + ref byte paletteAlphaRef = ref paletteAlpha[0]; + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o++) + { + int index = Unsafe.Add(ref scanlineSpanRef, o); + rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; + rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); + + pixel.PackFromRgba32(rgba); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + var rgba = new Rgba32(0, 0, 0, byte.MaxValue); + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o++) + { + int index = Unsafe.Add(ref scanlineSpanRef, o); + rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); + + pixel.PackFromRgba32(rgba); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + } + + public static void ProcessRgbScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int bytesPerPixel, + int bytesPerSample, + bool hasTrans, + Rgb48 rgb48Trans, + Rgb24 rgb24Trans) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + + if (!hasTrans) + { + if (header.BitDepth == 16) + { + Rgb48 rgb48 = default; + for (int x = 0, o = 0; x < header.Width; x++, o += bytesPerPixel) + { + rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + + pixel.PackFromRgb48(rgb48); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + PixelOperations.Instance.PackFromRgb24Bytes(scanlineSpan, rowSpan, header.Width); + } + + return; + } + + if (header.BitDepth == 16) + { + Rgb48 rgb48 = default; + Rgba64 rgba64 = default; + for (int x = 0, o = 0; x < header.Width; x++, o += bytesPerPixel) + { + rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + + rgba64.Rgb = rgb48; + rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; + + pixel.PackFromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan); + ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span); + for (int x = 0; x < header.Width; x++) + { + ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x); + Rgba32 rgba32 = default; + rgba32.Rgb = rgb24; + rgba32.A = rgb24.Equals(rgb24Trans) ? byte.MinValue : byte.MaxValue; + + pixel.PackFromRgba32(rgba32); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + } + + public static void ProcessInterlacedRgbScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + int bytesPerPixel, + int bytesPerSample, + bool hasTrans, + Rgb48 rgb48Trans, + Rgb24 rgb24Trans) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + + if (header.BitDepth == 16) + { + if (hasTrans) + { + Rgb48 rgb48 = default; + Rgba64 rgba64 = default; + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel) + { + rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + + rgba64.Rgb = rgb48; + rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; + + pixel.PackFromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + Rgb48 rgb48 = default; + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel) + { + rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + + pixel.PackFromRgb48(rgb48); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + + return; + } + + if (hasTrans) + { + Rgba32 rgba = default; + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel) + { + rgba.R = Unsafe.Add(ref scanlineSpanRef, o); + rgba.G = Unsafe.Add(ref scanlineSpanRef, o + bytesPerSample); + rgba.B = Unsafe.Add(ref scanlineSpanRef, o + (2 * bytesPerSample)); + rgba.A = rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue; + + pixel.PackFromRgba32(rgba); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + var rgba = new Rgba32(0, 0, 0, byte.MaxValue); + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel) + { + rgba.R = Unsafe.Add(ref scanlineSpanRef, o); + rgba.G = Unsafe.Add(ref scanlineSpanRef, o + bytesPerSample); + rgba.B = Unsafe.Add(ref scanlineSpanRef, o + (2 * bytesPerSample)); + + pixel.PackFromRgba32(rgba); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + } + + public static void ProcessRgbaScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int bytesPerPixel, + int bytesPerSample) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + + if (header.BitDepth == 16) + { + Rgba64 rgba64 = default; + for (int x = 0, o = 0; x < header.Width; x++, o += bytesPerPixel) + { + rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); + + pixel.PackFromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + PixelOperations.Instance.PackFromRgba32Bytes(scanlineSpan, rowSpan, header.Width); + } + } + + public static void ProcessInterlacedRgbaScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + int bytesPerPixel, + int bytesPerSample) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + + if (header.BitDepth == 16) + { + Rgba64 rgba64 = default; + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel) + { + rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); + + pixel.PackFromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + Rgba32 rgba = default; + for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel) + { + rgba.R = Unsafe.Add(ref scanlineSpanRef, o); + rgba.G = Unsafe.Add(ref scanlineSpanRef, o + bytesPerSample); + rgba.B = Unsafe.Add(ref scanlineSpanRef, o + (2 * bytesPerSample)); + rgba.A = Unsafe.Add(ref scanlineSpanRef, o + (3 * bytesPerSample)); + + pixel.PackFromRgba32(rgba); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs index 55432d60b1..583175b566 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs +++ b/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs @@ -43,22 +43,24 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private bool isDisposed; /// - /// Whether the crc value has been read. + /// The current data remaining to be read /// - private bool crcRead; + private int currentDataRemaining; /// - /// The current data remaining to be read + /// Delegate to get more data once we've exhausted the current data remaining /// - private int currentDataRemaining; + private readonly Func getData; /// /// Initializes a new instance of the class. /// /// The inner raw stream - public ZlibInflateStream(Stream innerStream) + /// A delegate to get more data from the inner stream + public ZlibInflateStream(Stream innerStream, Func getData) { this.innerStream = innerStream; + this.getData = getData; } /// @@ -112,12 +114,36 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { if (this.currentDataRemaining == 0) { - return 0; + // last buffer was read in its entirety, let's make sure we don't actually have more + this.currentDataRemaining = this.getData(); + + if (this.currentDataRemaining == 0) + { + return 0; + } } int bytesToRead = Math.Min(count, this.currentDataRemaining); this.currentDataRemaining -= bytesToRead; - return this.innerStream.Read(buffer, offset, bytesToRead); + int bytesRead = this.innerStream.Read(buffer, offset, bytesToRead); + + // keep reading data until we've reached the end of the stream or filled the buffer + while (this.currentDataRemaining == 0 && bytesRead < count) + { + this.currentDataRemaining = this.getData(); + + if (this.currentDataRemaining == 0) + { + return bytesRead; + } + + offset += bytesRead; + bytesToRead = Math.Min(count - bytesRead, this.currentDataRemaining); + this.currentDataRemaining -= bytesToRead; + bytesRead += this.innerStream.Read(buffer, offset, bytesToRead); + } + + return bytesRead; } /// @@ -153,14 +179,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib { this.compressedStream.Dispose(); this.compressedStream = null; - - if (!this.crcRead) - { - // Consume the trailing 4 bytes - this.innerStream.Read(ChecksumBuffer, 0, 4); - this.currentDataRemaining -= 4; - this.crcRead = true; - } } } diff --git a/src/ImageSharp/IDeepCloneable.cs b/src/ImageSharp/IDeepCloneable.cs new file mode 100644 index 0000000000..f80247a5d0 --- /dev/null +++ b/src/ImageSharp/IDeepCloneable.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp +{ + /// + /// A generic interface for a deeply cloneable type. + /// + /// The type of object to clone. + public interface IDeepCloneable + where T : class + { + /// + /// Creates a new that is a deep copy of the current instance. + /// + /// The . + T DeepClone(); + } + + /// + /// An interface for objects that can be cloned. This creates a deep copy of the object. + /// + public interface IDeepCloneable + { + /// + /// Creates a new object that is a deep copy of the current instance. + /// + /// The . + IDeepCloneable DeepClone(); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index 894551e08f..8b9f3fdb5b 100644 --- a/src/ImageSharp/Image.Decode.cs +++ b/src/ImageSharp/Image.Decode.cs @@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp return null; } - using (IManagedByteBuffer buffer = config.MemoryAllocator.AllocateManagedByteBuffer(maxHeaderSize)) + using (IManagedByteBuffer buffer = config.MemoryAllocator.AllocateManagedByteBuffer(maxHeaderSize, AllocationOptions.Clean)) { long startPosition = stream.Position; stream.Read(buffer.Array, 0, maxHeaderSize); diff --git a/src/ImageSharp/ImageFrameCollection.cs b/src/ImageSharp/ImageFrameCollection.cs index 59571ce92e..bbe05ce435 100644 --- a/src/ImageSharp/ImageFrameCollection.cs +++ b/src/ImageSharp/ImageFrameCollection.cs @@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp public ImageFrame InsertFrame(int index, ImageFrame source) { this.ValidateFrame(source); - ImageFrame clonedFrame = source.Clone(); + ImageFrame clonedFrame = source.Clone(this.parent.GetConfiguration()); this.frames.Insert(index, clonedFrame); return clonedFrame; } @@ -107,7 +107,7 @@ namespace SixLabors.ImageSharp public ImageFrame AddFrame(ImageFrame source) { this.ValidateFrame(source); - ImageFrame clonedFrame = source.Clone(); + ImageFrame clonedFrame = source.Clone(this.parent.GetConfiguration()); this.frames.Add(clonedFrame); return clonedFrame; } @@ -155,10 +155,7 @@ namespace SixLabors.ImageSharp /// /// true if the contains the specified frame; otherwise, false. /// - public bool Contains(ImageFrame frame) - { - return this.frames.Contains(frame); - } + public bool Contains(ImageFrame frame) => this.frames.Contains(frame); /// /// Moves an from to . @@ -195,7 +192,7 @@ namespace SixLabors.ImageSharp this.frames.Remove(frame); - return new Image(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { frame }); + return new Image(this.parent.GetConfiguration(), this.parent.MetaData.DeepClone(), new[] { frame }); } /// @@ -208,7 +205,7 @@ namespace SixLabors.ImageSharp { ImageFrame frame = this[index]; ImageFrame clonedFrame = frame.Clone(); - return new Image(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { clonedFrame }); + return new Image(this.parent.GetConfiguration(), this.parent.MetaData.DeepClone(), new[] { clonedFrame }); } /// @@ -217,10 +214,7 @@ namespace SixLabors.ImageSharp /// /// The new . /// - public ImageFrame CreateFrame() - { - return this.CreateFrame(default); - } + public ImageFrame CreateFrame() => this.CreateFrame(default); /// /// Creates a new and appends it to the end of the collection. diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 132ab598e5..be1792ced1 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -95,6 +96,10 @@ namespace SixLabors.ImageSharp /// /// Initializes a new instance of the class wrapping an existing buffer. /// + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The memory source. internal ImageFrame(Configuration configuration, int width, int height, MemorySource memorySource) : this(configuration, width, height, memorySource, new ImageFrameMetaData()) { @@ -103,12 +108,12 @@ namespace SixLabors.ImageSharp /// /// Initializes a new instance of the class wrapping an existing buffer. /// - internal ImageFrame( - Configuration configuration, - int width, - int height, - MemorySource memorySource, - ImageFrameMetaData metaData) + /// The configuration providing initialization code which allows extending the library. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The memory source. + /// The meta data. + internal ImageFrame(Configuration configuration, int width, int height, MemorySource memorySource, ImageFrameMetaData metaData) { Guard.NotNull(configuration, nameof(configuration)); Guard.MustBeGreaterThan(width, 0, nameof(width)); @@ -135,7 +140,7 @@ namespace SixLabors.ImageSharp this.MemoryAllocator = configuration.MemoryAllocator; this.PixelBuffer = this.MemoryAllocator.Allocate2D(source.PixelBuffer.Width, source.PixelBuffer.Height); source.PixelBuffer.GetSpan().CopyTo(this.PixelBuffer.GetSpan()); - this.MetaData = source.MetaData.Clone(); + this.MetaData = source.MetaData.DeepClone(); } /// @@ -247,35 +252,61 @@ namespace SixLabors.ImageSharp /// public override string ToString() => $"ImageFrame<{typeof(TPixel).Name}>: {this.Width}x{this.Height}"; + /// + /// Clones the current instance. + /// + /// The + internal ImageFrame Clone() => this.Clone(this.configuration); + + /// + /// Clones the current instance. + /// + /// The configuration providing initialization code which allows extending the library. + /// The + internal ImageFrame Clone(Configuration configuration) => new ImageFrame(configuration, this); + /// /// Returns a copy of the image frame in the given pixel format. /// /// The pixel format. /// The internal ImageFrame CloneAs() + where TPixel2 : struct, IPixel => this.CloneAs(this.configuration); + + /// + /// Returns a copy of the image frame in the given pixel format. + /// + /// The pixel format. + /// The configuration providing initialization code which allows extending the library. + /// The + internal ImageFrame CloneAs(Configuration configuration) where TPixel2 : struct, IPixel { if (typeof(TPixel2) == typeof(TPixel)) { - return this.Clone() as ImageFrame; + return this.Clone(configuration) as ImageFrame; } - var target = new ImageFrame(this.configuration, this.Width, this.Height, this.MetaData.Clone()); - - ParallelFor.WithTemporaryBuffer( - 0, - this.Height, - this.configuration, - this.Width, - (int y, IMemoryOwner tempRowBuffer) => - { - Span sourceRow = this.GetPixelRowSpan(y); - Span targetRow = target.GetPixelRowSpan(y); - Span tempRowSpan = tempRowBuffer.GetSpan(); - - PixelOperations.Instance.ToScaledVector4(sourceRow, tempRowSpan, sourceRow.Length); - PixelOperations.Instance.PackFromScaledVector4(tempRowSpan, targetRow, targetRow.Length); - }); + var target = new ImageFrame(configuration, this.Width, this.Height, this.MetaData.DeepClone()); + + ParallelHelper.IterateRowsWithTempBuffer( + this.Bounds(), + configuration, + (rows, tempRowBuffer) => + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span sourceRow = this.GetPixelRowSpan(y); + Span targetRow = target.GetPixelRowSpan(y); + Span tempRowSpan = tempRowBuffer.Span; + + PixelOperations.Instance.ToScaledVector4(sourceRow, tempRowSpan, sourceRow.Length); + PixelOperations.Instance.PackFromScaledVector4( + tempRowSpan, + targetRow, + targetRow.Length); + } + }); return target; } @@ -287,22 +318,17 @@ namespace SixLabors.ImageSharp /// The value to initialize the bitmap with. internal void Clear(ParallelOptions parallelOptions, TPixel value) { - Parallel.For( - 0, - this.Height, - parallelOptions, - y => - { - Span targetRow = this.GetPixelRowSpan(y); - targetRow.Fill(value); - }); - } + Span span = this.GetPixelSpan(); - /// - /// Clones the current instance. - /// - /// The - internal ImageFrame Clone() => new ImageFrame(this.configuration, this); + if (value.Equals(default)) + { + span.Clear(); + } + else + { + span.Fill(value); + } + } /// void IDisposable.Dispose() => this.Dispose(); diff --git a/src/ImageSharp/ImageSharp.csproj.DotSettings b/src/ImageSharp/ImageSharp.csproj.DotSettings new file mode 100644 index 0000000000..8b2e1bcf07 --- /dev/null +++ b/src/ImageSharp/ImageSharp.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 8bc5a40bdc..9d4c1ef0b3 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -11,7 +11,6 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; namespace SixLabors.ImageSharp { @@ -23,15 +22,12 @@ namespace SixLabors.ImageSharp where TPixel : struct, IPixel { private readonly Configuration configuration; - private readonly ImageFrameCollection frames; /// /// Initializes a new instance of the class /// with the height and the width of the image. /// - /// - /// The configuration providing initialization code which allows extending the library. - /// + /// The configuration providing initialization code which allows extending the library. /// The width of the image in pixels. /// The height of the image in pixels. public Image(Configuration configuration, int width, int height) @@ -43,9 +39,7 @@ namespace SixLabors.ImageSharp /// Initializes a new instance of the class /// with the height and the width of the image. /// - /// - /// The configuration providing initialization code which allows extending the library. - /// + /// The configuration providing initialization code which allows extending the library. /// The width of the image in pixels. /// The height of the image in pixels. /// The color to initialize the pixels with. @@ -69,9 +63,7 @@ namespace SixLabors.ImageSharp /// Initializes a new instance of the class /// with the height and the width of the image. /// - /// - /// The configuration providing initialization code which allows extending the library. - /// + /// The configuration providing initialization code which allows extending the library. /// The width of the image in pixels. /// The height of the image in pixels. /// The images metadata. @@ -80,37 +72,41 @@ namespace SixLabors.ImageSharp this.configuration = configuration ?? Configuration.Default; this.PixelType = new PixelTypeInfo(Unsafe.SizeOf() * 8); this.MetaData = metadata ?? new ImageMetaData(); - this.frames = new ImageFrameCollection(this, width, height, default(TPixel)); + this.Frames = new ImageFrameCollection(this, width, height, default(TPixel)); } /// /// Initializes a new instance of the class /// wrapping an external /// + /// The configuration providing initialization code which allows extending the library. + /// The memory source. + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The images metadata. internal Image(Configuration configuration, MemorySource memorySource, int width, int height, ImageMetaData metadata) { this.configuration = configuration; this.PixelType = new PixelTypeInfo(Unsafe.SizeOf() * 8); this.MetaData = metadata; - this.frames = new ImageFrameCollection(this, width, height, memorySource); + this.Frames = new ImageFrameCollection(this, width, height, memorySource); } /// /// Initializes a new instance of the class /// with the height and the width of the image. /// - /// - /// The configuration providing initialization code which allows extending the library. - /// + /// The configuration providing initialization code which allows extending the library. /// The width of the image in pixels. /// The height of the image in pixels. /// The color to initialize the pixels with. /// The images metadata. - internal Image(Configuration configuration, int width, int height, TPixel backgroundColor, ImageMetaData metadata) { + internal Image(Configuration configuration, int width, int height, TPixel backgroundColor, ImageMetaData metadata) + { this.configuration = configuration ?? Configuration.Default; this.PixelType = new PixelTypeInfo(Unsafe.SizeOf() * 8); this.MetaData = metadata ?? new ImageMetaData(); - this.frames = new ImageFrameCollection(this, width, height, backgroundColor); + this.Frames = new ImageFrameCollection(this, width, height, backgroundColor); } /// @@ -126,7 +122,7 @@ namespace SixLabors.ImageSharp this.PixelType = new PixelTypeInfo(Unsafe.SizeOf() * 8); this.MetaData = metadata ?? new ImageMetaData(); - this.frames = new ImageFrameCollection(this, frames); + this.Frames = new ImageFrameCollection(this, frames); } /// @@ -138,10 +134,10 @@ namespace SixLabors.ImageSharp public PixelTypeInfo PixelType { get; } /// - public int Width => this.frames.RootFrame.Width; + public int Width => this.Frames.RootFrame.Width; /// - public int Height => this.frames.RootFrame.Height; + public int Height => this.Frames.RootFrame.Height; /// public ImageMetaData MetaData { get; } @@ -149,12 +145,12 @@ namespace SixLabors.ImageSharp /// /// Gets the frames. /// - public ImageFrameCollection Frames => this.frames; + public ImageFrameCollection Frames { get; } /// /// Gets the root frame. /// - private IPixelSource PixelSource => this.frames?.RootFrame ?? throw new ObjectDisposedException(nameof(Image)); + private IPixelSource PixelSource => this.Frames?.RootFrame ?? throw new ObjectDisposedException(nameof(Image)); /// /// Gets or sets the pixel at the specified position. @@ -187,16 +183,17 @@ namespace SixLabors.ImageSharp /// Clones the current image /// /// Returns a new image with all the same metadata as the original. - public Image Clone() - { - IEnumerable> clonedFrames = this.frames.Select(x => x.Clone()); - return new Image(this.configuration, this.MetaData.Clone(), clonedFrames); - } + public Image Clone() => this.Clone(this.configuration); - /// - public override string ToString() + /// + /// Clones the current image with the given configueation. + /// + /// The configuration providing initialization code which allows extending the library. + /// Returns a new with all the same pixel data as the original. + public Image Clone(Configuration configuration) { - return $"Image<{typeof(TPixel).Name}>: {this.Width}x{this.Height}"; + IEnumerable> clonedFrames = this.Frames.Select(x => x.Clone(configuration)); + return new Image(configuration, this.MetaData.DeepClone(), clonedFrames); } /// @@ -205,22 +202,27 @@ namespace SixLabors.ImageSharp /// The pixel format. /// The public Image CloneAs() - where TPixel2 : struct, IPixel - { - IEnumerable> clonedFrames = this.frames.Select(x => x.CloneAs()); - var target = new Image(this.configuration, this.MetaData.Clone(), clonedFrames); - - return target; - } + where TPixel2 : struct, IPixel => this.CloneAs(this.configuration); /// - /// Releases managed resources. + /// Returns a copy of the image in the given pixel format. /// - public void Dispose() + /// The pixel format. + /// The configuration providing initialization code which allows extending the library. + /// The + public Image CloneAs(Configuration configuration) + where TPixel2 : struct, IPixel { - this.frames.Dispose(); + IEnumerable> clonedFrames = this.Frames.Select(x => x.CloneAs(configuration)); + return new Image(configuration, this.MetaData.DeepClone(), clonedFrames); } + /// + public void Dispose() => this.Frames.Dispose(); + + /// + public override string ToString() => $"Image<{typeof(TPixel).Name}>: {this.Width}x{this.Height}"; + /// /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer. /// @@ -229,9 +231,9 @@ namespace SixLabors.ImageSharp { Guard.NotNull(pixelSource, nameof(pixelSource)); - for (int i = 0; i < this.frames.Count; i++) + for (int i = 0; i < this.Frames.Count; i++) { - this.frames[i].SwapOrCopyPixelsBufferFrom(pixelSource.frames[i]); + this.Frames[i].SwapOrCopyPixelsBufferFrom(pixelSource.Frames[i]); } } } diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index 107457ae73..17ab6e2522 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -96,15 +96,14 @@ namespace SixLabors.ImageSharp.Memory /// The /// The rectangle subarea /// The - public static BufferArea GetArea(this Buffer2D buffer, Rectangle rectangle) + public static BufferArea GetArea(this Buffer2D buffer, in Rectangle rectangle) where T : struct => new BufferArea(buffer, rectangle); public static BufferArea GetArea(this Buffer2D buffer, int x, int y, int width, int height) - where T : struct - { - var rectangle = new Rectangle(x, y, width, height); - return new BufferArea(buffer, rectangle); - } + where T : struct => new BufferArea(buffer, new Rectangle(x, y, width, height)); + + public static BufferArea GetAreaBetweenRows(this Buffer2D buffer, int minY, int maxY) + where T : struct => new BufferArea(buffer, new Rectangle(0, minY, buffer.Width, maxY - minY)); /// /// Return a to the whole area of 'buffer' @@ -114,5 +113,14 @@ namespace SixLabors.ImageSharp.Memory /// The public static BufferArea GetArea(this Buffer2D buffer) where T : struct => new BufferArea(buffer); + + /// + /// Gets a span for all the pixels in defined by + /// + public static Span GetMultiRowSpan(this Buffer2D buffer, in RowInterval rows) + where T : struct + { + return buffer.Span.Slice(rows.Min * buffer.Width, rows.Height * buffer.Width); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Memory/RowInterval.cs b/src/ImageSharp/Memory/RowInterval.cs new file mode 100644 index 0000000000..0750e0368c --- /dev/null +++ b/src/ImageSharp/Memory/RowInterval.cs @@ -0,0 +1,45 @@ +// Copyright(c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// Represents an interval of rows in a and/or + /// + internal readonly struct RowInterval + { + /// + /// Initializes a new instance of the struct. + /// + public RowInterval(int min, int max) + { + DebugGuard.MustBeLessThan(min, max, nameof(min)); + + this.Min = min; + this.Max = max; + } + + /// + /// Gets the INCLUSIVE minimum + /// + public int Min { get; } + + /// + /// Gets the EXCLUSIVE maximum + /// + public int Max { get; } + + /// + /// Gets the difference ( - ) + /// + public int Height => this.Max - this.Min; + + /// + public override string ToString() + { + return $"RowInterval [{this.Min}->{this.Max}["; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/MetaData/ImageFrameMetaData.cs b/src/ImageSharp/MetaData/ImageFrameMetaData.cs index 4b819e2013..f1f884be68 100644 --- a/src/ImageSharp/MetaData/ImageFrameMetaData.cs +++ b/src/ImageSharp/MetaData/ImageFrameMetaData.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using SixLabors.ImageSharp.Formats; @@ -10,9 +9,9 @@ namespace SixLabors.ImageSharp.MetaData /// /// Encapsulates the metadata of an image frame. /// - public sealed class ImageFrameMetaData + public sealed class ImageFrameMetaData : IDeepCloneable { - private readonly Dictionary formatMetaData = new Dictionary(); + private readonly Dictionary formatMetaData = new Dictionary(); /// /// Initializes a new instance of the class. @@ -32,17 +31,14 @@ namespace SixLabors.ImageSharp.MetaData { DebugGuard.NotNull(other, nameof(other)); - foreach (KeyValuePair meta in other.formatMetaData) + foreach (KeyValuePair meta in other.formatMetaData) { - this.formatMetaData.Add(meta.Key, meta.Value); + this.formatMetaData.Add(meta.Key, meta.Value.DeepClone()); } } - /// - /// Clones this ImageFrameMetaData. - /// - /// The cloned instance. - public ImageFrameMetaData Clone() => new ImageFrameMetaData(this); + /// + public ImageFrameMetaData DeepClone() => new ImageFrameMetaData(this); /// /// Gets the metadata value associated with the specified key. @@ -55,9 +51,9 @@ namespace SixLabors.ImageSharp.MetaData /// public TFormatFrameMetaData GetFormatMetaData(IImageFormat key) where TFormatMetaData : class - where TFormatFrameMetaData : class + where TFormatFrameMetaData : class, IDeepCloneable { - if (this.formatMetaData.TryGetValue(key, out object meta)) + if (this.formatMetaData.TryGetValue(key, out IDeepCloneable meta)) { return (TFormatFrameMetaData)meta; } diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index 7e74157e70..73549d98aa 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.MetaData.Profiles.Exif; @@ -12,7 +11,7 @@ namespace SixLabors.ImageSharp.MetaData /// /// Encapsulates the metadata of an image. /// - public sealed class ImageMetaData + public sealed class ImageMetaData : IDeepCloneable { /// /// The default horizontal resolution value (dots per inch) in x direction. @@ -26,7 +25,13 @@ namespace SixLabors.ImageSharp.MetaData /// public const double DefaultVerticalResolution = 96; - private readonly Dictionary formatMetaData = new Dictionary(); + /// + /// The default pixel resolution units. + /// The default value is . + /// + public const PixelResolutionUnit DefaultPixelResolutionUnits = PixelResolutionUnit.PixelsPerInch; + + private readonly Dictionary formatMetaData = new Dictionary(); private double horizontalResolution; private double verticalResolution; @@ -37,6 +42,7 @@ namespace SixLabors.ImageSharp.MetaData { this.horizontalResolution = DefaultHorizontalResolution; this.verticalResolution = DefaultVerticalResolution; + this.ResolutionUnits = DefaultPixelResolutionUnits; } /// @@ -52,9 +58,9 @@ namespace SixLabors.ImageSharp.MetaData this.VerticalResolution = other.VerticalResolution; this.ResolutionUnits = other.ResolutionUnits; - foreach (KeyValuePair meta in other.formatMetaData) + foreach (KeyValuePair meta in other.formatMetaData) { - this.formatMetaData.Add(meta.Key, meta.Value); + this.formatMetaData.Add(meta.Key, meta.Value.DeepClone()); } foreach (ImageProperty property in other.Properties) @@ -62,13 +68,8 @@ namespace SixLabors.ImageSharp.MetaData this.Properties.Add(property); } - this.ExifProfile = other.ExifProfile != null - ? new ExifProfile(other.ExifProfile) - : null; - - this.IccProfile = other.IccProfile != null - ? new IccProfile(other.IccProfile) - : null; + this.ExifProfile = other.ExifProfile?.DeepClone(); + this.IccProfile = other.IccProfile?.DeepClone(); } /// @@ -114,7 +115,7 @@ namespace SixLabors.ImageSharp.MetaData /// 02 : Pixels per centimeter /// 03 : Pixels per meter /// - public PixelResolutionUnit ResolutionUnits { get; set; } = PixelResolutionUnit.PixelsPerInch; + public PixelResolutionUnit ResolutionUnits { get; set; } /// /// Gets or sets the Exif profile. @@ -140,9 +141,9 @@ namespace SixLabors.ImageSharp.MetaData /// The . /// public TFormatMetaData GetFormatMetaData(IImageFormat key) - where TFormatMetaData : class + where TFormatMetaData : class, IDeepCloneable { - if (this.formatMetaData.TryGetValue(key, out object meta)) + if (this.formatMetaData.TryGetValue(key, out IDeepCloneable meta)) { return (TFormatMetaData)meta; } @@ -152,11 +153,8 @@ namespace SixLabors.ImageSharp.MetaData return newMeta; } - /// - /// Clones this into a new instance - /// - /// The cloned metadata instance - public ImageMetaData Clone() => new ImageMetaData(this); + /// + public ImageMetaData DeepClone() => new ImageMetaData(this); /// /// Looks up a property with the provided name. diff --git a/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs index 1dd8857217..b48b146f11 100644 --- a/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs @@ -12,23 +12,18 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif /// /// Represents an EXIF profile providing access to the collection of values. /// - public sealed class ExifProfile + public sealed class ExifProfile : IDeepCloneable { /// /// The byte array to read the EXIF profile from. /// - private byte[] data; + private readonly byte[] data; /// /// The collection of EXIF values /// private List values; - /// - /// The list of invalid EXIF tags - /// - private IReadOnlyList invalidTags; - /// /// The thumbnail offset position in the byte stream /// @@ -55,7 +50,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif { this.Parts = ExifParts.All; this.data = data; - this.invalidTags = new List(); + this.InvalidTags = new List(); } /// @@ -63,22 +58,19 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif /// by making a copy from another EXIF profile. /// /// The other EXIF profile, where the clone should be made from. - /// is null. - public ExifProfile(ExifProfile other) + private ExifProfile(ExifProfile other) { - Guard.NotNull(other, nameof(other)); - this.Parts = other.Parts; this.thumbnailLength = other.thumbnailLength; this.thumbnailOffset = other.thumbnailOffset; - this.invalidTags = new List(other.invalidTags); + this.InvalidTags = new List(other.InvalidTags); if (other.values != null) { this.values = new List(other.Values.Count); foreach (ExifValue value in other.Values) { - this.values.Add(new ExifValue(value)); + this.values.Add(value.DeepClone()); } } @@ -97,7 +89,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif /// /// Gets the tags that where found but contained an invalid value. /// - public IReadOnlyList InvalidTags => this.invalidTags; + public IReadOnlyList InvalidTags { get; private set; } /// /// Gets the values of this EXIF profile. @@ -249,6 +241,9 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif return writer.GetData(); } + /// + public ExifProfile DeepClone() => new ExifProfile(this); + /// /// Synchronizes the profiles with the specified meta data. /// @@ -294,7 +289,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif this.values = reader.ReadValues(); - this.invalidTags = new List(reader.InvalidTags); + this.InvalidTags = new List(reader.InvalidTags); this.thumbnailOffset = (int)reader.ThumbnailOffset; this.thumbnailLength = (int)reader.ThumbnailLength; } diff --git a/src/ImageSharp/MetaData/Profiles/Exif/ExifReader.cs b/src/ImageSharp/MetaData/Profiles/Exif/ExifReader.cs index 549cb3fe09..5f95499088 100644 --- a/src/ImageSharp/MetaData/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/MetaData/Profiles/Exif/ExifReader.cs @@ -127,25 +127,14 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif private unsafe string ConvertToString(ReadOnlySpan buffer) { - Span nullChar = stackalloc byte[1] { 0 }; - - int nullCharIndex = buffer.IndexOf(nullChar); + int nullCharIndex = buffer.IndexOf((byte)0); if (nullCharIndex > -1) { buffer = buffer.Slice(0, nullCharIndex); } -#if NETSTANDARD1_1 - return Encoding.UTF8.GetString(buffer.ToArray(), 0, buffer.Length); -#elif NETCOREAPP2_1 return Encoding.UTF8.GetString(buffer); -#else - fixed (byte* pointer = &MemoryMarshal.GetReference(buffer)) - { - return Encoding.UTF8.GetString(pointer, buffer.Length); - } -#endif } /// diff --git a/src/ImageSharp/MetaData/Profiles/Exif/ExifValue.cs b/src/ImageSharp/MetaData/Profiles/Exif/ExifValue.cs index e6da9b7d1e..ccacfd0bf9 100644 --- a/src/ImageSharp/MetaData/Profiles/Exif/ExifValue.cs +++ b/src/ImageSharp/MetaData/Profiles/Exif/ExifValue.cs @@ -11,15 +11,30 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif /// /// Represent the value of the EXIF profile. /// - public sealed class ExifValue : IEquatable + public sealed class ExifValue : IEquatable, IDeepCloneable { + /// + /// Initializes a new instance of the class. + /// + /// The tag. + /// The data type. + /// The value. + /// Whether the value is an array. + internal ExifValue(ExifTag tag, ExifDataType dataType, object value, bool isArray) + { + this.Tag = tag; + this.DataType = dataType; + this.IsArray = isArray && dataType != ExifDataType.Ascii; + this.Value = value; + } + /// /// Initializes a new instance of the class /// by making a copy from another exif value. /// /// The other exif value, where the clone should be made from. - /// is null. - public ExifValue(ExifValue other) + /// is null. + private ExifValue(ExifValue other) { Guard.NotNull(other, nameof(other)); @@ -29,30 +44,17 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif if (!other.IsArray) { + // All types are value types except for string which is immutable so safe to simply assign. this.Value = other.Value; } else { + // All array types are value types so Clone() is sufficient here. var array = (Array)other.Value; this.Value = array.Clone(); } } - /// - /// Initializes a new instance of the class. - /// - /// The tag. - /// The data type. - /// The value. - /// Whether the value is an array. - internal ExifValue(ExifTag tag, ExifDataType dataType, object value, bool isArray) - { - this.Tag = tag; - this.DataType = dataType; - this.IsArray = isArray && dataType != ExifDataType.Ascii; - this.Value = value; - } - /// /// Gets the data type of the exif value. /// @@ -145,10 +147,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif /// /// True if the parameter is equal to the parameter; otherwise, false. /// - public static bool operator ==(ExifValue left, ExifValue right) - { - return ReferenceEquals(left, right) || left.Equals(right); - } + public static bool operator ==(ExifValue left, ExifValue right) => ReferenceEquals(left, right) || left.Equals(right); /// /// Compares two objects for equality. @@ -162,16 +161,10 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif /// /// True if the parameter is not equal to the parameter; otherwise, false. /// - public static bool operator !=(ExifValue left, ExifValue right) - { - return !(left == right); - } + public static bool operator !=(ExifValue left, ExifValue right) => !(left == right); /// - public override bool Equals(object obj) - { - return obj is ExifValue other && this.Equals(other); - } + public override bool Equals(object obj) => obj is ExifValue other && this.Equals(other); /// public bool Equals(ExifValue other) @@ -187,9 +180,9 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif } return - this.Tag == other.Tag && - this.DataType == other.DataType && - object.Equals(this.Value, other.Value); + this.Tag == other.Tag + && this.DataType == other.DataType + && object.Equals(this.Value, other.Value); } /// @@ -205,10 +198,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif } /// - public override int GetHashCode() - { - return this.GetHashCode(this); - } + public override int GetHashCode() => this.GetHashCode(this); /// public override string ToString() @@ -238,6 +228,9 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif return sb.ToString(); } + /// + public ExifValue DeepClone() => new ExifValue(this); + /// /// Creates a new /// @@ -584,7 +577,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif private static ExifValue CreateNumber(ExifTag tag, object value, bool isArray) { Type type = value?.GetType(); - if (type != null && type.IsArray) + if (type?.IsArray == true) { type = type.GetElementType(); } diff --git a/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs b/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs index dac56c608e..44990b7ecc 100644 --- a/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Icc /// /// Represents an ICC profile /// - public sealed class IccProfile + public sealed class IccProfile : IDeepCloneable { /// /// The byte array to read the ICC profile from @@ -42,23 +42,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Icc /// Initializes a new instance of the class. /// /// The raw ICC profile data - public IccProfile(byte[] data) - { - this.data = data; - } - - /// - /// Initializes a new instance of the class - /// by making a copy from another ICC profile. - /// - /// The other ICC profile, where the clone should be made from. - /// is null.> - public IccProfile(IccProfile other) - { - Guard.NotNull(other, nameof(other)); - - this.data = other.ToByteArray(); - } + public IccProfile(byte[] data) => this.data = data; /// /// Initializes a new instance of the class. @@ -74,6 +58,19 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Icc this.entries = new List(entries); } + /// + /// Initializes a new instance of the class + /// by making a copy from another ICC profile. + /// + /// The other ICC profile, where the clone should be made from. + /// is null.> + private IccProfile(IccProfile other) + { + Guard.NotNull(other, nameof(other)); + + this.data = other.ToByteArray(); + } + /// /// Gets or sets the profile header /// @@ -100,6 +97,9 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Icc } } + /// + public IccProfile DeepClone() => new IccProfile(this); + #if !NETSTANDARD1_1 /// diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs index c4f4266d98..60754b3bf2 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs @@ -2,10 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Threading.Tasks; + using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Binarization @@ -56,7 +56,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization public TPixel LowerColor { get; set; } /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + protected override void OnFrameApply( + ImageFrame source, + Rectangle sourceRectangle, + Configuration configuration) { float threshold = this.Threshold * 255F; TPixel upper = this.UpperColor; @@ -70,25 +73,29 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); - ParallelFor.WithConfiguration( - startY, - endY, + var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); + + ParallelHelper.IterateRows( + workingRect, configuration, - y => + rows => { - Span row = source.GetPixelRowSpan(y); - Rgba32 rgba = default; - - for (int x = startX; x < endX; x++) + for (int y = rows.Min; y < rows.Max; y++) { - ref TPixel color = ref row[x]; - color.ToRgba32(ref rgba); + Span row = source.GetPixelRowSpan(y); + Rgba32 rgba = default; + + for (int x = startX; x < endX; x++) + { + ref TPixel color = ref row[x]; + color.ToRgba32(ref rgba); - // Convert to grayscale using ITU-R Recommendation BT.709 if required - float luminance = isAlphaOnly - ? rgba.A - : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B); - color = luminance >= threshold ? upper : lower; + // Convert to grayscale using ITU-R Recommendation BT.709 if required + float luminance = isAlphaOnly + ? rgba.A + : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B); + color = luminance >= threshold ? upper : lower; + } } }); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs index b5a2725437..d2282ec0e1 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs @@ -3,12 +3,12 @@ using System; using System.Numerics; -using System.Threading.Tasks; + using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Primitives; -using SixLabors.Memory; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Convolution @@ -42,7 +42,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution public DenseMatrix KernelY { get; } /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + protected override void OnFrameApply( + ImageFrame source, + Rectangle sourceRectangle, + Configuration configuration) { int kernelYHeight = this.KernelY.Rows; int kernelYWidth = this.KernelY.Columns; @@ -58,71 +61,77 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution int maxY = endY - 1; int maxX = endX - 1; - using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) + using (Buffer2D targetPixels = + configuration.MemoryAllocator.Allocate2D(source.Width, source.Height)) { source.CopyTo(targetPixels); - ParallelFor.WithConfiguration( - startY, - endY, - configuration, - y => - { - Span sourceRow = source.GetPixelRowSpan(y); - Span targetRow = targetPixels.GetRowSpan(y); + var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - for (int x = startX; x < endX; x++) + ParallelHelper.IterateRows( + workingRectangle, + configuration, + rows => { - float rX = 0; - float gX = 0; - float bX = 0; - float rY = 0; - float gY = 0; - float bY = 0; - - // Apply each matrix multiplier to the color components for each pixel. - for (int fy = 0; fy < kernelYHeight; fy++) + for (int y = rows.Min; y < rows.Max; y++) { - int fyr = fy - radiusY; - int offsetY = y + fyr; + Span sourceRow = source.GetPixelRowSpan(y); + Span targetRow = targetPixels.GetRowSpan(y); - offsetY = offsetY.Clamp(0, maxY); - Span sourceOffsetRow = source.GetPixelRowSpan(offsetY); - - for (int fx = 0; fx < kernelXWidth; fx++) + for (int x = startX; x < endX; x++) { - int fxr = fx - radiusX; - int offsetX = x + fxr; - - offsetX = offsetX.Clamp(0, maxX); - Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply(); - - if (fy < kernelXHeight) + float rX = 0; + float gX = 0; + float bX = 0; + float rY = 0; + float gY = 0; + float bY = 0; + + // Apply each matrix multiplier to the color components for each pixel. + for (int fy = 0; fy < kernelYHeight; fy++) { - Vector4 kx = this.KernelX[fy, fx] * currentColor; - rX += kx.X; - gX += kx.Y; - bX += kx.Z; + int fyr = fy - radiusY; + int offsetY = y + fyr; + + offsetY = offsetY.Clamp(0, maxY); + Span sourceOffsetRow = source.GetPixelRowSpan(offsetY); + + for (int fx = 0; fx < kernelXWidth; fx++) + { + int fxr = fx - radiusX; + int offsetX = x + fxr; + + offsetX = offsetX.Clamp(0, maxX); + Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply(); + + if (fy < kernelXHeight) + { + Vector4 kx = this.KernelX[fy, fx] * currentColor; + rX += kx.X; + gX += kx.Y; + bX += kx.Z; + } + + if (fx < kernelYWidth) + { + Vector4 ky = this.KernelY[fy, fx] * currentColor; + rY += ky.X; + gY += ky.Y; + bY += ky.Z; + } + } } - if (fx < kernelYWidth) - { - Vector4 ky = this.KernelY[fy, fx] * currentColor; - rY += ky.X; - gY += ky.Y; - bY += ky.Z; - } + float red = MathF.Sqrt((rX * rX) + (rY * rY)); + float green = MathF.Sqrt((gX * gX) + (gY * gY)); + float blue = MathF.Sqrt((bX * bX) + (bY * bY)); + + ref TPixel pixel = ref targetRow[x]; + pixel.PackFromVector4( + new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply()); } } - - float red = MathF.Sqrt((rX * rX) + (rY * rY)); - float green = MathF.Sqrt((gX * gX) + (gY * gY)); - float blue = MathF.Sqrt((bX * bX) + (bY * bY)); - - ref TPixel pixel = ref targetRow[x]; - pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply()); - } - }); + }); Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs index 0808c07d03..e45bb3ab2e 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs @@ -6,6 +6,7 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing.Processors; @@ -82,43 +83,47 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution int maxY = endY - 1; int maxX = endX - 1; - ParallelFor.WithConfiguration( - startY, - endY, - configuration, - y => - { - Span targetRow = targetPixels.GetRowSpan(y); + var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY); - for (int x = startX; x < endX; x++) + ParallelHelper.IterateRows( + workingRectangle, + configuration, + rows => { - Vector4 destination = default; - - // Apply each matrix multiplier to the color components for each pixel. - for (int fy = 0; fy < kernelHeight; fy++) + for (int y = rows.Min; y < rows.Max; y++) { - int fyr = fy - radiusY; - int offsetY = y + fyr; + Span targetRow = targetPixels.GetRowSpan(y); - offsetY = offsetY.Clamp(0, maxY); - Span row = sourcePixels.GetRowSpan(offsetY); - - for (int fx = 0; fx < kernelWidth; fx++) + for (int x = startX; x < endX; x++) { - int fxr = fx - radiusX; - int offsetX = x + fxr; + Vector4 destination = default; + + // Apply each matrix multiplier to the color components for each pixel. + for (int fy = 0; fy < kernelHeight; fy++) + { + int fyr = fy - radiusY; + int offsetY = y + fyr; - offsetX = offsetX.Clamp(0, maxX); + offsetY = offsetY.Clamp(0, maxY); + Span row = sourcePixels.GetRowSpan(offsetY); - Vector4 currentColor = row[offsetX].ToVector4().Premultiply(); - destination += kernel[fy, fx] * currentColor; + for (int fx = 0; fx < kernelWidth; fx++) + { + int fxr = fx - radiusX; + int offsetX = x + fxr; + + offsetX = offsetX.Clamp(0, maxX); + + Vector4 currentColor = row[offsetX].ToVector4().Premultiply(); + destination += kernel[fy, fx] * currentColor; + } + } + + ref TPixel pixel = ref targetRow[x]; + pixel.PackFromVector4(destination.UnPremultiply()); } } - - ref TPixel pixel = ref targetRow[x]; - pixel.PackFromVector4(destination.UnPremultiply()); - } - }); + }); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs index 31e638a0ad..bac9a86cfe 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs @@ -6,6 +6,7 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing.Processors; @@ -52,50 +53,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution { source.CopyTo(targetPixels); - ParallelFor.WithConfiguration( - startY, - endY, - configuration, - y => - { - Span sourceRow = source.GetPixelRowSpan(y); - Span targetRow = targetPixels.GetRowSpan(y); - - for (int x = startX; x < endX; x++) - { - float red = 0; - float green = 0; - float blue = 0; - - // Apply each matrix multiplier to the color components for each pixel. - for (int fy = 0; fy < kernelLength; fy++) - { - int fyr = fy - radius; - int offsetY = y + fyr; - - offsetY = offsetY.Clamp(0, maxY); - Span sourceOffsetRow = source.GetPixelRowSpan(offsetY); - - for (int fx = 0; fx < kernelLength; fx++) - { - int fxr = fx - radius; - int offsetX = x + fxr; - - offsetX = offsetX.Clamp(0, maxX); - - Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply(); - currentColor *= this.KernelXY[fy, fx]; - - red += currentColor.X; - green += currentColor.Y; - blue += currentColor.Z; - } - } - - ref TPixel pixel = ref targetRow[x]; - pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply()); - } - }); + var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); + + ParallelHelper.IterateRows( + workingRect, + configuration, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span sourceRow = source.GetPixelRowSpan(y); + Span targetRow = targetPixels.GetRowSpan(y); + + for (int x = startX; x < endX; x++) + { + float red = 0; + float green = 0; + float blue = 0; + + // Apply each matrix multiplier to the color components for each pixel. + for (int fy = 0; fy < kernelLength; fy++) + { + int fyr = fy - radius; + int offsetY = y + fyr; + + offsetY = offsetY.Clamp(0, maxY); + Span sourceOffsetRow = source.GetPixelRowSpan(offsetY); + + for (int fx = 0; fx < kernelLength; fx++) + { + int fxr = fx - radius; + int offsetX = x + fxr; + + offsetX = offsetX.Clamp(0, maxX); + + Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply(); + currentColor *= this.KernelXY[fy, fx]; + + red += currentColor.X; + green += currentColor.Y; + blue += currentColor.Z; + } + } + + ref TPixel pixel = ref targetRow[x]; + pixel.PackFromVector4( + new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply()); + } + } + }); Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs index 316de422f5..ebf9c8dec2 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing.Processors.Filters; @@ -124,6 +125,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution shiftY = 0; } + var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); + // Additional runs. // ReSharper disable once ForCanBeConvertedToForeach for (int i = 1; i < kernels.Length; i++) @@ -135,30 +138,35 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution Buffer2D passPixels = pass.PixelBuffer; Buffer2D targetPixels = source.PixelBuffer; - ParallelFor.WithConfiguration( - minY, - maxY, + ParallelHelper.IterateRows( + workingRect, configuration, - y => + rows => { - int offsetY = y - shiftY; + for (int y = rows.Min; y < rows.Max; y++) + { + int offsetY = y - shiftY; - ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(passPixels.GetRowSpan(offsetY)); - ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(targetPixels.GetRowSpan(offsetY)); + ref TPixel passPixelsBase = + ref MemoryMarshal.GetReference(passPixels.GetRowSpan(offsetY)); + ref TPixel targetPixelsBase = + ref MemoryMarshal.GetReference(targetPixels.GetRowSpan(offsetY)); - for (int x = minX; x < maxX; x++) - { - int offsetX = x - shiftX; + for (int x = minX; x < maxX; x++) + { + int offsetX = x - shiftX; - // Grab the max components of the two pixels - ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, offsetX); - ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, offsetX); + // Grab the max components of the two pixels + ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, offsetX); + ref TPixel currentTargetPixel = + ref Unsafe.Add(ref targetPixelsBase, offsetX); - var pixelValue = Vector4.Max( - currentPassPixel.ToVector4(), - currentTargetPixel.ToVector4()); + var pixelValue = Vector4.Max( + currentPassPixel.ToVector4(), + currentTargetPixel.ToVector4()); - currentTargetPixel.PackFromVector4(pixelValue); + currentTargetPixel.PackFromVector4(pixelValue); + } } }); } diff --git a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs index 59898e9fc1..6ad4dcba97 100644 --- a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs @@ -3,11 +3,11 @@ using System; using System.Numerics; -using System.Threading.Tasks; + using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Memory; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Effects @@ -49,7 +49,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects public int BrushSize { get; } /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + protected override void OnFrameApply( + ImageFrame source, + Rectangle sourceRectangle, + Configuration configuration) { if (this.BrushSize <= 0 || this.BrushSize > source.Height || this.BrushSize > source.Width) { @@ -70,69 +73,74 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects { source.CopyTo(targetPixels); - ParallelFor.WithConfiguration( - startY, - maxY, + var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY); + ParallelHelper.IterateRows( + workingRect, configuration, - y => - { - Span sourceRow = source.GetPixelRowSpan(y); - Span targetRow = targetPixels.GetRowSpan(y); - - for (int x = startX; x < endX; x++) + rows => { - int maxIntensity = 0; - int maxIndex = 0; + for (int y = rows.Min; y < rows.Max; y++) + { + Span sourceRow = source.GetPixelRowSpan(y); + Span targetRow = targetPixels.GetRowSpan(y); - int[] intensityBin = new int[levels]; - float[] redBin = new float[levels]; - float[] blueBin = new float[levels]; - float[] greenBin = new float[levels]; + for (int x = startX; x < endX; x++) + { + int maxIntensity = 0; + int maxIndex = 0; - for (int fy = 0; fy <= radius; fy++) - { - int fyr = fy - radius; - int offsetY = y + fyr; + int[] intensityBin = new int[levels]; + float[] redBin = new float[levels]; + float[] blueBin = new float[levels]; + float[] greenBin = new float[levels]; - offsetY = offsetY.Clamp(0, maxY); + for (int fy = 0; fy <= radius; fy++) + { + int fyr = fy - radius; + int offsetY = y + fyr; - Span sourceOffsetRow = source.GetPixelRowSpan(offsetY); + offsetY = offsetY.Clamp(0, maxY); - for (int fx = 0; fx <= radius; fx++) - { - int fxr = fx - radius; - int offsetX = x + fxr; - offsetX = offsetX.Clamp(0, maxX); + Span sourceOffsetRow = source.GetPixelRowSpan(offsetY); - var vector = sourceOffsetRow[offsetX].ToVector4(); + for (int fx = 0; fx <= radius; fx++) + { + int fxr = fx - radius; + int offsetX = x + fxr; + offsetX = offsetX.Clamp(0, maxX); - float sourceRed = vector.X; - float sourceBlue = vector.Z; - float sourceGreen = vector.Y; + var vector = sourceOffsetRow[offsetX].ToVector4(); - int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (levels - 1)); + float sourceRed = vector.X; + float sourceBlue = vector.Z; + float sourceGreen = vector.Y; - intensityBin[currentIntensity]++; - blueBin[currentIntensity] += sourceBlue; - greenBin[currentIntensity] += sourceGreen; - redBin[currentIntensity] += sourceRed; + int currentIntensity = (int)MathF.Round( + (sourceBlue + sourceGreen + sourceRed) / 3F * (levels - 1)); - if (intensityBin[currentIntensity] > maxIntensity) - { - maxIntensity = intensityBin[currentIntensity]; - maxIndex = currentIntensity; - } - } + intensityBin[currentIntensity]++; + blueBin[currentIntensity] += sourceBlue; + greenBin[currentIntensity] += sourceGreen; + redBin[currentIntensity] += sourceRed; - float red = MathF.Abs(redBin[maxIndex] / maxIntensity); - float green = MathF.Abs(greenBin[maxIndex] / maxIntensity); - float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity); + if (intensityBin[currentIntensity] > maxIntensity) + { + maxIntensity = intensityBin[currentIntensity]; + maxIndex = currentIntensity; + } + } - ref TPixel pixel = ref targetRow[x]; - pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W)); + float red = MathF.Abs(redBin[maxIndex] / maxIntensity); + float green = MathF.Abs(greenBin[maxIndex] / maxIntensity); + float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity); + + ref TPixel pixel = ref targetRow[x]; + pixel.PackFromVector4( + new Vector4(red, green, blue, sourceRow[x].ToVector4().W)); + } + } } - } - }); + }); Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } diff --git a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs index 6244d8bf76..e20b42eb7c 100644 --- a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -35,25 +36,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); - int startY = interest.Y; - int endY = interest.Bottom; - int startX = interest.X; - int endX = interest.Right; + Matrix4x4 matrix = this.Matrix; - ParallelFor.WithConfiguration( - startY, - endY, + ParallelHelper.IterateRows( + interest, configuration, - y => + rows => { - Span row = source.GetPixelRowSpan(y); - - for (int x = startX; x < endX; x++) + for (int y = rows.Min; y < rows.Max; y++) { - ref TPixel pixel = ref row[x]; - var vector = Vector4.Transform(pixel.ToVector4(), matrix); - pixel.PackFromVector4(vector); + Span row = source.GetPixelRowSpan(y); + + for (int x = interest.X; x < interest.Right; x++) + { + ref TPixel pixel = ref row[x]; + var vector = Vector4.Transform(pixel.ToVector4(), matrix); + pixel.PackFromVector4(vector); + } } }); } diff --git a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs index ecbeebeb06..4adddd1536 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs @@ -6,6 +6,7 @@ using System.Buffers; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -67,6 +68,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays int width = maxX - minX; + var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); + using (IMemoryOwner colors = source.MemoryAllocator.Allocate(width)) using (IMemoryOwner amount = source.MemoryAllocator.Allocate(width)) { @@ -74,25 +77,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays Span colorSpan = colors.GetSpan(); Span amountSpan = amount.GetSpan(); - // TODO: Use Span.Fill? - for (int i = 0; i < width; i++) - { - colorSpan[i] = this.Color; - amountSpan[i] = this.GraphicsOptions.BlendPercentage; - } + colorSpan.Fill(this.Color); + amountSpan.Fill(this.GraphicsOptions.BlendPercentage); PixelBlender blender = PixelOperations.Instance.GetPixelBlender(this.GraphicsOptions); - ParallelFor.WithConfiguration( - minY, - maxY, + + ParallelHelper.IterateRows( + workingRect, configuration, - y => - { - Span destination = source.GetPixelRowSpan(y - startY).Slice(minX - startX, width); + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span destination = + source.GetPixelRowSpan(y - startY).Slice(minX - startX, width); - // This switched color & destination in the 2nd and 3rd places because we are applying the target color under the current one - blender.Blend(source.MemoryAllocator, destination, colors.GetSpan(), destination, amount.GetSpan()); - }); + // This switched color & destination in the 2nd and 3rd places because we are applying the target color under the current one + blender.Blend( + source.MemoryAllocator, + destination, + colors.GetSpan(), + destination, + amount.GetSpan()); + } + }); } } } diff --git a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs index eb91fec043..93d6edff19 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs @@ -7,6 +7,7 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Primitives; using SixLabors.Memory; @@ -84,6 +85,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { + // TODO: can we simplify the rectangle calculation? int startY = sourceRectangle.Y; int endY = sourceRectangle.Bottom; int startX = sourceRectangle.X; @@ -113,36 +115,43 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays } int width = maxX - minX; + int offsetX = minX - startX; + + var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); + using (IMemoryOwner rowColors = source.MemoryAllocator.Allocate(width)) { - // Be careful! Do not capture rowColorsSpan in the lambda below! - Span rowColorsSpan = rowColors.GetSpan(); - - for (int i = 0; i < width; i++) - { - rowColorsSpan[i] = glowColor; - } + rowColors.GetSpan().Fill(glowColor); - ParallelFor.WithTemporaryBuffer( - minY, - maxY, + ParallelHelper.IterateRowsWithTempBuffer( + workingRect, configuration, - width, - (y, amounts) => - { - Span amountsSpan = amounts.GetSpan(); - int offsetY = y - startY; - int offsetX = minX - startX; - for (int i = 0; i < width; i++) + (rows, amounts) => { - float distance = Vector2.Distance(center, new Vector2(i + offsetX, offsetY)); - amountsSpan[i] = (this.GraphicsOptions.BlendPercentage * (1 - (.95F * (distance / maxDistance)))).Clamp(0, 1); - } - - Span destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width); - - this.blender.Blend(source.MemoryAllocator, destination, destination, rowColors.GetSpan(), amountsSpan); - }); + Span amountsSpan = amounts.Span; + + for (int y = rows.Min; y < rows.Max; y++) + { + int offsetY = y - startY; + + for (int i = 0; i < width; i++) + { + float distance = Vector2.Distance(center, new Vector2(i + offsetX, offsetY)); + amountsSpan[i] = + (this.GraphicsOptions.BlendPercentage * (1 - (.95F * (distance / maxDistance)))) + .Clamp(0, 1); + } + + Span destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width); + + this.blender.Blend( + source.MemoryAllocator, + destination, + destination, + rowColors.GetSpan(), + amountsSpan); + } + }); } } } diff --git a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs index 63780df476..52dade4eff 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs @@ -7,6 +7,7 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Primitives; using SixLabors.Memory; @@ -115,43 +116,43 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays } int width = maxX - minX; + int offsetX = minX - startX; + + var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); + using (IMemoryOwner rowColors = source.MemoryAllocator.Allocate(width)) { - // Be careful! Do not capture rowColorsSpan in the lambda below! - Span rowColorsSpan = rowColors.GetSpan(); - - for (int i = 0; i < width; i++) - { - rowColorsSpan[i] = vignetteColor; - } + rowColors.GetSpan().Fill(vignetteColor); - ParallelFor.WithTemporaryBuffer( - minY, - maxY, + ParallelHelper.IterateRowsWithTempBuffer( + workingRect, configuration, - width, - (y, amounts) => + (rows, amounts) => { - Span amountsSpan = amounts.GetSpan(); - int offsetY = y - startY; - int offsetX = minX - startX; - for (int i = 0; i < width; i++) + Span amountsSpan = amounts.Span; + + for (int y = rows.Min; y < rows.Max; y++) { - float distance = Vector2.Distance(centre, new Vector2(i + offsetX, offsetY)); - amountsSpan[i] = - (this.GraphicsOptions.BlendPercentage * (.9F * (distance / maxDistance))).Clamp( - 0, - 1); + int offsetY = y - startY; + + for (int i = 0; i < width; i++) + { + float distance = Vector2.Distance(centre, new Vector2(i + offsetX, offsetY)); + amountsSpan[i] = + (this.GraphicsOptions.BlendPercentage * (.9F * (distance / maxDistance))).Clamp( + 0, + 1); + } + + Span destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width); + + this.blender.Blend( + source.MemoryAllocator, + destination, + destination, + rowColors.GetSpan(), + amountsSpan); } - - Span destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width); - - this.blender.Blend( - source.MemoryAllocator, - destination, - destination, - rowColors.GetSpan(), - amountsSpan); }); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs index 2e3bb2c419..38862ef446 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs @@ -3,7 +3,7 @@ using System; using System.Buffers; - +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; @@ -57,8 +57,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Gets the pixels of this . /// /// The + [MethodImpl(InliningOptions.ShortMethod)] public Span GetPixelSpan() => this.pixels.GetSpan(); + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the the first pixel on that row. + /// + /// The row. + /// The + [MethodImpl(InliningOptions.ShortMethod)] + public Span GetRowSpan(int rowIndex) => this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width); + /// public void Dispose() { diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs index 3993ab1a8d..3469161e6d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -51,10 +52,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { // We will always be creating the clone even for mutate because we may need to resize the canvas IEnumerable> frames = - source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.TargetDimensions, x.MetaData.Clone())); + source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.TargetDimensions, x.MetaData.DeepClone())); // Use the overload to prevent an extra frame being added - return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); + return new Image(source.GetConfiguration(), source.MetaData.DeepClone(), frames); } /// @@ -78,23 +79,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms if (this.Sampler is NearestNeighborResampler) { - ParallelFor.WithConfiguration( - 0, - height, + ParallelHelper.IterateRows( + targetBounds, configuration, - y => - { - Span destRow = destination.GetPixelRowSpan(y); - - for (int x = 0; x < width; x++) + rows => { - var point = Point.Transform(new Point(x, y), matrix); - if (sourceBounds.Contains(point.X, point.Y)) + for (int y = rows.Min; y < rows.Max; y++) { - destRow[x] = source[point.X, point.Y]; + Span destRow = destination.GetPixelRowSpan(y); + + for (int x = 0; x < width; x++) + { + var point = Point.Transform(new Point(x, y), matrix); + if (sourceBounds.Contains(point.X, point.Y)) + { + destRow[x] = source[point.X, point.Y]; + } + } } - } - }); + }); return; } @@ -116,86 +119,107 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) { - ParallelFor.WithConfiguration( - 0, - height, + ParallelHelper.IterateRows( + targetBounds, configuration, - y => + rows => { - ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); - ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); - - for (int x = 0; x < width; x++) + for (int y = rows.Min; y < rows.Max; y++) { - // Use the single precision position to calculate correct bounding pixels - // otherwise we get rogue pixels outside of the bounds. - var point = Vector2.Transform(new Vector2(x, y), matrix); - - // Clamp sampling pixel radial extents to the source image edges - Vector2 maxXY = point + radius; - Vector2 minXY = point - radius; - - // max, maxY, minX, minY - var extents = new Vector4( - MathF.Floor(maxXY.X + .5F), - MathF.Floor(maxXY.Y + .5F), - MathF.Ceiling(minXY.X - .5F), - MathF.Ceiling(minXY.Y - .5F)); - - int right = (int)extents.X; - int bottom = (int)extents.Y; - int left = (int)extents.Z; - int top = (int)extents.W; - - extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); - - int maxX = (int)extents.X; - int maxY = (int)extents.Y; - int minX = (int)extents.Z; - int minY = (int)extents.W; - - if (minX == maxX || minY == maxY) - { - continue; - } + ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); + ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); + ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); - // It appears these have to be calculated on-the-fly. - // Precalulating transformed weights would require prior knowledge of every transformed pixel location - // since they can be at sub-pixel positions on both axis. - // I've optimized where I can but am always open to suggestions. - if (yScale > 1 && xScale > 1) - { - CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ref ySpanRef, yLength); - CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, ref xSpanRef, xLength); - } - else + for (int x = 0; x < width; x++) { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); - } + // Use the single precision position to calculate correct bounding pixels + // otherwise we get rogue pixels outside of the bounds. + var point = Vector2.Transform(new Vector2(x, y), matrix); + + // Clamp sampling pixel radial extents to the source image edges + Vector2 maxXY = point + radius; + Vector2 minXY = point - radius; + + // max, maxY, minX, minY + var extents = new Vector4( + MathF.Floor(maxXY.X + .5F), + MathF.Floor(maxXY.Y + .5F), + MathF.Ceiling(minXY.X - .5F), + MathF.Ceiling(minXY.Y - .5F)); + + int right = (int)extents.X; + int bottom = (int)extents.Y; + int left = (int)extents.Z; + int top = (int)extents.W; + + extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); + + int maxX = (int)extents.X; + int maxY = (int)extents.Y; + int minX = (int)extents.Z; + int minY = (int)extents.W; + + if (minX == maxX || minY == maxY) + { + continue; + } - // Now multiply the results against the offsets - Vector4 sum = Vector4.Zero; - for (int yy = 0, j = minY; j <= maxY; j++, yy++) - { - float yWeight = Unsafe.Add(ref ySpanRef, yy); + // It appears these have to be calculated on-the-fly. + // Precalculating transformed weights would require prior knowledge of every transformed pixel location + // since they can be at sub-pixel positions on both axis. + // I've optimized where I can but am always open to suggestions. + if (yScale > 1 && xScale > 1) + { + CalculateWeightsDown( + top, + bottom, + minY, + maxY, + point.Y, + sampler, + yScale, + ref ySpanRef, + yLength); + + CalculateWeightsDown( + left, + right, + minX, + maxX, + point.X, + sampler, + xScale, + ref xSpanRef, + xLength); + } + else + { + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); + CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); + } - for (int xx = 0, i = minX; i <= maxX; i++, xx++) + // Now multiply the results against the offsets + Vector4 sum = Vector4.Zero; + for (int yy = 0, j = minY; j <= maxY; j++, yy++) { - float xWeight = Unsafe.Add(ref xSpanRef, xx); - var vector = source[i, j].ToVector4(); + float yWeight = Unsafe.Add(ref ySpanRef, yy); + + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float xWeight = Unsafe.Add(ref xSpanRef, xx); + var vector = source[i, j].ToVector4(); - // Values are first premultiplied to prevent darkening of edge pixels - Vector4 multiplied = vector.Premultiply(); - sum += multiplied * xWeight * yWeight; + // Values are first premultiplied to prevent darkening of edge pixels + Vector4 multiplied = vector.Premultiply(); + sum += multiplied * xWeight * yWeight; + } } - } - ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); + ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); - // Reverse the premultiplication - dest.PackFromVector4(sum.UnPremultiply()); + // Reverse the premultiplication + dest.PackFromVector4(sum.UnPremultiply()); + } } }); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs index 0c52123755..8e6a826fd3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -36,10 +36,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { // We will always be creating the clone even for mutate because we may need to resize the canvas - IEnumerable> frames = source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.CropRectangle.Width, this.CropRectangle.Height, x.MetaData.Clone())); + IEnumerable> frames = source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.CropRectangle.Width, this.CropRectangle.Height, x.MetaData.DeepClone())); // Use the overload to prevent an extra frame being added - return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); + return new Image(source.GetConfiguration(), source.MetaData.DeepClone(), frames); } /// @@ -53,21 +53,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return; } - int minY = Math.Max(this.CropRectangle.Y, sourceRectangle.Y); - int maxY = Math.Min(this.CropRectangle.Bottom, sourceRectangle.Bottom); - int minX = Math.Max(this.CropRectangle.X, sourceRectangle.X); - int maxX = Math.Min(this.CropRectangle.Right, sourceRectangle.Right); + var rect = Rectangle.Intersect(this.CropRectangle, sourceRectangle); - ParallelFor.WithConfiguration( - minY, - maxY, - configuration, - y => - { - Span sourceRow = source.GetPixelRowSpan(y).Slice(minX); - Span targetRow = destination.GetPixelRowSpan(y - minY); - sourceRow.Slice(0, maxX - minX).CopyTo(targetRow); - }); + // Copying is cheap, we should process more pixels per task: + ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4); + + ParallelHelper.IterateRows( + rect, + parallelSettings, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span sourceRow = source.GetPixelRowSpan(y).Slice(rect.Left); + Span targetRow = destination.GetPixelRowSpan(y - rect.Top); + sourceRow.Slice(0, rect.Width).CopyTo(targetRow); + } + }); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/FlipProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/FlipProcessor.cs index cea6df391f..c6f5e9d7b8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/FlipProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/FlipProcessor.cs @@ -2,9 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -55,27 +57,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private void FlipX(ImageFrame source, Configuration configuration) { int height = source.Height; - int halfHeight = (int)Math.Ceiling(source.Height * .5F); - using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Size())) + using (IMemoryOwner tempBuffer = configuration.MemoryAllocator.Allocate(source.Width)) { - ParallelFor.WithConfiguration( - 0, - halfHeight, - configuration, - y => - { - int newY = height - y - 1; - Span sourceRow = source.GetPixelRowSpan(y); - Span altSourceRow = source.GetPixelRowSpan(newY); - Span targetRow = targetPixels.GetRowSpan(y); - Span altTargetRow = targetPixels.GetRowSpan(newY); - - sourceRow.CopyTo(altTargetRow); - altSourceRow.CopyTo(targetRow); - }); + Span temp = tempBuffer.Memory.Span; - Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + for (int yTop = 0; yTop < height / 2; yTop++) + { + int yBottom = height - yTop - 1; + Span topRow = source.GetPixelRowSpan(yBottom); + Span bottomRow = source.GetPixelRowSpan(yTop); + topRow.CopyTo(temp); + bottomRow.CopyTo(topRow); + temp.CopyTo(bottomRow); + } } } @@ -86,31 +81,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The configuration. private void FlipY(ImageFrame source, Configuration configuration) { - int width = source.Width; - int height = source.Height; - int halfWidth = (int)Math.Ceiling(width * .5F); - - using (Buffer2D targetPixels = configuration.MemoryAllocator.Allocate2D(source.Size())) - { - ParallelFor.WithConfiguration( - 0, - height, - configuration, - y => + ParallelHelper.IterateRows( + source.Bounds(), + configuration, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) { - Span sourceRow = source.GetPixelRowSpan(y); - Span targetRow = targetPixels.GetRowSpan(y); - - for (int x = 0; x < halfWidth; x++) - { - int newX = width - x - 1; - targetRow[x] = sourceRow[newX]; - targetRow[newX] = sourceRow[x]; - } - }); - - Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); - } + source.GetPixelRowSpan(y).Reverse(); + } + }); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index 042ce2ff6d..b03dec032f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -51,10 +52,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { // We will always be creating the clone even for mutate because we may need to resize the canvas IEnumerable> frames = - source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.TargetDimensions.Width, this.TargetDimensions.Height, x.MetaData.Clone())); + source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.TargetDimensions.Width, this.TargetDimensions.Height, x.MetaData.DeepClone())); // Use the overload to prevent an extra frame being added - return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); + return new Image(source.GetConfiguration(), source.MetaData.DeepClone(), frames); } /// @@ -75,28 +76,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms if (this.Sampler is NearestNeighborResampler) { - ParallelFor.WithConfiguration( - 0, - height, + ParallelHelper.IterateRows( + targetBounds, configuration, - y => - { - Span destRow = destination.GetPixelRowSpan(y); - - for (int x = 0; x < width; x++) + rows => { - var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); + for (int y = rows.Min; y < rows.Max; y++) + { + Span destRow = destination.GetPixelRowSpan(y); - float z = MathF.Max(v3.Z, Epsilon); - int px = (int)MathF.Round(v3.X / z); - int py = (int)MathF.Round(v3.Y / z); + for (int x = 0; x < width; x++) + { + var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); - if (sourceBounds.Contains(px, py)) - { - destRow[x] = source[px, py]; + float z = MathF.Max(v3.Z, Epsilon); + int px = (int)MathF.Round(v3.X / z); + int py = (int)MathF.Round(v3.Y / z); + + if (sourceBounds.Contains(px, py)) + { + destRow[x] = source[px, py]; + } + } } - } - }); + }); return; } @@ -121,92 +124,113 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms using (Buffer2D yBuffer = memoryAllocator.Allocate2D(yLength, height)) using (Buffer2D xBuffer = memoryAllocator.Allocate2D(xLength, height)) { - ParallelFor.WithConfiguration( - 0, - height, + ParallelHelper.IterateRows( + targetBounds, configuration, - y => - { - ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); - ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); - - for (int x = 0; x < width; x++) + rows => { - // Use the single precision position to calculate correct bounding pixels - // otherwise we get rogue pixels outside of the bounds. - var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); - float z = MathF.Max(v3.Z, Epsilon); - - // Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable: - Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z; - - // Clamp sampling pixel radial extents to the source image edges - Vector4 maxXY = point + radius; - Vector4 minXY = point - radius; - - // max, maxY, minX, minY - var extents = new Vector4( - MathF.Floor(maxXY.X + .5F), - MathF.Floor(maxXY.Y + .5F), - MathF.Ceiling(minXY.X - .5F), - MathF.Ceiling(minXY.Y - .5F)); - - int right = (int)extents.X; - int bottom = (int)extents.Y; - int left = (int)extents.Z; - int top = (int)extents.W; - - extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); - - int maxX = (int)extents.X; - int maxY = (int)extents.Y; - int minX = (int)extents.Z; - int minY = (int)extents.W; - - if (minX == maxX || minY == maxY) - { - continue; - } - - // It appears these have to be calculated on-the-fly. - // Precalulating transformed weights would require prior knowledge of every transformed pixel location - // since they can be at sub-pixel positions on both axis. - // I've optimized where I can but am always open to suggestions. - if (yScale > 1 && xScale > 1) - { - CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ref ySpanRef, yLength); - CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, ref xSpanRef, xLength); - } - else + for (int y = rows.Min; y < rows.Max; y++) { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); - } + ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); + ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); + ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); - // Now multiply the results against the offsets - Vector4 sum = Vector4.Zero; - for (int yy = 0, j = minY; j <= maxY; j++, yy++) - { - float yWeight = Unsafe.Add(ref ySpanRef, yy); - - for (int xx = 0, i = minX; i <= maxX; i++, xx++) + for (int x = 0; x < width; x++) { - float xWeight = Unsafe.Add(ref xSpanRef, xx); - var vector = source[i, j].ToVector4(); - - // Values are first premultiplied to prevent darkening of edge pixels - Vector4 multiplied = vector.Premultiply(); - sum += multiplied * xWeight * yWeight; + // Use the single precision position to calculate correct bounding pixels + // otherwise we get rogue pixels outside of the bounds. + var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); + float z = MathF.Max(v3.Z, Epsilon); + + // Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable: + Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z; + + // Clamp sampling pixel radial extents to the source image edges + Vector4 maxXY = point + radius; + Vector4 minXY = point - radius; + + // max, maxY, minX, minY + var extents = new Vector4( + MathF.Floor(maxXY.X + .5F), + MathF.Floor(maxXY.Y + .5F), + MathF.Ceiling(minXY.X - .5F), + MathF.Ceiling(minXY.Y - .5F)); + + int right = (int)extents.X; + int bottom = (int)extents.Y; + int left = (int)extents.Z; + int top = (int)extents.W; + + extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); + + int maxX = (int)extents.X; + int maxY = (int)extents.Y; + int minX = (int)extents.Z; + int minY = (int)extents.W; + + if (minX == maxX || minY == maxY) + { + continue; + } + + // It appears these have to be calculated on-the-fly. + // Precalulating transformed weights would require prior knowledge of every transformed pixel location + // since they can be at sub-pixel positions on both axis. + // I've optimized where I can but am always open to suggestions. + if (yScale > 1 && xScale > 1) + { + CalculateWeightsDown( + top, + bottom, + minY, + maxY, + point.Y, + sampler, + yScale, + ref ySpanRef, + yLength); + + CalculateWeightsDown( + left, + right, + minX, + maxX, + point.X, + sampler, + xScale, + ref xSpanRef, + xLength); + } + else + { + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); + CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); + } + + // Now multiply the results against the offsets + Vector4 sum = Vector4.Zero; + for (int yy = 0, j = minY; j <= maxY; j++, yy++) + { + float yWeight = Unsafe.Add(ref ySpanRef, yy); + + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float xWeight = Unsafe.Add(ref xSpanRef, xx); + var vector = source[i, j].ToVector4(); + + // Values are first premultiplied to prevent darkening of edge pixels + Vector4 multiplied = vector.Premultiply(); + sum += multiplied * xWeight * yWeight; + } + } + + ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); + + // Reverse the premultiplication + dest.PackFromVector4(sum.UnPremultiply()); } } - - ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); - - // Reverse the premultiplication - dest.PackFromVector4(sum.UnPremultiply()); - } - }); + }); } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index fd3c34d6c1..76abc64996 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -11,6 +11,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; using SixLabors.Primitives; @@ -215,10 +216,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms protected override Image CreateDestination(Image source, Rectangle sourceRectangle) { // We will always be creating the clone even for mutate because we may need to resize the canvas - IEnumerable> frames = source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.Width, this.Height, x.MetaData.Clone())); + IEnumerable> frames = source.Frames.Select(x => new ImageFrame(source.GetConfiguration(), this.Width, this.Height, x.MetaData.DeepClone())); // Use the overload to prevent an extra frame being added - return new Image(source.GetConfiguration(), source.MetaData.Clone(), frames); + return new Image(source.GetConfiguration(), source.MetaData.DeepClone(), frames); } /// @@ -267,26 +268,31 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms if (this.Sampler is NearestNeighborResampler) { + var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); + // Scaling factors float widthFactor = sourceRectangle.Width / (float)this.ResizeRectangle.Width; float heightFactor = sourceRectangle.Height / (float)this.ResizeRectangle.Height; - ParallelFor.WithConfiguration( - minY, - maxY, + ParallelHelper.IterateRows( + workingRect, configuration, - y => - { - // Y coordinates of source points - Span sourceRow = source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY)); - Span targetRow = destination.GetPixelRowSpan(y); - - for (int x = minX; x < maxX; x++) + rows => { - // X coordinates of source points - targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; - } - }); + for (int y = rows.Min; y < rows.Max; y++) + { + // Y coordinates of source points + Span sourceRow = + source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY)); + Span targetRow = destination.GetPixelRowSpan(y); + + for (int x = minX; x < maxX; x++) + { + // X coordinates of source points + targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; + } + } + }); return; } @@ -300,72 +306,88 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { firstPassPixels.MemorySource.Clear(); - ParallelFor.WithTemporaryBuffer( - 0, - sourceRectangle.Bottom, + var processColsRect = new Rectangle(0, 0, source.Width, sourceRectangle.Bottom); + + ParallelHelper.IterateRowsWithTempBuffer( + processColsRect, configuration, - source.Width, - (int y, IMemoryOwner tempRowBuffer) => + (rows, tempRowBuffer) => { - ref Vector4 firstPassRow = ref MemoryMarshal.GetReference(firstPassPixels.GetRowSpan(y)); - Span sourceRow = source.GetPixelRowSpan(y); - Span tempRowSpan = tempRowBuffer.GetSpan(); + for (int y = rows.Min; y < rows.Max; y++) + { + ref Vector4 firstPassRow = + ref MemoryMarshal.GetReference(firstPassPixels.GetRowSpan(y)); + Span sourceRow = source.GetPixelRowSpan(y); + Span tempRowSpan = tempRowBuffer.Span; - PixelOperations.Instance.ToVector4(sourceRow, tempRowSpan, sourceRow.Length); + PixelOperations.Instance.ToVector4(sourceRow, tempRowSpan, sourceRow.Length); - if (this.Compand) - { - for (int x = minX; x < maxX; x++) + if (this.Compand) { - WeightsWindow window = this.horizontalWeights.Weights[x - startX]; - Unsafe.Add(ref firstPassRow, x) = window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX); + for (int x = minX; x < maxX; x++) + { + WeightsWindow window = this.horizontalWeights.Weights[x - startX]; + Unsafe.Add(ref firstPassRow, x) = + window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX); + } } - } - else - { - for (int x = minX; x < maxX; x++) + else { - WeightsWindow window = this.horizontalWeights.Weights[x - startX]; - Unsafe.Add(ref firstPassRow, x) = window.ComputeWeightedRowSum(tempRowSpan, sourceX); + for (int x = minX; x < maxX; x++) + { + WeightsWindow window = this.horizontalWeights.Weights[x - startX]; + Unsafe.Add(ref firstPassRow, x) = + window.ComputeWeightedRowSum(tempRowSpan, sourceX); + } } } }); + var processRowsRect = Rectangle.FromLTRB(0, minY, width, maxY); + // Now process the rows. - ParallelFor.WithConfiguration( - minY, - maxY, + ParallelHelper.IterateRows( + processRowsRect, configuration, - y => - { - // Ensure offsets are normalized for cropping and padding. - WeightsWindow window = this.verticalWeights.Weights[y - startY]; - ref TPixel targetRow = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - - if (this.Compand) - { - for (int x = 0; x < width; x++) - { - // Destination color components - Vector4 destinationVector = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY); - destinationVector = destinationVector.Compress(); - - ref TPixel pixel = ref Unsafe.Add(ref targetRow, x); - pixel.PackFromVector4(destinationVector); - } - } - else + rows => { - for (int x = 0; x < width; x++) + for (int y = rows.Min; y < rows.Max; y++) { - // Destination color components - Vector4 destinationVector = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY); + // Ensure offsets are normalized for cropping and padding. + WeightsWindow window = this.verticalWeights.Weights[y - startY]; + ref TPixel targetRow = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); - ref TPixel pixel = ref Unsafe.Add(ref targetRow, x); - pixel.PackFromVector4(destinationVector); + if (this.Compand) + { + for (int x = 0; x < width; x++) + { + // Destination color components + Vector4 destinationVector = window.ComputeWeightedColumnSum( + firstPassPixels, + x, + sourceY); + destinationVector = destinationVector.Compress(); + + ref TPixel pixel = ref Unsafe.Add(ref targetRow, x); + pixel.PackFromVector4(destinationVector); + } + } + else + { + for (int x = 0; x < width; x++) + { + // Destination color components + Vector4 destinationVector = window.ComputeWeightedColumnSum( + firstPassPixels, + x, + sourceY); + + ref TPixel pixel = ref Unsafe.Add(ref targetRow, x); + pixel.PackFromVector4(destinationVector); + } + } } - } - }); + }); } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index 93c847d598..2ad626755c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.MetaData.Profiles.Exif; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; @@ -147,25 +148,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int height = source.Height; Rectangle destinationBounds = destination.Bounds(); - ParallelFor.WithConfiguration( - 0, - height, + ParallelHelper.IterateRows( + source.Bounds(), configuration, - y => - { - Span sourceRow = source.GetPixelRowSpan(y); - for (int x = 0; x < width; x++) + rows => { - int newX = height - y - 1; - newX = height - newX - 1; - int newY = width - x - 1; - - if (destinationBounds.Contains(newX, newY)) + for (int y = rows.Min; y < rows.Max; y++) { - destination[newX, newY] = sourceRow[x]; + Span sourceRow = source.GetPixelRowSpan(y); + for (int x = 0; x < width; x++) + { + int newX = height - y - 1; + newX = height - newX - 1; + int newY = width - x - 1; + + if (destinationBounds.Contains(newX, newY)) + { + destination[newX, newY] = sourceRow[x]; + } + } } - } - }); + }); } /// @@ -179,20 +182,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int width = source.Width; int height = source.Height; - ParallelFor.WithConfiguration( - 0, - height, + ParallelHelper.IterateRows( + source.Bounds(), configuration, - y => - { - Span sourceRow = source.GetPixelRowSpan(y); - Span targetRow = destination.GetPixelRowSpan(height - y - 1); - - for (int x = 0; x < width; x++) + rows => { - targetRow[width - x - 1] = sourceRow[x]; - } - }); + for (int y = rows.Min; y < rows.Max; y++) + { + Span sourceRow = source.GetPixelRowSpan(y); + Span targetRow = destination.GetPixelRowSpan(height - y - 1); + + for (int x = 0; x < width; x++) + { + targetRow[width - x - 1] = sourceRow[x]; + } + } + }); } /// @@ -207,22 +212,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int height = source.Height; Rectangle destinationBounds = destination.Bounds(); - ParallelFor.WithConfiguration( - 0, - height, + ParallelHelper.IterateRows( + source.Bounds(), configuration, - y => - { - Span sourceRow = source.GetPixelRowSpan(y); - int newX = height - y - 1; - for (int x = 0; x < width; x++) + rows => { - if (destinationBounds.Contains(newX, x)) + for (int y = rows.Min; y < rows.Max; y++) { - destination[newX, x] = sourceRow[x]; + Span sourceRow = source.GetPixelRowSpan(y); + int newX = height - y - 1; + for (int x = 0; x < width; x++) + { + // TODO: Optimize this: + if (destinationBounds.Contains(newX, x)) + { + destination[newX, x] = sourceRow[x]; + } + } } - } - }); + }); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/Codecs/CopyPixels.cs b/tests/ImageSharp.Benchmarks/Codecs/CopyPixels.cs deleted file mode 100644 index d55c231a73..0000000000 --- a/tests/ImageSharp.Benchmarks/Codecs/CopyPixels.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Threading.Tasks; - -using BenchmarkDotNet.Attributes; - -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Benchmarks.Codecs -{ - public class CopyPixels : BenchmarkBase - { - [Benchmark(Baseline = true, Description = "PixelAccessor Copy by indexer")] - public Rgba32 CopyByPixelAccesor() - { - using (var source = new Image(1024, 768)) - using (var target = new Image(1024, 768)) - { - Buffer2D sourcePixels = source.GetRootFramePixelBuffer(); - Buffer2D targetPixels = target.GetRootFramePixelBuffer(); - ParallelFor.WithConfiguration( - 0, - source.Height, - Configuration.Default, - y => - { - for (int x = 0; x < source.Width; x++) - { - targetPixels[x, y] = sourcePixels[x, y]; - } - }); - - return targetPixels[0, 0]; - } - } - - [Benchmark(Description = "PixelAccessor Copy by Span")] - public Rgba32 CopyByPixelAccesorSpan() - { - using (var source = new Image(1024, 768)) - using (var target = new Image(1024, 768)) - { - Buffer2D sourcePixels = source.GetRootFramePixelBuffer(); - Buffer2D targetPixels = target.GetRootFramePixelBuffer(); - ParallelFor.WithConfiguration( - 0, - source.Height, - Configuration.Default, - y => - { - Span sourceRow = sourcePixels.GetRowSpan(y); - Span targetRow = targetPixels.GetRowSpan(y); - - for (int x = 0; x < source.Width; x++) - { - targetRow[x] = sourceRow[x]; - } - }); - - return targetPixels[0, 0]; - } - } - - [Benchmark(Description = "Copy by indexer")] - public Rgba32 Copy() - { - using (var source = new Image(1024, 768)) - using (var target = new Image(1024, 768)) - { - ParallelFor.WithConfiguration( - 0, - source.Height, - Configuration.Default, - y => - { - for (int x = 0; x < source.Width; x++) - { - target[x, y] = source[x, y]; - } - }); - - return target[0, 0]; - } - } - - [Benchmark(Description = "Copy by Span")] - public Rgba32 CopySpan() - { - using (var source = new Image(1024, 768)) - using (var target = new Image(1024, 768)) - { - ParallelFor.WithConfiguration( - 0, - source.Height, - Configuration.Default, - y => - { - Span sourceRow = source.Frames.RootFrame.GetPixelRowSpan(y); - Span targetRow = target.Frames.RootFrame.GetPixelRowSpan(y); - - for (int x = 0; x < source.Width; x++) - { - targetRow[x] = sourceRow[x]; - } - }); - - return target[0, 0]; - } - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Benchmarks/Samplers/Glow.cs b/tests/ImageSharp.Benchmarks/Samplers/Glow.cs index fe1d4221d1..ff2e57b974 100644 --- a/tests/ImageSharp.Benchmarks/Samplers/Glow.cs +++ b/tests/ImageSharp.Benchmarks/Samplers/Glow.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors; using SixLabors.ImageSharp.Processing.Processors.Overlays; @@ -112,26 +113,29 @@ namespace SixLabors.ImageSharp.Benchmarks Buffer2D sourcePixels = source.PixelBuffer; rowColors.GetSpan().Fill(glowColor); - ParallelFor.WithConfiguration( - minY, - maxY, + var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); + ParallelHelper.IterateRows( + workingRect, configuration, - y => + rows => { - int offsetY = y - startY; - - for (int x = minX; x < maxX; x++) + for (int y = rows.Min; y < rows.Max; y++) { - int offsetX = x - startX; - float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY)); - Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4(); - TPixel packed = default(TPixel); - packed.PackFromVector4( - PremultipliedLerp( - sourceColor, - glowColor.ToVector4(), - 1 - (.95F * (distance / maxDistance)))); - sourcePixels[offsetX, offsetY] = packed; + int offsetY = y - startY; + + for (int x = minX; x < maxX; x++) + { + int offsetX = x - startX; + float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY)); + Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4(); + TPixel packed = default(TPixel); + packed.PackFromVector4( + PremultipliedLerp( + sourceColor, + glowColor.ToVector4(), + 1 - (.95F * (distance / maxDistance)))); + sourcePixels[offsetX, offsetY] = packed; + } } }); } diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 6a8479b2b2..963d674466 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Tests { // the shallow copy of configuration should behave exactly like the default configuration, // so by using the copy, we test both the default and the copy. - this.DefaultConfiguration = Configuration.CreateDefaultInstance().ShallowCopy(); + this.DefaultConfiguration = Configuration.CreateDefaultInstance().Clone(); this.ConfigurationEmpty = new Configuration(); } diff --git a/tests/ImageSharp.Tests/Drawing/FillPatternTests.cs b/tests/ImageSharp.Tests/Drawing/FillPatternTests.cs index 93715c586e..b310c7afc6 100644 --- a/tests/ImageSharp.Tests/Drawing/FillPatternTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillPatternTests.cs @@ -54,172 +54,225 @@ namespace SixLabors.ImageSharp.Tests.Drawing [Fact] public void ImageShouldBeFloodFilledWithPercent10() { - this.Test("Percent10", Rgba32.Blue, Brushes.Percent10(Rgba32.HotPink, Rgba32.LimeGreen), + this.Test( + "Percent10", + Rgba32.Blue, + Brushes.Percent10(Rgba32.HotPink, Rgba32.LimeGreen), new[,] - { - { Rgba32.HotPink , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink , Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen} - }); + { + { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen } + }); } [Fact] public void ImageShouldBeFloodFilledWithPercent10Transparent() { - Test("Percent10_Transparent", Rgba32.Blue, Brushes.Percent10(Rgba32.HotPink), - new Rgba32[,] { - { Rgba32.HotPink , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink , Rgba32.Blue}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue} - }); + this.Test( + "Percent10_Transparent", + Rgba32.Blue, + Brushes.Percent10(Rgba32.HotPink), + new Rgba32[,] + { + { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue } + }); } [Fact] public void ImageShouldBeFloodFilledWithPercent20() { - Test("Percent20", Rgba32.Blue, Brushes.Percent20(Rgba32.HotPink, Rgba32.LimeGreen), - new Rgba32[,] { - { Rgba32.HotPink , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink , Rgba32.LimeGreen}, - { Rgba32.HotPink , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink , Rgba32.LimeGreen} - }); + this.Test( + "Percent20", + Rgba32.Blue, + Brushes.Percent20(Rgba32.HotPink, Rgba32.LimeGreen), + new Rgba32[,] + { + { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen }, + { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen } + }); } [Fact] public void ImageShouldBeFloodFilledWithPercent20_transparent() { - Test("Percent20_Transparent", Rgba32.Blue, Brushes.Percent20(Rgba32.HotPink), - new Rgba32[,] { - { Rgba32.HotPink , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink , Rgba32.Blue}, - { Rgba32.HotPink , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink , Rgba32.Blue} - }); + this.Test( + "Percent20_Transparent", + Rgba32.Blue, + Brushes.Percent20(Rgba32.HotPink), + new Rgba32[,] + { + { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue }, + { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue } + }); } [Fact] public void ImageShouldBeFloodFilledWithHorizontal() { - Test("Horizontal", Rgba32.Blue, Brushes.Horizontal(Rgba32.HotPink, Rgba32.LimeGreen), - new Rgba32[,] { - { Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink}, - { Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen , Rgba32.LimeGreen} - }); + this.Test( + "Horizontal", + Rgba32.Blue, + Brushes.Horizontal(Rgba32.HotPink, Rgba32.LimeGreen), + new Rgba32[,] + { + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen } + }); } [Fact] public void ImageShouldBeFloodFilledWithHorizontal_transparent() { - Test("Horizontal_Transparent", Rgba32.Blue, Brushes.Horizontal(Rgba32.HotPink), - new Rgba32[,] { - { Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink}, - { Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue , Rgba32.Blue} - }); + this.Test( + "Horizontal_Transparent", + Rgba32.Blue, + Brushes.Horizontal(Rgba32.HotPink), + new Rgba32[,] + { + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue } + }); } - - [Fact] public void ImageShouldBeFloodFilledWithMin() { - Test("Min", Rgba32.Blue, Brushes.Min(Rgba32.HotPink, Rgba32.LimeGreen), - new Rgba32[,] { - { Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen , Rgba32.LimeGreen}, - { Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink} - }); + this.Test( + "Min", + Rgba32.Blue, + Brushes.Min(Rgba32.HotPink, Rgba32.LimeGreen), + new Rgba32[,] + { + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink } + }); } [Fact] public void ImageShouldBeFloodFilledWithMin_transparent() { - Test("Min_Transparent", Rgba32.Blue, Brushes.Min(Rgba32.HotPink), - new Rgba32[,] { - { Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue , Rgba32.Blue}, - { Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink}, - }); + this.Test( + "Min_Transparent", + Rgba32.Blue, + Brushes.Min(Rgba32.HotPink), + new Rgba32[,] + { + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink }, + }); } [Fact] public void ImageShouldBeFloodFilledWithVertical() { - Test("Vertical", Rgba32.Blue, Brushes.Vertical(Rgba32.HotPink, Rgba32.LimeGreen), - new Rgba32[,] { - { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen} - }); + this.Test( + "Vertical", + Rgba32.Blue, + Brushes.Vertical(Rgba32.HotPink, Rgba32.LimeGreen), + new Rgba32[,] + { + { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen } + }); } [Fact] public void ImageShouldBeFloodFilledWithVertical_transparent() { - Test("Vertical_Transparent", Rgba32.Blue, Brushes.Vertical(Rgba32.HotPink), - new Rgba32[,] { - { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue} - }); + this.Test( + "Vertical_Transparent", + Rgba32.Blue, + Brushes.Vertical(Rgba32.HotPink), + new Rgba32[,] + { + { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue } + }); } [Fact] public void ImageShouldBeFloodFilledWithForwardDiagonal() { - Test("ForwardDiagonal", Rgba32.Blue, Brushes.ForwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen), - new Rgba32[,] { - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen} - }); + this.Test( + "ForwardDiagonal", + Rgba32.Blue, + Brushes.ForwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen), + new Rgba32[,] + { + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen } + }); } [Fact] public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent() { - Test("ForwardDiagonal_Transparent", Rgba32.Blue, Brushes.ForwardDiagonal(Rgba32.HotPink), - new Rgba32[,] { - { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue} - }); + this.Test( + "ForwardDiagonal_Transparent", + Rgba32.Blue, + Brushes.ForwardDiagonal(Rgba32.HotPink), + new Rgba32[,] + { + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue } + }); } [Fact] public void ImageShouldBeFloodFilledWithBackwardDiagonal() { - Test("BackwardDiagonal", Rgba32.Blue, Brushes.BackwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen), - new Rgba32[,] { - { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen}, - { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink} - }); + this.Test( + "BackwardDiagonal", + Rgba32.Blue, + Brushes.BackwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen), + new Rgba32[,] + { + { Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen }, + { Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink } + }); } [Fact] public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent() { - Test("BackwardDiagonal_Transparent", Rgba32.Blue, Brushes.BackwardDiagonal(Rgba32.HotPink), - new Rgba32[,] { - { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue}, - { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink} - }); + this.Test( + "BackwardDiagonal_Transparent", + Rgba32.Blue, + Brushes.BackwardDiagonal(Rgba32.HotPink), + new Rgba32[,] + { + { Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue }, + { Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink } + }); } } -} +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs b/tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs index e86d41f574..32f723e72a 100644 --- a/tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs +++ b/tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs @@ -1,20 +1,21 @@ // 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.Primitives; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.Primitives; using SixLabors.Shapes; + using Xunit; + // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Drawing { - using System; - - using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; - using SixLabors.Primitives; - [GroupOutput("Drawing")] public class FillSolidBrushTests { @@ -55,7 +56,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing [Theory] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] - public void WhenColorIsOpaque_OverridePreviousColor(TestImageProvider provider, string newColorName) + public void WhenColorIsOpaque_OverridePreviousColor( + TestImageProvider provider, + string newColorName) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -63,7 +66,11 @@ namespace SixLabors.ImageSharp.Tests.Drawing TPixel color = TestUtils.GetPixelOfNamedColor(newColorName); image.Mutate(c => c.Fill(color)); - image.DebugSave(provider, newColorName, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + image.DebugSave( + provider, + newColorName, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); image.ComparePixelBufferTo(color); } } @@ -84,7 +91,12 @@ namespace SixLabors.ImageSharp.Tests.Drawing [Theory] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion_WorksOnWrappedMemoryImage(TestImageProvider provider, int x0, int y0, int w, int h) + public void FillRegion_WorksOnWrappedMemoryImage( + TestImageProvider provider, + int x0, + int y0, + int w, + int h) where TPixel : struct, IPixel { FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; @@ -105,27 +117,22 @@ namespace SixLabors.ImageSharp.Tests.Drawing { false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, { false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, { false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, { false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, { false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, { false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, { false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, { false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, { false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, { true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, { true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, { true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, { true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, { true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, { true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, { true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, { true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, @@ -155,8 +162,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing var options = new GraphicsOptions(false) { - ColorBlendingMode = blenderMode, - BlendPercentage = blendPercentage + ColorBlendingMode = blenderMode, BlendPercentage = blendPercentage }; if (triggerFillRegion) @@ -185,11 +191,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - PixelBlender blender = PixelOperations.Instance.GetPixelBlender(blenderMode, PixelAlphaCompositionMode.SrcOver); + PixelBlender blender = PixelOperations.Instance.GetPixelBlender( + blenderMode, + PixelAlphaCompositionMode.SrcOver); TPixel expectedPixel = blender.Blend(bgColor, fillColor, blendPercentage); image.ComparePixelBufferTo(expectedPixel); } } } -} +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetaDataTests.cs new file mode 100644 index 0000000000..991768a11a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetaDataTests.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Bmp; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Bmp +{ + public class BmpMetaDataTests + { + [Fact] + public void CloneIsDeep() + { + var meta = new BmpMetaData() { BitsPerPixel = BmpBitsPerPixel.Pixel24 }; + var clone = (BmpMetaData)meta.DeepClone(); + + clone.BitsPerPixel = BmpBitsPerPixel.Pixel32; + + Assert.False(meta.BitsPerPixel.Equals(clone.BitsPerPixel)); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetaDataTests.cs new file mode 100644 index 0000000000..a39fc47b40 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetaDataTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Gif; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Gif +{ + public class GifFrameMetaDataTests + { + [Fact] + public void CloneIsDeep() + { + var meta = new GifFrameMetaData() + { + FrameDelay = 1, + DisposalMethod = GifDisposalMethod.RestoreToBackground, + ColorTableLength = 2 + }; + + var clone = (GifFrameMetaData)meta.DeepClone(); + + clone.FrameDelay = 2; + clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious; + clone.ColorTableLength = 1; + + Assert.False(meta.FrameDelay.Equals(clone.FrameDelay)); + Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod)); + Assert.False(meta.ColorTableLength.Equals(clone.ColorTableLength)); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs new file mode 100644 index 0000000000..29db32b4ab --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Gif; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Gif +{ + public class GifMetaDataTests + { + [Fact] + public void CloneIsDeep() + { + var meta = new GifMetaData() + { + RepeatCount = 1, + ColorTableMode = GifColorTableMode.Global, + GlobalColorTableLength = 2 + }; + + var clone = (GifMetaData)meta.DeepClone(); + + clone.RepeatCount = 2; + clone.ColorTableMode = GifColorTableMode.Local; + clone.GlobalColorTableLength = 1; + + Assert.False(meta.RepeatCount.Equals(clone.RepeatCount)); + Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode)); + Assert.False(meta.GlobalColorTableLength.Equals(clone.GlobalColorTableLength)); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs index 464b1564b8..c2100c302f 100644 --- a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs +++ b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs @@ -2,17 +2,15 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Generic; using System.IO; using System.Linq; +using Moq; using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Gif; -using Moq; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -27,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests { this.DefaultFormatsManager = Configuration.CreateDefaultInstance().ImageFormatsManager; this.FormatsManagerEmpty = new ImageFormatManager(); - } + } [Fact] public void IfAutoloadWellKnownFormatsIsTrueAllFormatsAreLoaded() @@ -114,7 +112,7 @@ namespace SixLabors.ImageSharp.Tests IImageDecoder found2 = this.FormatsManagerEmpty.FindDecoder(TestFormat.GlobalTestFormat); Assert.Equal(decoder2, found2); Assert.NotEqual(found, found2); - } + } [Fact] public void AddFormatCallsConfig() @@ -125,5 +123,24 @@ namespace SixLabors.ImageSharp.Tests provider.Verify(x => x.Configure(config)); } + + [Fact] + public void DetectFormatAllocatesCleanBuffer() + { + byte[] jpegImage; + using (var buffer = new MemoryStream()) + { + using (var image = new Image(100, 100)) + { + image.SaveAsJpeg(buffer); + jpegImage = buffer.ToArray(); + } + } + + byte[] invalidImage = { 1, 2, 3 }; + + Assert.Equal(Image.DetectFormat(jpegImage), JpegFormat.Instance); + Assert.True(Image.DetectFormat(invalidImage) is null); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetaDataTests.cs new file mode 100644 index 0000000000..431de4be31 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetaDataTests.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Jpeg; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + public class JpegMetaDataTests + { + [Fact] + public void CloneIsDeep() + { + var meta = new JpegMetaData() { Quality = 50 }; + var clone = (JpegMetaData)meta.DeepClone(); + + clone.Quality = 99; + + Assert.False(meta.Quality.Equals(clone.Quality)); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 0508ac8c26..5d328db361 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -19,10 +18,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png { public class PngEncoderTests { - // This is bull. Failing online for no good reason. - // The images are an exact match. Maybe the submodule isn't updating? - private const float ToleranceThresholdForPaletteEncoder = 1.3F / 100; - public static readonly TheoryData PngBitDepthFiles = new TheoryData { @@ -137,19 +132,32 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } [Theory] - [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.Rgb)] - [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.RgbWithAlpha)] - [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.RgbWithAlpha)] - public void WorksWithBitDepth16(TestImageProvider provider, PngColorType pngColorType) + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Rgb, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.Rgb, PngBitDepth.Bit16)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit1)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit2)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit4)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit1)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit2)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit4)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgb48, PngColorType.Grayscale, PngBitDepth.Bit16)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)] + [WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit16)] + public void WorksWithAllBitDepths(TestImageProvider provider, PngColorType pngColorType, PngBitDepth pngBitDepth) where TPixel : struct, IPixel { TestPngEncoderCore( provider, pngColorType, PngFilterMethod.Adaptive, - PngBitDepth.Bit16, + pngBitDepth, appendPngColorType: true, - appendPixelType: true); + appendPixelType: true, + appendPngBitDepth: true); } [Theory] @@ -166,87 +174,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png appendPaletteSize: true); } - private static bool HasAlpha(PngColorType pngColorType) => - pngColorType == PngColorType.GrayscaleWithAlpha || pngColorType == PngColorType.RgbWithAlpha; - - private static void TestPngEncoderCore( - TestImageProvider provider, - PngColorType pngColorType, - PngFilterMethod pngFilterMethod, - PngBitDepth bitDepth, - int compressionLevel = 6, - int paletteSize = 255, - bool appendPngColorType = false, - bool appendPngFilterMethod = false, - bool appendPixelType = false, - bool appendCompressionLevel = false, - bool appendPaletteSize = false) - where TPixel : struct, IPixel - { - using (Image image = provider.GetImage()) - { - if (!HasAlpha(pngColorType)) - { - image.Mutate(c => c.MakeOpaque()); - } - - var encoder = new PngEncoder - { - ColorType = pngColorType, - FilterMethod = pngFilterMethod, - CompressionLevel = compressionLevel, - BitDepth = bitDepth, - Quantizer = new WuQuantizer(paletteSize) - }; - - string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty; - string pngFilterMethodInfo = appendPngFilterMethod ? pngFilterMethod.ToString() : string.Empty; - string compressionLevelInfo = appendCompressionLevel ? $"_C{compressionLevel}" : string.Empty; - string paletteSizeInfo = appendPaletteSize ? $"_PaletteSize-{paletteSize}" : string.Empty; - string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}"; - //string referenceInfo = $"{pngColorTypeInfo}"; - - // Does DebugSave & load reference CompareToReferenceInput(): - string actualOutputFile = ((ITestImageProvider)provider).Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType); - - if (TestEnvironment.IsMono) - { - // There are bugs in mono's System.Drawing implementation, reference decoders are not always reliable! - return; - } - - IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); - string referenceOutputFile = ((ITestImageProvider)provider).Utility.GetReferenceOutputFileName("png", debugInfo, appendPixelType, true); - - bool referenceOutputFileExists = File.Exists(referenceOutputFile); - - using (var actualImage = Image.Load(actualOutputFile, referenceDecoder)) - { - // TODO: Do we still need the reference output files? - Image referenceImage = referenceOutputFileExists - ? Image.Load(referenceOutputFile, referenceDecoder) - : image; - - float paletteToleranceHack = 80f / paletteSize; - paletteToleranceHack *= paletteToleranceHack; - ImageComparer comparer = pngColorType == PngColorType.Palette - ? ImageComparer.Tolerant(ToleranceThresholdForPaletteEncoder * paletteToleranceHack) - : ImageComparer.Exact; - try - { - comparer.VerifySimilarity(referenceImage, actualImage); - } - finally - { - if (referenceOutputFileExists) - { - referenceImage.Dispose(); - } - } - } - } - } - [Theory] [WithBlankImages(1, 1, PixelTypes.Rgba32)] public void WritesFileMarker(TestImageProvider provider) @@ -321,5 +248,54 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } } + + private static void TestPngEncoderCore( + TestImageProvider provider, + PngColorType pngColorType, + PngFilterMethod pngFilterMethod, + PngBitDepth bitDepth, + int compressionLevel = 6, + int paletteSize = 255, + bool appendPngColorType = false, + bool appendPngFilterMethod = false, + bool appendPixelType = false, + bool appendCompressionLevel = false, + bool appendPaletteSize = false, + bool appendPngBitDepth = false) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + var encoder = new PngEncoder + { + ColorType = pngColorType, + FilterMethod = pngFilterMethod, + CompressionLevel = compressionLevel, + BitDepth = bitDepth, + Quantizer = new WuQuantizer(paletteSize) + }; + + string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty; + string pngFilterMethodInfo = appendPngFilterMethod ? pngFilterMethod.ToString() : string.Empty; + string compressionLevelInfo = appendCompressionLevel ? $"_C{compressionLevel}" : string.Empty; + string paletteSizeInfo = appendPaletteSize ? $"_PaletteSize-{paletteSize}" : string.Empty; + string pngBitDepthInfo = appendPngBitDepth ? bitDepth.ToString() : string.Empty; + string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}{pngBitDepthInfo}"; + + string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType); + + // Compare to the Magick reference decoder. + IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); + + // We compare using both our decoder and the reference decoder as pixel transformation + // occurrs within the encoder itself leaving the input image unaffected. + // This means we are benefiting from testing our decoder also. + using (var imageSharpImage = Image.Load(actualOutputFile, new PngDecoder())) + using (var referenceImage = Image.Load(actualOutputFile, referenceDecoder)) + { + ImageComparer.Exact.VerifySimilarity(referenceImage, imageSharpImage); + } + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs new file mode 100644 index 0000000000..a21bb9acbe --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Png; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Png +{ + public class PngMetaDataTests + { + [Fact] + public void CloneIsDeep() + { + var meta = new PngMetaData() + { + BitDepth = PngBitDepth.Bit16, + ColorType = PngColorType.GrayscaleWithAlpha, + Gamma = 2 + }; + var clone = (PngMetaData)meta.DeepClone(); + + clone.BitDepth = PngBitDepth.Bit2; + clone.ColorType = PngColorType.Palette; + clone.Gamma = 1; + + Assert.False(meta.BitDepth.Equals(clone.BitDepth)); + Assert.False(meta.ColorType.Equals(clone.ColorType)); + Assert.False(meta.Gamma.Equals(clone.Gamma)); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Helpers/ParallelHelperTests.cs b/tests/ImageSharp.Tests/Helpers/ParallelHelperTests.cs new file mode 100644 index 0000000000..ef6b133f75 --- /dev/null +++ b/tests/ImageSharp.Tests/Helpers/ParallelHelperTests.cs @@ -0,0 +1,338 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Numerics; +using System.Threading; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.ParallelUtils; +using SixLabors.Memory; +using SixLabors.Primitives; + +using Xunit; +using Xunit.Abstractions; + +namespace SixLabors.ImageSharp.Tests.Helpers +{ + public class ParallelHelperTests + { + private readonly ITestOutputHelper Output; + + public ParallelHelperTests(ITestOutputHelper output) + { + this.Output = output; + } + + /// + /// maxDegreeOfParallelism, minY, maxY, expectedStepLength, expectedLastStepLength + /// + public static TheoryData IterateRows_OverMinimumPixelsLimit_Data = + new TheoryData() + { + { 1, 0, 100, -1, 100 }, + { 2, 0, 9, 5, 4 }, + { 4, 0, 19, 5, 4 }, + { 2, 10, 19, 5, 4 }, + { 4, 0, 200, 50, 50 }, + { 4, 123, 323, 50, 50 }, + { 4, 0, 1201, 301, 298 }, + { 8, 10, 236, 29, 23 } + }; + + [Theory] + [MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))] + public void IterateRows_OverMinimumPixelsLimit_IntervalsAreCorrect( + int maxDegreeOfParallelism, + int minY, + int maxY, + int expectedStepLength, + int expectedLastStepLength) + { + var parallelSettings = new ParallelExecutionSettings( + maxDegreeOfParallelism, + 1, + Configuration.Default.MemoryAllocator); + + var rectangle = new Rectangle(0, minY, 10, maxY - minY); + + int actualNumberOfSteps = 0; + + ParallelHelper.IterateRows( + rectangle, + parallelSettings, + rows => + { + Assert.True(rows.Min >= minY); + Assert.True(rows.Max <= maxY); + + int step = rows.Max - rows.Min; + int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength; + + Interlocked.Increment(ref actualNumberOfSteps); + Assert.Equal(expected, step); + }); + + Assert.Equal(maxDegreeOfParallelism, actualNumberOfSteps); + } + + [Theory] + [MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))] + public void IterateRows_OverMinimumPixelsLimit_ShouldVisitAllRows( + int maxDegreeOfParallelism, + int minY, + int maxY, + int expectedStepLength, + int expectedLastStepLength) + { + var parallelSettings = new ParallelExecutionSettings( + maxDegreeOfParallelism, + 1, + Configuration.Default.MemoryAllocator); + + var rectangle = new Rectangle(0, minY, 10, maxY - minY); + + + int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray(); + int[] actualData = new int[maxY]; + + ParallelHelper.IterateRows( + rectangle, + parallelSettings, + rows => + { + for (int y = rows.Min; y < rows.Max; y++) + { + actualData[y] = y; + } + }); + + Assert.Equal(expectedData, actualData); + } + + [Theory] + [MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))] + public void IterateRowsWithTempBuffer_OverMinimumPixelsLimit( + int maxDegreeOfParallelism, + int minY, + int maxY, + int expectedStepLength, + int expectedLastStepLength) + { + var parallelSettings = new ParallelExecutionSettings( + maxDegreeOfParallelism, + 1, + Configuration.Default.MemoryAllocator); + + var rectangle = new Rectangle(0, minY, 10, maxY - minY); + + var bufferHashes = new ConcurrentBag(); + + int actualNumberOfSteps = 0; + ParallelHelper.IterateRowsWithTempBuffer( + rectangle, + parallelSettings, + (RowInterval rows, Memory buffer) => + { + Assert.True(rows.Min >= minY); + Assert.True(rows.Max <= maxY); + + bufferHashes.Add(buffer.GetHashCode()); + + int step = rows.Max - rows.Min; + int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength; + + Interlocked.Increment(ref actualNumberOfSteps); + Assert.Equal(expected, step); + }); + + Assert.Equal(maxDegreeOfParallelism, actualNumberOfSteps); + + int numberOfDifferentBuffers = bufferHashes.Distinct().Count(); + Assert.Equal(actualNumberOfSteps, numberOfDifferentBuffers); + } + + [Theory] + [MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))] + public void IterateRowsWithTempBuffer_OverMinimumPixelsLimit_ShouldVisitAllRows( + int maxDegreeOfParallelism, + int minY, + int maxY, + int expectedStepLength, + int expectedLastStepLength) + { + var parallelSettings = new ParallelExecutionSettings( + maxDegreeOfParallelism, + 1, + Configuration.Default.MemoryAllocator); + + var rectangle = new Rectangle(0, minY, 10, maxY - minY); + + int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray(); + int[] actualData = new int[maxY]; + + ParallelHelper.IterateRowsWithTempBuffer( + rectangle, + parallelSettings, + (RowInterval rows, Memory buffer) => + { + for (int y = rows.Min; y < rows.Max; y++) + { + actualData[y] = y; + } + }); + + Assert.Equal(expectedData, actualData); + + } + + public static TheoryData IterateRows_WithEffectiveMinimumPixelsLimit_Data = + new TheoryData() + { + { 2, 200, 50, 2, 1, -1, 2 }, + { 2, 200, 200, 1, 1, -1, 1 }, + { 4, 200, 100, 4, 2, 2, 2 }, + { 4, 300, 100, 8, 3, 3, 2 }, + { 2, 5000, 1, 4500, 1, -1, 4500 }, + { 2, 5000, 1, 5000, 1, -1, 5000 }, + { 2, 5000, 1, 5001, 2, 2501, 2500 }, + }; + + [Theory] + [MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))] + public void IterateRows_WithEffectiveMinimumPixelsLimit( + int maxDegreeOfParallelism, + int minimumPixelsProcessedPerTask, + int width, + int height, + int expectedNumberOfSteps, + int expectedStepLength, + int expectedLastStepLength) + { + var parallelSettings = new ParallelExecutionSettings( + maxDegreeOfParallelism, + minimumPixelsProcessedPerTask, + Configuration.Default.MemoryAllocator); + + var rectangle = new Rectangle(0, 0, width, height); + + int actualNumberOfSteps = 0; + + ParallelHelper.IterateRows( + rectangle, + parallelSettings, + rows => + { + Assert.True(rows.Min >= 0); + Assert.True(rows.Max <= height); + + int step = rows.Max - rows.Min; + int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength; + + Interlocked.Increment(ref actualNumberOfSteps); + Assert.Equal(expected, step); + }); + + Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); + } + + [Theory] + [MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))] + public void IterateRowsWithTempBuffer_WithEffectiveMinimumPixelsLimit( + int maxDegreeOfParallelism, + int minimumPixelsProcessedPerTask, + int width, + int height, + int expectedNumberOfSteps, + int expectedStepLength, + int expectedLastStepLength) + { + var parallelSettings = new ParallelExecutionSettings( + maxDegreeOfParallelism, + minimumPixelsProcessedPerTask, + Configuration.Default.MemoryAllocator); + + var rectangle = new Rectangle(0, 0, width, height); + + int actualNumberOfSteps = 0; + ParallelHelper.IterateRowsWithTempBuffer( + rectangle, + parallelSettings, + (RowInterval rows, Memory buffer) => + { + Assert.True(rows.Min >= 0); + Assert.True(rows.Max <= height); + + int step = rows.Max - rows.Min; + int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength; + + Interlocked.Increment(ref actualNumberOfSteps); + Assert.Equal(expected, step); + }); + + Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps); + } + + public static readonly TheoryData IterateRectangularBuffer_Data = + new TheoryData() + { + { 8, 582, 453, 10, 10, 291, 226 }, // boundary data from DetectEdgesTest.DetectEdges_InBox + { 2, 582, 453, 10, 10, 291, 226 }, + { 16, 582, 453, 10, 10, 291, 226 }, + { 16, 582, 453, 10, 10, 1, 226 }, + { 16, 1, 453, 0, 10, 1, 226 }, + }; + + [Theory] + [MemberData(nameof(IterateRectangularBuffer_Data))] + public void IterateRectangularBuffer( + int maxDegreeOfParallelism, + int bufferWidth, + int bufferHeight, + int rectX, + int rectY, + int rectWidth, + int rectHeight) + { + MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; + + using (Buffer2D expected = memoryAllocator.Allocate2D(bufferWidth, bufferHeight, AllocationOptions.Clean)) + using (Buffer2D actual = memoryAllocator.Allocate2D(bufferWidth, bufferHeight, AllocationOptions.Clean)) + { + var rect = new Rectangle(rectX, rectY, rectWidth, rectHeight); + + void FillRow(int y, Buffer2D buffer) + { + for (int x = rect.Left; x < rect.Right; x++) + { + buffer[x, y] = new Point(x, y); + } + } + + // Fill Expected data: + for (int y = rectY; y < rect.Bottom; y++) + { + FillRow(y, expected); + } + + // Fill actual data using IterateRows: + var settings = new ParallelExecutionSettings(maxDegreeOfParallelism, memoryAllocator); + + ParallelHelper.IterateRows(rect, settings, + rows => + { + this.Output.WriteLine(rows.ToString()); + for (int y = rows.Min; y < rows.Max; y++) + { + FillRow(y, actual); + } + }); + + // Assert: + TestImageExtensions.CompareBuffers(expected.Span, actual.Span); + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs b/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs new file mode 100644 index 0000000000..f092da7082 --- /dev/null +++ b/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.CompilerServices; + +using SixLabors.ImageSharp.Memory; +using SixLabors.Memory; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Helpers +{ + public class RowIntervalTests + { + [Theory] + [InlineData(10, 20, 5, 10)] + [InlineData(1, 10, 0, 10)] + [InlineData(1, 10, 5, 8)] + [InlineData(1, 1, 0, 1)] + [InlineData(10, 20, 9, 10)] + [InlineData(10, 20, 0, 1)] + public void GetMultiRowSpan(int width, int height, int min, int max) + { + using (Buffer2D buffer = Configuration.Default.MemoryAllocator.Allocate2D(width, height)) + { + var rows = new RowInterval(min, max); + + Span span = buffer.GetMultiRowSpan(rows); + + ref int expected0 = ref buffer.Span[min * width]; + int expectedLength = (max - min) * width; + + ref int actual0 = ref span[0]; + + Assert.Equal(span.Length, expectedLength); + Assert.True(Unsafe.AreSame(ref expected0, ref actual0)); + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 69572425c9..16d999ad25 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void WrapMemory_CreatedImageIsCorrect() { - Configuration cfg = Configuration.Default.ShallowCopy(); + Configuration cfg = Configuration.Default.Clone(); var metaData = new ImageMetaData(); var array = new Rgba32[25]; diff --git a/tests/ImageSharp.Tests/Image/ImageTests.cs b/tests/ImageSharp.Tests/Image/ImageTests.cs index ed142ed974..f3c04d5e14 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void Configuration_Width_Height() { - Configuration configuration = Configuration.Default.ShallowCopy(); + Configuration configuration = Configuration.Default.Clone(); using (var image = new Image(configuration, 11, 23)) { @@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void Configuration_Width_Height_BackroundColor() { - Configuration configuration = Configuration.Default.ShallowCopy(); + Configuration configuration = Configuration.Default.Clone(); Rgba32 color = Rgba32.Aquamarine; using (var image = new Image(configuration, 11, 23, color)) diff --git a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs index 0a0ca1efa4..8c49039603 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs @@ -32,5 +32,13 @@ namespace SixLabors.ImageSharp.Tests Assert.Equal(colorTableLength, cloneGifFrameMetaData.ColorTableLength); Assert.Equal(disposalMethod, cloneGifFrameMetaData.DisposalMethod); } + + [Fact] + public void CloneIsDeep() + { + var metaData = new ImageFrameMetaData(); + ImageFrameMetaData clone = metaData.DeepClone(); + Assert.False(metaData.GetFormatMetaData(GifFormat.Instance).Equals(clone.GetFormatMetaData(GifFormat.Instance))); + } } } diff --git a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs index d681c90ba2..b9619cb3f8 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests metaData.VerticalResolution = 2; metaData.Properties.Add(imageProperty); - ImageMetaData clone = metaData.Clone(); + ImageMetaData clone = metaData.DeepClone(); Assert.Equal(exifProfile.ToByteArray(), clone.ExifProfile.ToByteArray()); Assert.Equal(4, clone.HorizontalResolution); @@ -37,19 +37,43 @@ namespace SixLabors.ImageSharp.Tests Assert.Equal(imageProperty, clone.Properties[0]); } + [Fact] + public void CloneIsDeep() + { + var metaData = new ImageMetaData(); + + var exifProfile = new ExifProfile(); + var imageProperty = new ImageProperty("name", "value"); + + metaData.ExifProfile = exifProfile; + metaData.HorizontalResolution = 4; + metaData.VerticalResolution = 2; + metaData.Properties.Add(imageProperty); + + ImageMetaData clone = metaData.DeepClone(); + clone.HorizontalResolution = 2; + clone.VerticalResolution = 4; + + Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile)); + Assert.False(metaData.HorizontalResolution.Equals(clone.HorizontalResolution)); + Assert.False(metaData.VerticalResolution.Equals(clone.VerticalResolution)); + Assert.False(metaData.Properties.Equals(clone.Properties)); + Assert.False(metaData.GetFormatMetaData(GifFormat.Instance).Equals(clone.GetFormatMetaData(GifFormat.Instance))); + } + [Fact] public void HorizontalResolution() { var metaData = new ImageMetaData(); Assert.Equal(96, metaData.HorizontalResolution); - metaData.HorizontalResolution=0; + metaData.HorizontalResolution = 0; Assert.Equal(96, metaData.HorizontalResolution); - metaData.HorizontalResolution=-1; + metaData.HorizontalResolution = -1; Assert.Equal(96, metaData.HorizontalResolution); - metaData.HorizontalResolution=1; + metaData.HorizontalResolution = 1; Assert.Equal(1, metaData.HorizontalResolution); } diff --git a/tests/ImageSharp.Tests/MetaData/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/MetaData/Profiles/Exif/ExifProfileTests.cs index 3deb382ea6..c10ffb6c8e 100644 --- a/tests/ImageSharp.Tests/MetaData/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/MetaData/Profiles/Exif/ExifProfileTests.cs @@ -67,16 +67,16 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void ConstructorCopy() { - Assert.Throws(() => { new ExifProfile((ExifProfile)null); }); + Assert.Throws(() => ((ExifProfile)null).DeepClone()); ExifProfile profile = GetExifProfile(); - var clone = new ExifProfile(profile); + ExifProfile clone = profile.DeepClone(); TestProfile(clone); profile.SetValue(ExifTag.ColorSpace, (ushort)2); - clone = new ExifProfile(profile); + clone = profile.DeepClone(); TestProfile(clone); } @@ -234,10 +234,12 @@ namespace SixLabors.ImageSharp.Tests exifProfile.SetValue(ExifTag.XResolution, new Rational(200)); exifProfile.SetValue(ExifTag.YResolution, new Rational(300)); - var metaData = new ImageMetaData(); - metaData.ExifProfile = exifProfile; - metaData.HorizontalResolution = 200; - metaData.VerticalResolution = 300; + var metaData = new ImageMetaData + { + ExifProfile = exifProfile, + HorizontalResolution = 200, + VerticalResolution = 300 + }; metaData.HorizontalResolution = 100; @@ -355,11 +357,11 @@ namespace SixLabors.ImageSharp.Tests // act Image reloadedImage = WriteAndRead(image, imageFormat); - + // assert ExifProfile actual = reloadedImage.MetaData.ExifProfile; Assert.NotNull(actual); - foreach(KeyValuePair expectedProfileValue in TestProfileValues) + foreach (KeyValuePair expectedProfileValue in TestProfileValues) { ExifValue actualProfileValue = actual.GetValue(expectedProfileValue.Key); Assert.NotNull(actualProfileValue); @@ -371,7 +373,7 @@ namespace SixLabors.ImageSharp.Tests public void ProfileToByteArray() { // arrange - byte[] exifBytesWithExifCode = ProfileResolver.ExifMarker.Concat(ExifConstants.LittleEndianByteOrderMarker).ToArray(); + byte[] exifBytesWithExifCode = ProfileResolver.ExifMarker.Concat(ExifConstants.LittleEndianByteOrderMarker).ToArray(); byte[] exifBytesWithoutExifCode = ExifConstants.LittleEndianByteOrderMarker; ExifProfile expectedProfile = CreateExifProfile(); var expectedProfileTags = expectedProfile.Values.Select(x => x.Tag).ToList(); @@ -384,7 +386,7 @@ namespace SixLabors.ImageSharp.Tests Assert.NotNull(actualBytes); Assert.NotEmpty(actualBytes); Assert.Equal(exifBytesWithoutExifCode, actualBytes.Take(exifBytesWithoutExifCode.Length).ToArray()); - foreach(ExifTag expectedProfileTag in expectedProfileTags) + foreach (ExifTag expectedProfileTag in expectedProfileTags) { ExifValue actualProfileValue = actualProfile.GetValue(expectedProfileTag); ExifValue expectedProfileValue = expectedProfile.GetValue(expectedProfileTag); @@ -396,7 +398,7 @@ namespace SixLabors.ImageSharp.Tests { var profile = new ExifProfile(); - foreach(KeyValuePair exifProfileValue in TestProfileValues) + foreach (KeyValuePair exifProfileValue in TestProfileValues) { profile.SetValue(exifProfileValue.Key, exifProfileValue.Value); } @@ -416,7 +418,7 @@ namespace SixLabors.ImageSharp.Tests private static Image WriteAndRead(Image image, TestImageWriteFormat imageFormat) { - switch(imageFormat) + switch (imageFormat) { case TestImageWriteFormat.Jpeg: return WriteAndReadJpeg(image); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs index a32239d96f..de72f6d09e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs @@ -10,8 +10,6 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution { - using SixLabors.ImageSharp.Advanced; - public class DetectEdgesTest : FileTestBase { private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs index c154c8ff3c..2f78915120 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs @@ -1,24 +1,34 @@ // 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.Tests.TestUtilities.ImageComparison; +using SixLabors.Primitives; + using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { + [GroupOutput("Transforms")] public class CropTest : FileTestBase { [Theory] - [WithFileCollection(nameof(DefaultFiles), DefaultPixelType)] - public void ImageShouldCrop(TestImageProvider provider) + [WithTestPatternImages(70, 30, PixelTypes.Rgba32, 0, 0, 70, 30)] + [WithTestPatternImages(50, 50, PixelTypes.Rgba32, -1, -1, 100, 200)] + [WithTestPatternImages(30, 70, PixelTypes.Rgba32, 7, 13, 20, 50)] + public void Crop(TestImageProvider provider, int x, int y, int w, int h) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Crop(image.Width / 2, image.Height / 2)); - image.DebugSave(provider); - } + var rect = new Rectangle(x, y, w, h); + FormattableString info = $"X{x}Y{y}.W{w}H{h}"; + provider.RunValidatingProcessorTest( + ctx => ctx.Crop(rect), + info, + appendPixelTypeToFileName: false, + comparer: ImageComparer.Exact); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs index d7e7a724c4..3c932bfaa6 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs @@ -5,23 +5,24 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Xunit; + // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { - using SixLabors.ImageSharp.Processing; - + [GroupOutput("Transforms")] public class FlipTests { - public static readonly TheoryData FlipValues - = new TheoryData - { - { FlipMode.None }, - { FlipMode.Vertical }, - { FlipMode.Horizontal }, - }; + public static readonly TheoryData FlipValues = + new TheoryData + { + FlipMode.None, + FlipMode.Vertical, + FlipMode.Horizontal, + }; [Theory] + [WithTestPatternImages(nameof(FlipValues), 20, 37, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(FlipValues), 53, 37, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(FlipValues), 17, 32, PixelTypes.Rgba32)] public void Flip(TestImageProvider provider, FlipMode flipMode) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs index c0db205f9e..7801c71432 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs @@ -7,7 +7,8 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { - public class RotateTests : FileTestBase + [GroupOutput("Transforms")] + public class RotateTests { public static readonly TheoryData RotateAngles = new TheoryData @@ -25,29 +26,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms }; [Theory] - [WithTestPatternImages(nameof(RotateAngles), 100, 50, DefaultPixelType)] - [WithTestPatternImages(nameof(RotateAngles), 50, 100, DefaultPixelType)] + [WithTestPatternImages(nameof(RotateAngles), 100, 50, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(RotateAngles), 50, 100, PixelTypes.Rgba32)] public void Rotate_WithAngle(TestImageProvider provider, float value) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Rotate(value)); - image.DebugSave(provider, value); - } + provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false); } [Theory] - [WithTestPatternImages(nameof(RotateEnumValues), 100, 50, DefaultPixelType)] - [WithTestPatternImages(nameof(RotateEnumValues), 50, 100, DefaultPixelType)] + [WithTestPatternImages(nameof(RotateEnumValues), 100, 50, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(RotateEnumValues), 50, 100, PixelTypes.Rgba32)] public void Rotate_WithRotateTypeEnum(TestImageProvider provider, RotateMode value) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Rotate(value)); - image.DebugSave(provider, value); - } + provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 8cf9dd62f5..5190a71e71 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3); private ITestOutputHelper Output { get; } - + public static readonly TheoryData ResamplerNames = new TheoryData { nameof(KnownResamplers.Bicubic), diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs index ab0cc42f93..30ac0856c7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Tests public virtual string SourceFileOrDescription => ""; - public Configuration Configuration { get; set; } = Configuration.Default.ShallowCopy(); + public Configuration Configuration { get; set; } = Configuration.Default.Clone(); /// /// Utility instance to provide informations about the test image & manage input/output diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 8cfc2472f5..7e942691e9 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -29,13 +29,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs { if (magickImage.Depth == 8) { - byte[] data = pixels.ToByteArray("RGBA"); - + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); + PixelOperations.Instance.PackFromRgba32Bytes(data, resultPixels, resultPixels.Length); } else if (magickImage.Depth == 16) { - ushort[] data = pixels.ToShortArray("RGBA"); + ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); Span bytes = MemoryMarshal.Cast(data.AsSpan()); PixelOperations.Instance.PackFromRgba64Bytes(bytes, resultPixels, resultPixels.Length); diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index a935873670..2384333bfb 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -441,14 +441,23 @@ namespace SixLabors.ImageSharp.Tests { Span actualPixels = image.GetPixelSpan(); - Assert.True(expectedPixels.Length == actualPixels.Length, "Buffer sizes are not equal!"); + CompareBuffers(expectedPixels, actualPixels); - for (int i = 0; i < expectedPixels.Length; i++) + return image; + } + + public static void CompareBuffers(Span expected, Span actual) + where T : struct, IEquatable + { + Assert.True(expected.Length == actual.Length, "Buffer sizes are not equal!"); + + for (int i = 0; i < expected.Length; i++) { - Assert.True(expectedPixels[i].Equals(actualPixels[i]), $"Pixels are different on position {i}!"); - } + T x = expected[i]; + T a = actual[i]; - return image; + Assert.True(x.Equals(a), $"Buffers differ at position {i}! Expected: {x} | Actual: {a}"); + } } /// diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs index db651886f2..b60439b488 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs @@ -14,10 +14,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests public class MagickReferenceCodecTests { - public MagickReferenceCodecTests(ITestOutputHelper output) - { - this.Output = output; - } + public MagickReferenceCodecTests(ITestOutputHelper output) => this.Output = output; private ITestOutputHelper Output { get; } @@ -61,6 +58,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests [WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Rgb48Bpp)] [WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Rgb48BppInterlaced)] [WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Rgb48BppTrans)] + [WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Gray16Bit)] public void MagickDecode_16BitDepthImage_IsApproximatelyEquivalentTo_SystemDrawingResult(TestImageProvider dummyProvider, string testImage) where TPixel : struct, IPixel { @@ -71,7 +69,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests // 1020 == 4 * 255 (Equivalent to manhattan distance of 1+1+1+1=4 in Rgba32 space) var comparer = ImageComparer.TolerantPercentage(1, 1020); - + using (var mImage = Image.Load(path, magickDecoder)) using (var sdImage = Image.Load(path, sdDecoder)) { diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs index 724c2e4144..033f0866a3 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests private ITestOutputHelper Output { get; } public const string SkipBenchmarks = -#if false +#if true "Benchmark, enable manually!"; #else null; diff --git a/tests/Images/External b/tests/Images/External index 6abc3bc0ac..7f4d2d64c6 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 6abc3bc0ac253a24c9e88e68d7b7d853350a85da +Subproject commit 7f4d2d64c6b820ca2b6827e6a8540a1013305ccf