diff --git a/.editorconfig b/.editorconfig index 06e698247..0e4883082 100644 --- a/.editorconfig +++ b/.editorconfig @@ -368,8 +368,6 @@ csharp_style_throw_expression = true:suggestion csharp_style_unused_value_expression_statement_preference = discard_variable:silent csharp_style_unused_value_assignment_preference = discard_variable:suggestion -csharp_style_var_for_built_in_types = false:silent +csharp_style_var_for_built_in_types = never csharp_style_var_when_type_is_apparent = true:warning csharp_style_var_elsewhere = false:warning - -csharp_prefer_simple_using_statement = false:silent diff --git a/ImageSharp.sln b/ImageSharp.sln index 40878c575..f1d4afef4 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -334,6 +334,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Tests.ProfilingS EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution + shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2aa31a1f-142c-43f4-8687-09abca4b3a26}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/shared-infrastructure b/shared-infrastructure index 36b2d55f5..ea561c249 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 36b2d55f5bb0d91024955bd26ba220ee41cc96e5 +Subproject commit ea561c249ba86352fe3b69e612b8072f3652eacb diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2c13e469f..a78a75d42 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -34,4 +34,6 @@ + + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 68d4f8949..d7171aa0f 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -52,4 +52,24 @@ + + + + + + + + + + + + + diff --git a/src/ImageSharp/Advanced/IRowIntervalOperation.cs b/src/ImageSharp/Advanced/IRowIntervalOperation.cs index 3e1b08621..980ed91a7 100644 --- a/src/ImageSharp/Advanced/IRowIntervalOperation.cs +++ b/src/ImageSharp/Advanced/IRowIntervalOperation.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Advanced diff --git a/src/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs b/src/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs index c18842a92..47fcf253e 100644 --- a/src/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs +++ b/src/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs @@ -2,8 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Advanced diff --git a/src/ImageSharp/Advanced/IRowOperation.cs b/src/ImageSharp/Advanced/IRowOperation.cs new file mode 100644 index 000000000..0a6065e4b --- /dev/null +++ b/src/ImageSharp/Advanced/IRowOperation.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Advanced +{ + /// + /// Defines the contract for an action that operates on a row. + /// + public interface IRowOperation + { + /// + /// Invokes the method passing the row y coordinate. + /// + /// The row y coordinate. + void Invoke(int y); + } +} diff --git a/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs b/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs new file mode 100644 index 000000000..7a13930fa --- /dev/null +++ b/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Advanced +{ + /// + /// Defines the contract for an action that operates on a row with a temporary buffer. + /// + /// The type of buffer elements. + public interface IRowOperation + where TBuffer : unmanaged + { + /// + /// Invokes the method passing the row and a buffer. + /// + /// The row y coordinate. + /// The contiguous region of memory. + void Invoke(int y, Span span); + } +} diff --git a/src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs b/src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs index adbad0d66..3f0f77ca3 100644 --- a/src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs +++ b/src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs @@ -17,51 +17,130 @@ namespace SixLabors.ImageSharp.Advanced /// public static partial class ParallelRowIterator { - private readonly struct IterationParameters + private readonly struct RowOperationWrapper + where T : struct, IRowOperation { - public readonly int MinY; - public readonly int MaxY; - public readonly int StepY; - public readonly int Width; + private readonly int minY; + private readonly int maxY; + private readonly int stepY; + private readonly T action; - public IterationParameters(int minY, int maxY, int stepY) - : this(minY, maxY, stepY, 0) + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperationWrapper( + int minY, + int maxY, + int stepY, + in T action) { + this.minY = minY; + this.maxY = maxY; + this.stepY = stepY; + this.action = action; } - public IterationParameters(int minY, int maxY, int stepY, int width) + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int i) { - this.MinY = minY; - this.MaxY = maxY; - this.StepY = stepY; - this.Width = width; + int yMin = this.minY + (i * this.stepY); + + if (yMin >= this.maxY) + { + return; + } + + int yMax = Math.Min(yMin + this.stepY, this.maxY); + + for (int y = yMin; y < yMax; y++) + { + // Skip the safety copy when invoking a potentially impure method on a readonly field + Unsafe.AsRef(this.action).Invoke(y); + } + } + } + + private readonly struct RowOperationWrapper + where T : struct, IRowOperation + where TBuffer : unmanaged + { + private readonly int minY; + private readonly int maxY; + private readonly int stepY; + private readonly int width; + private readonly MemoryAllocator allocator; + private readonly T action; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowOperationWrapper( + int minY, + int maxY, + int stepY, + int width, + MemoryAllocator allocator, + in T action) + { + this.minY = minY; + this.maxY = maxY; + this.stepY = stepY; + this.width = width; + this.allocator = allocator; + this.action = action; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int i) + { + int yMin = this.minY + (i * this.stepY); + + if (yMin >= this.maxY) + { + return; + } + + int yMax = Math.Min(yMin + this.stepY, this.maxY); + + using IMemoryOwner buffer = this.allocator.Allocate(this.width); + + Span span = buffer.Memory.Span; + + for (int y = yMin; y < yMax; y++) + { + Unsafe.AsRef(this.action).Invoke(y, span); + } } } private readonly struct RowIntervalOperationWrapper where T : struct, IRowIntervalOperation { - private readonly IterationParameters info; + private readonly int minY; + private readonly int maxY; + private readonly int stepY; private readonly T operation; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperationWrapper(in IterationParameters info, in T operation) + public RowIntervalOperationWrapper( + int minY, + int maxY, + int stepY, + in T operation) { - this.info = info; + this.minY = minY; + this.maxY = maxY; + this.stepY = stepY; this.operation = operation; } [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int i) { - int yMin = this.info.MinY + (i * this.info.StepY); + int yMin = this.minY + (i * this.stepY); - if (yMin >= this.info.MaxY) + if (yMin >= this.maxY) { return; } - int yMax = Math.Min(yMin + this.info.StepY, this.info.MaxY); + int yMax = Math.Min(yMin + this.stepY, this.maxY); var rows = new RowInterval(yMin, yMax); // Skip the safety copy when invoking a potentially impure method on a readonly field @@ -73,17 +152,26 @@ namespace SixLabors.ImageSharp.Advanced where T : struct, IRowIntervalOperation where TBuffer : unmanaged { - private readonly IterationParameters info; + private readonly int minY; + private readonly int maxY; + private readonly int stepY; + private readonly int width; private readonly MemoryAllocator allocator; private readonly T operation; [MethodImpl(InliningOptions.ShortMethod)] public RowIntervalOperationWrapper( - in IterationParameters info, + int minY, + int maxY, + int stepY, + int width, MemoryAllocator allocator, in T operation) { - this.info = info; + this.minY = minY; + this.maxY = maxY; + this.stepY = stepY; + this.width = width; this.allocator = allocator; this.operation = operation; } @@ -91,17 +179,17 @@ namespace SixLabors.ImageSharp.Advanced [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int i) { - int yMin = this.info.MinY + (i * this.info.StepY); + int yMin = this.minY + (i * this.stepY); - if (yMin >= this.info.MaxY) + if (yMin >= this.maxY) { return; } - int yMax = Math.Min(yMin + this.info.StepY, this.info.MaxY); + int yMax = Math.Min(yMin + this.stepY, this.maxY); var rows = new RowInterval(yMin, yMax); - using IMemoryOwner buffer = this.allocator.Allocate(this.info.Width); + using IMemoryOwner buffer = this.allocator.Allocate(this.width); Unsafe.AsRef(in this.operation).Invoke(in rows, buffer.Memory.Span); } diff --git a/src/ImageSharp/Advanced/ParallelRowIterator.cs b/src/ImageSharp/Advanced/ParallelRowIterator.cs index 123784c57..fb85de986 100644 --- a/src/ImageSharp/Advanced/ParallelRowIterator.cs +++ b/src/ImageSharp/Advanced/ParallelRowIterator.cs @@ -17,6 +17,135 @@ namespace SixLabors.ImageSharp.Advanced /// public static partial class ParallelRowIterator { + /// + /// Iterate through the rows of a rectangle in optimized batches. + /// + /// The type of row operation to perform. + /// The to get the parallel settings from. + /// The . + /// The operation defining the iteration logic on a single row. + [MethodImpl(InliningOptions.ShortMethod)] + public static void IterateRows(Configuration configuration, Rectangle rectangle, in T operation) + where T : struct, IRowOperation + { + var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); + IterateRows(rectangle, in parallelSettings, in operation); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches. + /// + /// The type of row operation to perform. + /// The . + /// The . + /// The operation defining the iteration logic on a single row. + public static void IterateRows( + Rectangle rectangle, + in ParallelExecutionSettings parallelSettings, + in T operation) + where T : struct, IRowOperation + { + ValidateRectangle(rectangle); + + int top = rectangle.Top; + int bottom = rectangle.Bottom; + int width = rectangle.Width; + int height = rectangle.Height; + + int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask); + int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps); + + // Avoid TPL overhead in this trivial case: + if (numOfSteps == 1) + { + for (int y = top; y < bottom; y++) + { + Unsafe.AsRef(operation).Invoke(y); + } + + return; + } + + int verticalStep = DivideCeil(rectangle.Height, numOfSteps); + var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; + var wrappingOperation = new RowOperationWrapper(top, bottom, verticalStep, in operation); + + Parallel.For( + 0, + numOfSteps, + parallelOptions, + wrappingOperation.Invoke); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches. + /// instantiating a temporary buffer for each invocation. + /// + /// The type of row operation to perform. + /// The type of buffer elements. + /// The to get the parallel settings from. + /// The . + /// The operation defining the iteration logic on a single row. + public static void IterateRows(Configuration configuration, Rectangle rectangle, in T operation) + where T : struct, IRowOperation + where TBuffer : unmanaged + { + var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); + IterateRows(rectangle, in parallelSettings, in operation); + } + + /// + /// Iterate through the rows of a rectangle in optimized batches. + /// instantiating a temporary buffer for each invocation. + /// + /// The type of row operation to perform. + /// The type of buffer elements. + /// The . + /// The . + /// The operation defining the iteration logic on a single row. + public static void IterateRows( + Rectangle rectangle, + in ParallelExecutionSettings parallelSettings, + in T operation) + where T : struct, IRowOperation + where TBuffer : unmanaged + { + ValidateRectangle(rectangle); + + int top = rectangle.Top; + int bottom = rectangle.Bottom; + int width = rectangle.Width; + int height = rectangle.Height; + + int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask); + int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps); + MemoryAllocator allocator = parallelSettings.MemoryAllocator; + + // Avoid TPL overhead in this trivial case: + if (numOfSteps == 1) + { + using IMemoryOwner buffer = allocator.Allocate(width); + Span span = buffer.Memory.Span; + + for (int y = top; y < bottom; y++) + { + Unsafe.AsRef(operation).Invoke(y, span); + } + + return; + } + + int verticalStep = DivideCeil(height, numOfSteps); + var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; + var wrappingOperation = new RowOperationWrapper(top, bottom, verticalStep, width, allocator, in operation); + + Parallel.For( + 0, + numOfSteps, + parallelOptions, + wrappingOperation.Invoke); + } + /// /// Iterate through the rows of a rectangle in optimized batches defined by -s. /// @@ -25,11 +154,11 @@ namespace SixLabors.ImageSharp.Advanced /// The . /// The operation defining the iteration logic on a single . [MethodImpl(InliningOptions.ShortMethod)] - public static void IterateRows(Configuration configuration, Rectangle rectangle, in T operation) + public static void IterateRowIntervals(Configuration configuration, Rectangle rectangle, in T operation) where T : struct, IRowIntervalOperation { var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); - IterateRows(rectangle, in parallelSettings, in operation); + IterateRowIntervals(rectangle, in parallelSettings, in operation); } /// @@ -39,7 +168,7 @@ namespace SixLabors.ImageSharp.Advanced /// The . /// The . /// The operation defining the iteration logic on a single . - public static void IterateRows( + public static void IterateRowIntervals( Rectangle rectangle, in ParallelExecutionSettings parallelSettings, in T operation) @@ -65,8 +194,7 @@ namespace SixLabors.ImageSharp.Advanced int verticalStep = DivideCeil(rectangle.Height, numOfSteps); var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; - var info = new IterationParameters(top, bottom, verticalStep); - var wrappingOperation = new RowIntervalOperationWrapper(in info, in operation); + var wrappingOperation = new RowIntervalOperationWrapper(top, bottom, verticalStep, in operation); Parallel.For( 0, @@ -84,12 +212,12 @@ namespace SixLabors.ImageSharp.Advanced /// The to get the parallel settings from. /// The . /// The operation defining the iteration logic on a single . - public static void IterateRows(Configuration configuration, Rectangle rectangle, in T operation) + public static void IterateRowIntervals(Configuration configuration, Rectangle rectangle, in T operation) where T : struct, IRowIntervalOperation where TBuffer : unmanaged { var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); - IterateRows(rectangle, in parallelSettings, in operation); + IterateRowIntervals(rectangle, in parallelSettings, in operation); } /// @@ -101,7 +229,7 @@ namespace SixLabors.ImageSharp.Advanced /// The . /// The . /// The operation defining the iteration logic on a single . - public static void IterateRows( + public static void IterateRowIntervals( Rectangle rectangle, in ParallelExecutionSettings parallelSettings, in T operation) @@ -123,18 +251,16 @@ namespace SixLabors.ImageSharp.Advanced if (numOfSteps == 1) { var rows = new RowInterval(top, bottom); - using (IMemoryOwner buffer = allocator.Allocate(width)) - { - Unsafe.AsRef(operation).Invoke(in rows, buffer.Memory.Span); - } + using IMemoryOwner buffer = allocator.Allocate(width); + + Unsafe.AsRef(operation).Invoke(in rows, buffer.Memory.Span); return; } int verticalStep = DivideCeil(height, numOfSteps); var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; - var info = new IterationParameters(top, bottom, verticalStep, width); - var wrappingOperation = new RowIntervalOperationWrapper(in info, allocator, in operation); + var wrappingOperation = new RowIntervalOperationWrapper(top, bottom, verticalStep, width, allocator, in operation); Parallel.For( 0, diff --git a/src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs b/src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs index 4be3f0079..5b312f4f8 100644 --- a/src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs +++ b/src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs @@ -248,7 +248,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion } /// - /// Performs the bulk conversion from into . + /// Performs the bulk conversion from into . /// /// The span to the source colors /// The span to the destination colors @@ -435,4 +435,4 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion return this.ToLinearRgb(rgb); } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs index 971bff322..5d8668257 100644 --- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs +++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs @@ -2,9 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.IO; using SixLabors.ImageSharp.Memory; +#if !SUPPORTS_SPAN_STREAM +using System.Buffers; +#endif namespace SixLabors.ImageSharp { diff --git a/src/ImageSharp/Common/Helpers/Guard.cs b/src/ImageSharp/Common/Helpers/Guard.cs index 1d215d286..3ab1b199a 100644 --- a/src/ImageSharp/Common/Helpers/Guard.cs +++ b/src/ImageSharp/Common/Helpers/Guard.cs @@ -22,7 +22,7 @@ namespace SixLabors { if (!value.GetType().GetTypeInfo().IsValueType) { - ThrowArgumentException("Type must be a struct.", parameterName); + ThrowHelper.ThrowArgumentException("Type must be a struct.", parameterName); } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 66a60d533..7d2799503 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -336,8 +336,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void Write8BitColor(Stream stream, ImageFrame image, Span colorPalette) where TPixel : unmanaged, IPixel { - using IFrameQuantizer quantizer = this.quantizer.CreateFrameQuantizer(this.configuration); - using QuantizedFrame quantized = quantizer.QuantizeFrame(image, image.Bounds()); + using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image, image.Bounds()); ReadOnlySpan quantizedColors = quantized.Palette.Span; var color = default(Rgba32); @@ -360,7 +360,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp for (int y = image.Height - 1; y >= 0; y--) { - ReadOnlySpan pixelSpan = quantized.GetRowSpan(y); + ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y); stream.Write(pixelSpan); for (int i = 0; i < this.padding; i++) diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index c9d631da0..06c4b3fc6 100644 --- a/src/ImageSharp/Formats/Gif/GifConstants.cs +++ b/src/ImageSharp/Formats/Gif/GifConstants.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.Text; @@ -21,11 +22,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// public const string FileVersion = "89a"; - /// - /// The ASCII encoded bytes used to identify the GIF file. - /// - internal static readonly byte[] MagicNumber = Encoding.ASCII.GetBytes(FileType + FileVersion); - /// /// The extension block introducer !. /// @@ -51,11 +47,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// public const string NetscapeApplicationIdentification = "NETSCAPE2.0"; - /// - /// The ASCII encoded application identification bytes. - /// - internal static readonly byte[] NetscapeApplicationIdentificationBytes = Encoding.ASCII.GetBytes(NetscapeApplicationIdentification); - /// /// The Netscape looping application sub block size. /// @@ -110,5 +101,25 @@ namespace SixLabors.ImageSharp.Formats.Gif /// The collection of file extensions that equate to a Gif. /// public static readonly IEnumerable FileExtensions = new[] { "gif" }; + + /// + /// Gets the ASCII encoded bytes used to identify the GIF file (combining and ). + /// + internal static ReadOnlySpan MagicNumber => new[] + { + (byte)'G', (byte)'I', (byte)'F', + (byte)'8', (byte)'9', (byte)'a' + }; + + /// + /// Gets the ASCII encoded application identification bytes (representing ). + /// + internal static ReadOnlySpan NetscapeApplicationIdentificationBytes => new[] + { + (byte)'N', (byte)'E', (byte)'T', + (byte)'S', (byte)'C', (byte)'A', + (byte)'P', (byte)'E', + (byte)'2', (byte)'.', (byte)'0' + }; } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 978609d7f..53c4c6f3f 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -4,6 +4,7 @@ using System.IO; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Gif @@ -17,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Gets or sets the quantizer for reducing the color count. /// Defaults to the /// - public IQuantizer Quantizer { get; set; } = new OctreeQuantizer(); + public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree; /// /// Gets or sets the color table mode: Global or local. diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index e32910d37..887540930 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -79,14 +79,14 @@ namespace SixLabors.ImageSharp.Formats.Gif bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; // Quantize the image returning a palette. - QuantizedFrame quantized; + IndexedImageFrame quantized; using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration)) { quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); } // Get the number of bits. - this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length); // Write the header. this.WriteHeader(stream); @@ -119,15 +119,20 @@ namespace SixLabors.ImageSharp.Formats.Gif } // Clean up. - quantized?.Dispose(); + quantized.Dispose(); // TODO: Write extension etc stream.WriteByte(GifConstants.EndIntroducer); } - private void EncodeGlobal(Image image, QuantizedFrame quantized, int transparencyIndex, Stream stream) + private void EncodeGlobal(Image image, IndexedImageFrame quantized, int transparencyIndex, Stream stream) where TPixel : unmanaged, IPixel { + // The palette quantizer can reuse the same pixel map across multiple frames + // since the palette is unchanging. This allows a reduction of memory usage across + // multi frame gifs using a global palette. + EuclideanPixelMap pixelMap = default; + bool pixelMapSet = false; for (int i = 0; i < image.Frames.Count; i++) { ImageFrame frame = image.Frames[i]; @@ -142,22 +147,27 @@ namespace SixLabors.ImageSharp.Formats.Gif } else { - using (var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, quantized.Palette)) - using (QuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds())) + if (!pixelMapSet) { - this.WriteImageData(paletteQuantized, stream); + pixelMapSet = true; + pixelMap = new EuclideanPixelMap(this.configuration, quantized.Palette); } + + using var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, pixelMap); + using IndexedImageFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()); + this.WriteImageData(paletteQuantized, stream); } } } - private void EncodeLocal(Image image, QuantizedFrame quantized, Stream stream) + private void EncodeLocal(Image image, IndexedImageFrame quantized, Stream stream) where TPixel : unmanaged, IPixel { ImageFrame previousFrame = null; GifFrameMetadata previousMeta = null; - foreach (ImageFrame frame in image.Frames) + for (int i = 0; i < image.Frames.Count; i++) { + ImageFrame frame = image.Frames[i]; ImageFrameMetadata metadata = frame.Metadata; GifFrameMetadata frameMetadata = metadata.GetGifMetadata(); if (quantized is null) @@ -173,27 +183,23 @@ namespace SixLabors.ImageSharp.Formats.Gif MaxColors = frameMetadata.ColorTableLength }; - using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, options)) - { - quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); - } + using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, options); + quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } else { - using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration)) - { - quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); - } + using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration); + quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } } - this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length); this.WriteGraphicalControlExtension(frameMetadata, this.GetTransparentIndex(quantized), stream); this.WriteImageDescriptor(frame, true, stream); this.WriteColorTable(quantized, stream); this.WriteImageData(quantized, stream); - quantized?.Dispose(); + quantized.Dispose(); quantized = null; // So next frame can regenerate it previousFrame = frame; previousMeta = frameMetadata; @@ -208,25 +214,23 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The . /// - private int GetTransparentIndex(QuantizedFrame quantized) + private int GetTransparentIndex(IndexedImageFrame quantized) where TPixel : unmanaged, IPixel { - // Transparent pixels are much more likely to be found at the end of a palette + // Transparent pixels are much more likely to be found at the end of a palette. int index = -1; - int length = quantized.Palette.Length; + ReadOnlySpan paletteSpan = quantized.Palette.Span; - using (IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(length)) - { - Span rgbaSpan = rgbaBuffer.GetSpan(); - ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan); - PixelOperations.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan); + using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteSpan.Length); + Span rgbaSpan = rgbaOwner.GetSpan(); + PixelOperations.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan); + ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan); - for (int i = quantized.Palette.Length - 1; i >= 0; i--) + for (int i = rgbaSpan.Length - 1; i >= 0; i--) + { + if (Unsafe.Add(ref rgbaSpanRef, i).Equals(default)) { - if (Unsafe.Add(ref paletteRef, i).Equals(default)) - { - index = i; - } + index = i; } } @@ -238,7 +242,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The stream to write to. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteHeader(Stream stream) => stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length); + private void WriteHeader(Stream stream) => stream.Write(GifConstants.MagicNumber); /// /// Writes the logical screen descriptor to the stream. @@ -326,8 +330,9 @@ namespace SixLabors.ImageSharp.Formats.Gif return; } - foreach (string comment in metadata.Comments) + for (var i = 0; i < metadata.Comments.Count; i++) { + string comment = metadata.Comments[i]; this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[1] = GifConstants.CommentLabel; stream.Write(this.buffer, 0, 2); @@ -335,7 +340,9 @@ namespace SixLabors.ImageSharp.Formats.Gif // Comment will be stored in chunks of 255 bytes, if it exceeds this size. ReadOnlySpan commentSpan = comment.AsSpan(); int idx = 0; - for (; idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; idx += GifConstants.MaxCommentSubBlockLength) + for (; + idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; + idx += GifConstants.MaxCommentSubBlockLength) { WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength); } @@ -391,7 +398,8 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The extension to write to the stream. /// The stream to write to. - public void WriteExtension(IGifExtension extension, Stream stream) + private void WriteExtension(TGifExtension extension, Stream stream) + where TGifExtension : struct, IGifExtension { this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[1] = extension.Label; @@ -437,37 +445,33 @@ namespace SixLabors.ImageSharp.Formats.Gif /// The pixel format. /// The to encode. /// The stream to write to. - private void WriteColorTable(QuantizedFrame image, Stream stream) + private void WriteColorTable(IndexedImageFrame image, Stream stream) where TPixel : unmanaged, IPixel { // The maximum number of colors for the bit depth - int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3; - int pixelCount = image.Palette.Length; + int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf(); - using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) - { - PixelOperations.Instance.ToRgb24Bytes( - this.configuration, - image.Palette.Span, - colorTable.GetSpan(), - pixelCount); - stream.Write(colorTable.Array, 0, colorTableLength); - } + using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength, AllocationOptions.Clean); + PixelOperations.Instance.ToRgb24Bytes( + this.configuration, + image.Palette.Span, + colorTable.GetSpan(), + image.Palette.Length); + + stream.Write(colorTable.Array, 0, colorTableLength); } /// /// Writes the image pixel data to the stream. /// /// The pixel format. - /// The containing indexed pixels. + /// The containing indexed pixels. /// The stream to write to. - private void WriteImageData(QuantizedFrame image, Stream stream) + private void WriteImageData(IndexedImageFrame image, Stream stream) where TPixel : unmanaged, IPixel { - using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth)) - { - encoder.Encode(image.GetPixelSpan(), stream); - } + using var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth); + encoder.Encode(image.GetPixelBufferSpan(), stream); } } } diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs index eda0c5fb8..056076bf0 100644 --- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs @@ -274,7 +274,7 @@ namespace SixLabors.ImageSharp.Formats.Gif ent = this.NextPixel(indexedPixels); - // TODO: PERF: It looks likt hshift could be calculated once statically. + // TODO: PERF: It looks like hshift could be calculated once statically. hshift = 0; for (fcode = this.hsize; fcode < 65536; fcode *= 2) { diff --git a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs index 8af5b81c2..5e26370ba 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Gif buffer[0] = GifConstants.ApplicationBlockSize; // Write NETSCAPE2.0 - GifConstants.NetscapeApplicationIdentificationBytes.AsSpan().CopyTo(buffer.Slice(1, 11)); + GifConstants.NetscapeApplicationIdentificationBytes.CopyTo(buffer.Slice(1, 11)); // Application Data ---- buffer[12] = 3; // Application block length (always 3) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs index 54633a5d7..87b486ea6 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs @@ -1,8 +1,7 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; -using System.Text; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { @@ -12,24 +11,38 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder internal static class ProfileResolver { /// - /// Describes the JFIF specific markers. + /// Gets the JFIF specific markers. /// - public static readonly byte[] JFifMarker = Encoding.ASCII.GetBytes("JFIF\0"); + public static ReadOnlySpan JFifMarker => new[] + { + (byte)'J', (byte)'F', (byte)'I', (byte)'F', (byte)'\0' + }; /// - /// Describes the ICC specific markers. + /// Gets the ICC specific markers. /// - public static readonly byte[] IccMarker = Encoding.ASCII.GetBytes("ICC_PROFILE\0"); + public static ReadOnlySpan IccMarker => new[] + { + (byte)'I', (byte)'C', (byte)'C', (byte)'_', + (byte)'P', (byte)'R', (byte)'O', (byte)'F', + (byte)'I', (byte)'L', (byte)'E', (byte)'\0' + }; /// - /// Describes the EXIF specific markers. + /// Gets the EXIF specific markers. /// - public static readonly byte[] ExifMarker = Encoding.ASCII.GetBytes("Exif\0\0"); + public static ReadOnlySpan ExifMarker => new[] + { + (byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0' + }; /// - /// Describes Adobe specific markers . + /// Gets the Adobe specific markers . /// - public static readonly byte[] AdobeMarker = Encoding.ASCII.GetBytes("Adobe"); + public static ReadOnlySpan AdobeMarker => new[] + { + (byte)'A', (byte)'d', (byte)'o', (byte)'b', (byte)'e' + }; /// /// Returns a value indicating whether the passed bytes are a match to the profile identifier. @@ -43,4 +56,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder && bytesToCheck.Slice(0, profileIdentifier.Length).SequenceEqual(profileIdentifier); } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/ZigZag.cs b/src/ImageSharp/Formats/Jpeg/Components/ZigZag.cs index 059e2052b..669abad28 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ZigZag.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ZigZag.cs @@ -34,12 +34,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components public fixed byte Data[Size]; /// - /// Unzig maps from the zigzag ordering to the natural ordering. For example, - /// unzig[3] is the column and row of the fourth element in zigzag order. The - /// value is 16, which means first column (16%8 == 0) and third row (16/8 == 2). + /// Gets the unzigs map, which maps from the zigzag ordering to the natural ordering. + /// For example, unzig[3] is the column and row of the fourth element in zigzag order. + /// The value is 16, which means first column (16%8 == 0) and third row (16/8 == 2). /// - private static readonly byte[] Unzig = - new byte[Size] + private static ReadOnlySpan Unzig => new byte[] { 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, @@ -75,8 +74,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components public static ZigZag CreateUnzigTable() { ZigZag result = default; - byte* unzigPtr = result.Data; - Marshal.Copy(Unzig, 0, (IntPtr)unzigPtr, Size); + ref byte sourceRef = ref MemoryMarshal.GetReference(Unzig); + ref byte destinationRef = ref Unsafe.AsRef(result.Data); + + Unzig.CopyTo(new Span(result.Data, Size)); + return result; } diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index 632460ec4..247bb3c75 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/src/ImageSharp/Formats/Png/PngConstants.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.Text; @@ -36,21 +37,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// public static readonly IEnumerable FileExtensions = new[] { "png" }; - /// - /// The header bytes identifying a Png. - /// - public static readonly byte[] HeaderBytes = - { - 0x89, // Set the high bit. - 0x50, // P - 0x4E, // N - 0x47, // G - 0x0D, // Line ending CRLF - 0x0A, // Line ending CRLF - 0x1A, // EOF - 0x0A // LF - }; - /// /// The header bytes as a big-endian coded ulong. /// @@ -77,5 +63,20 @@ namespace SixLabors.ImageSharp.Formats.Png /// The minimum length of a keyword in a text chunk is 1 byte. /// public const int MinTextKeywordLength = 1; + + /// + /// Gets the header bytes identifying a Png. + /// + public static ReadOnlySpan HeaderBytes => new byte[] + { + 0x89, // Set the high bit. + 0x50, // P + 0x4E, // N + 0x47, // G + 0x0D, // Line ending CRLF + 0x0A, // Line ending CRLF + 0x1A, // EOF + 0x0A // LF + }; } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index c62683e10..45e1ffd2d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -146,10 +146,10 @@ namespace SixLabors.ImageSharp.Formats.Png ImageMetadata metadata = image.Metadata; PngMetadata pngMetadata = metadata.GetPngMetadata(); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); - QuantizedFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); + IndexedImageFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized); - stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); + stream.Write(PngConstants.HeaderBytes); this.WriteHeaderChunk(stream); this.WritePaletteChunk(stream, quantized); @@ -371,7 +371,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The row span. /// The quantized pixels. Can be null. /// The row. - private void CollectPixelBytes(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row) + private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row) where TPixel : unmanaged, IPixel { switch (this.options.ColorType) @@ -380,12 +380,11 @@ namespace SixLabors.ImageSharp.Formats.Png if (this.bitDepth < 8) { - PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth); + PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetPixelRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth); } else { - int stride = this.currentScanline.Length(); - quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan()); + quantized.GetPixelRowSpan(row).CopyTo(this.currentScanline.GetSpan()); } break; @@ -440,7 +439,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The quantized pixels. Can be null. /// The row. /// The - private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row) + private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row) where TPixel : unmanaged, IPixel { this.CollectPixelBytes(rowSpan, quantized, row); @@ -546,59 +545,54 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixel format. /// The containing image data. /// The quantized frame. - private void WritePaletteChunk(Stream stream, QuantizedFrame quantized) + private void WritePaletteChunk(Stream stream, IndexedImageFrame quantized) where TPixel : unmanaged, IPixel { - if (quantized == null) + if (quantized is null) { return; } // Grab the palette and write it to the stream. ReadOnlySpan palette = quantized.Palette.Span; - int paletteLength = Math.Min(palette.Length, 256); - int colorTableLength = paletteLength * 3; - bool anyAlpha = false; + int paletteLength = palette.Length; + int colorTableLength = paletteLength * Unsafe.SizeOf(); + bool hasAlpha = false; - using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) - using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength)) - { - ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan()); - ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan()); - ReadOnlySpan quantizedSpan = quantized.GetPixelSpan(); - - Rgba32 rgba = default; - - for (int i = 0; i < paletteLength; i++) - { - if (quantizedSpan.IndexOf((byte)i) > -1) - { - int offset = i * 3; - palette[i].ToRgba32(ref rgba); + using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength); + using IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength); - byte alpha = rgba.A; + ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(colorTable.GetSpan())); + ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan()); - Unsafe.Add(ref colorTableRef, offset) = rgba.R; - Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G; - Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B; + // Bulk convert our palette to RGBA to allow assignment to tables. + using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteLength); + Span rgbaPaletteSpan = rgbaOwner.GetSpan(); + PixelOperations.Instance.ToRgba32(quantized.Configuration, quantized.Palette.Span, rgbaPaletteSpan); + ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan); - if (alpha > this.options.Threshold) - { - alpha = byte.MaxValue; - } + // Loop, assign, and extract alpha values from the palette. + for (int i = 0; i < paletteLength; i++) + { + Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, i); + byte alpha = rgba.A; - anyAlpha = anyAlpha || alpha < byte.MaxValue; - Unsafe.Add(ref alphaTableRef, i) = alpha; - } + Unsafe.Add(ref colorTableRef, i) = rgba.Rgb; + if (alpha > this.options.Threshold) + { + alpha = byte.MaxValue; } - this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength); + hasAlpha = hasAlpha || alpha < byte.MaxValue; + Unsafe.Add(ref alphaTableRef, i) = alpha; + } - // Write the transparency data - if (anyAlpha) - { - this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength); - } + this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength); + + // Write the transparency data + if (hasAlpha) + { + this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength); } } @@ -783,7 +777,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The image. /// The quantized pixel data. Can be null. /// The stream. - private void WriteDataChunks(ImageFrame pixels, QuantizedFrame quantized, Stream stream) + private void WriteDataChunks(ImageFrame pixels, IndexedImageFrame quantized, Stream stream) where TPixel : unmanaged, IPixel { byte[] buffer; @@ -881,7 +875,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixels. /// The quantized pixels span. /// The deflate stream. - private void EncodePixels(ImageFrame pixels, QuantizedFrame quantized, ZlibDeflateStream deflateStream) + private void EncodePixels(ImageFrame pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { int bytesPerScanline = this.CalculateScanlineLength(this.width); @@ -960,7 +954,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The type of the pixel. /// The quantized. /// The deflate stream. - private void EncodeAdam7IndexedPixels(QuantizedFrame quantized, ZlibDeflateStream deflateStream) + private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { int width = quantized.Width; @@ -987,7 +981,7 @@ namespace SixLabors.ImageSharp.Formats.Png row += Adam7.RowIncrement[pass]) { // collect data - ReadOnlySpan srcRow = quantized.GetRowSpan(row); + ReadOnlySpan srcRow = quantized.GetPixelRowSpan(row); for (int col = startCol, i = 0; col < width; col += Adam7.ColumnIncrement[pass]) diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs index 20b8c41c9..3f490ca6f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The type of the pixel. /// The options. /// The image. - public static QuantizedFrame CreateQuantizedFrame( + public static IndexedImageFrame CreateQuantizedFrame( PngEncoderOptions options, Image image) where TPixel : unmanaged, IPixel @@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Png public static byte CalculateBitDepth( PngEncoderOptions options, Image image, - QuantizedFrame quantizedFrame) + IndexedImageFrame quantizedFrame) where TPixel : unmanaged, IPixel { byte bitDepth; diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs index e8dd8a520..543a1fe30 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs @@ -36,12 +36,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib private const int EofSymbol = 256; - // The lengths of the bit length codes are sent in order of decreasing - // probability, to avoid transmitting the lengths for unused bit length codes. - private static readonly int[] BitLengthOrder = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; - - private static readonly byte[] Bit4Reverse = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 }; - private static readonly short[] StaticLCodes; private static readonly byte[] StaticLLength; private static readonly short[] StaticDCodes; @@ -128,6 +122,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; } + /// + /// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes. + /// + private static ReadOnlySpan BitLengthOrder => new byte[] { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + private static ReadOnlySpan Bit4Reverse => new byte[] { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 }; + /// /// Gets the pending buffer to use. /// @@ -158,6 +159,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5); this.Pending.WriteBits(this.distTree.NumCodes - 1, 5); this.Pending.WriteBits(blTreeCodes - 4, 4); + for (int rank = 0; rank < blTreeCodes; rank++) { this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3); @@ -250,6 +252,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib this.blTree.BuildTree(); int blTreeCodes = 4; + for (int i = 18; i > blTreeCodes; i--) { if (this.blTree.Length[BitLengthOrder[i]] > 0) @@ -363,10 +366,30 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib [MethodImpl(InliningOptions.ShortMethod)] public static short BitReverse(int toReverse) { - return (short)(Bit4Reverse[toReverse & 0xF] << 12 - | Bit4Reverse[(toReverse >> 4) & 0xF] << 8 - | Bit4Reverse[(toReverse >> 8) & 0xF] << 4 - | Bit4Reverse[toReverse >> 12]); + /* Use unsafe offsetting and manually validate the input index to reduce the + * total number of conditional branches. There are two main cases to test here: + * 1. In the first 3, the input value (or some combination of it) is combined + * with & 0xF, which results in a maximum value of 0xF no matter what the + * input value was. That is 15, which is always in range for the target span. + * As a result, no input validation is needed at all in this case. + * 2. There are two cases where the input value might cause an invalid access: + * when it is either negative, or greater than 15 << 12. We can test both + * conditions in a single pass by casting the input value to uint and right + * shifting it by 12, which also preserves the sign. If it is a negative + * value (2-complement), the test will fail as the uint cast will result + * in a much larger value. If the value was simply too high, the test will + * fail as expected. We can't simply check whether the value is lower than + * 15 << 12, because higher values are acceptable in the first 3 accesses. + * Doing this reduces the total number of index checks from 4 down to just 1. */ + int toReverseRightShiftBy12 = toReverse >> 12; + Guard.MustBeLessThanOrEqualTo((uint)toReverseRightShiftBy12, 15, nameof(toReverse)); + + ref byte bit4ReverseRef = ref MemoryMarshal.GetReference(Bit4Reverse); + + return (short)(Unsafe.Add(ref bit4ReverseRef, toReverse & 0xF) << 12 + | Unsafe.Add(ref bit4ReverseRef, (toReverse >> 4) & 0xF) << 8 + | Unsafe.Add(ref bit4ReverseRef, (toReverse >> 8) & 0xF) << 4 + | Unsafe.Add(ref bit4ReverseRef, toReverseRightShiftBy12)); } /// diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 321f2938c..a35443ec9 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -277,7 +277,7 @@ namespace SixLabors.ImageSharp var target = new ImageFrame(configuration, this.Width, this.Height, this.Metadata.DeepClone()); var operation = new RowIntervalOperation(this, target, configuration); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( configuration, this.Bounds(), in operation); diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index be0e9032b..baf4a2ce1 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -36,7 +36,7 @@ - + True True @@ -209,5 +209,4 @@ - diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs index 131abc5d2..20ee645fd 100644 --- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs +++ b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Memory /// public override Span GetSpan() { - if (this.Data == null) + if (this.Data is null) { ThrowObjectDisposedException(); } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs index 2649b7fb1..89aca914d 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs @@ -18,12 +18,12 @@ namespace SixLabors.ImageSharp.Memory /// Gets the number of elements per contiguous sub-buffer preceding the last buffer. /// The last buffer is allowed to be smaller. /// - public int BufferLength { get; } + int BufferLength { get; } /// /// Gets the aggregate number of elements in the group. /// - public long TotalLength { get; } + long TotalLength { get; } /// /// Gets a value indicating whether the group has been invalidated. diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs index c7112c47a..c58b224e4 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs @@ -1,11 +1,13 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; + namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { internal static class ExifConstants { - public static readonly byte[] LittleEndianByteOrderMarker = + public static ReadOnlySpan LittleEndianByteOrderMarker => new byte[] { (byte)'I', (byte)'I', @@ -13,7 +15,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 0x00, }; - public static readonly byte[] BigEndianByteOrderMarker = + public static ReadOnlySpan BigEndianByteOrderMarker => new byte[] { (byte)'M', (byte)'M', @@ -21,4 +23,4 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 0x2A }; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index c068461b2..b00813730 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -77,7 +77,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif int i = 0; // The byte order marker for little-endian, followed by the number 42 and a 0 - ExifConstants.LittleEndianByteOrderMarker.AsSpan().CopyTo(result.AsSpan(start: i)); + ExifConstants.LittleEndianByteOrderMarker.CopyTo(result.AsSpan(start: i)); i += ExifConstants.LittleEndianByteOrderMarker.Length; uint ifdOffset = ((uint)i - startIndex) + 4U; diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs b/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs index 94127432b..8184f1577 100644 --- a/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs +++ b/src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs @@ -13,7 +13,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders - /// /// Returns the result of the "NormalSrc" compositing equation. /// @@ -419,7 +418,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders return dest; } - /// /// Returns the result of the "MultiplySrc" compositing equation. /// @@ -825,7 +823,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders return dest; } - /// /// Returns the result of the "AddSrc" compositing equation. /// @@ -1231,7 +1228,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders return dest; } - /// /// Returns the result of the "SubtractSrc" compositing equation. /// @@ -1637,7 +1633,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders return dest; } - /// /// Returns the result of the "ScreenSrc" compositing equation. /// @@ -2043,7 +2038,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders return dest; } - /// /// Returns the result of the "DarkenSrc" compositing equation. /// @@ -2449,7 +2443,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders return dest; } - /// /// Returns the result of the "LightenSrc" compositing equation. /// @@ -2855,7 +2848,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders return dest; } - /// /// Returns the result of the "OverlaySrc" compositing equation. /// @@ -3261,7 +3253,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders return dest; } - /// /// Returns the result of the "HardLightSrc" compositing equation. /// diff --git a/src/ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs b/src/ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs index 088f61884..f89540e24 100644 --- a/src/ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing public static class FilterExtensions { /// - /// Filters an image but the given color matrix + /// Filters an image by the given color matrix /// /// The image this method extends. /// The filter color matrix @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Processing => source.ApplyProcessor(new FilterProcessor(matrix)); /// - /// Filters an image but the given color matrix + /// Filters an image by the given color matrix /// /// The image this method extends. /// The filter color matrix @@ -32,4 +32,4 @@ namespace SixLabors.ImageSharp.Processing public static IImageProcessingContext Filter(this IImageProcessingContext source, ColorMatrix matrix, Rectangle rectangle) => source.ApplyProcessor(new FilterProcessor(matrix), rectangle); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs index 18b347144..0d363689d 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Binarization @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); bool isAlphaOnly = typeof(TPixel) == typeof(A8); - var operation = new RowIntervalOperation(interest, source, upper, lower, threshold, isAlphaOnly); + var operation = new RowOperation(interest, source, upper, lower, threshold, isAlphaOnly); ParallelRowIterator.IterateRows( configuration, interest, @@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization /// /// A implementing the clone logic for . /// - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly ImageFrame source; private readonly TPixel upper; @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization private readonly bool isAlphaOnly; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( Rectangle bounds, ImageFrame source, TPixel upper, @@ -84,22 +84,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { Rgba32 rgba = default; - for (int y = rows.Min; y < rows.Max; y++) - { - Span row = this.source.GetPixelRowSpan(y); + Span row = this.source.GetPixelRowSpan(y); + ref TPixel rowRef = ref MemoryMarshal.GetReference(row); - for (int x = this.minX; x < this.maxX; x++) - { - ref TPixel color = ref row[x]; - color.ToRgba32(ref rgba); + for (int x = this.minX; x < this.maxX; x++) + { + ref TPixel color = ref Unsafe.Add(ref rowRef, x); + color.ToRgba32(ref rgba); - // Convert to grayscale using ITU-R Recommendation BT.709 if required - byte luminance = this.isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - color = luminance >= this.threshold ? this.upper : this.lower; - } + // Convert to grayscale using ITU-R Recommendation BT.709 if required + byte luminance = this.isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); + color = luminance >= this.threshold ? this.upper : this.lower; } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs index 17f60ebef..493218cde 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs @@ -72,8 +72,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution protected override void OnFrameApply(ImageFrame source) { // Preliminary gamma highlight pass - var gammaOperation = new ApplyGammaExposureRowIntervalOperation(this.SourceRectangle, source.PixelBuffer, this.Configuration, this.gamma); - ParallelRowIterator.IterateRows( + var gammaOperation = new ApplyGammaExposureRowOperation(this.SourceRectangle, source.PixelBuffer, this.Configuration, this.gamma); + ParallelRowIterator.IterateRows( this.Configuration, this.SourceRectangle, in gammaOperation); @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution float inverseGamma = 1 / this.gamma; // Apply the inverse gamma exposure pass, and write the final pixel data - var operation = new ApplyInverseGammaExposureRowIntervalOperation(this.SourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, inverseGamma); + var operation = new ApplyInverseGammaExposureRowOperation(this.SourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, inverseGamma); ParallelRowIterator.IterateRows( this.Configuration, this.SourceRectangle, @@ -120,14 +120,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution Vector4 parameters = Unsafe.Add(ref paramsRef, i); // Compute the vertical 1D convolution - var verticalOperation = new ApplyVerticalConvolutionRowIntervalOperation(sourceRectangle, firstPassBuffer, source.PixelBuffer, kernel); + var verticalOperation = new ApplyVerticalConvolutionRowOperation(sourceRectangle, firstPassBuffer, source.PixelBuffer, kernel); ParallelRowIterator.IterateRows( configuration, sourceRectangle, in verticalOperation); // Compute the horizontal 1D convolutions and accumulate the partial results on the target buffer - var horizontalOperation = new ApplyHorizontalConvolutionRowIntervalOperation(sourceRectangle, processingBuffer, firstPassBuffer, kernel, parameters.Z, parameters.W); + var horizontalOperation = new ApplyHorizontalConvolutionRowOperation(sourceRectangle, processingBuffer, firstPassBuffer, kernel, parameters.Z, parameters.W); ParallelRowIterator.IterateRows( configuration, sourceRectangle, @@ -138,7 +138,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// A implementing the vertical convolution logic for . /// - private readonly struct ApplyVerticalConvolutionRowIntervalOperation : IRowIntervalOperation + private readonly struct ApplyVerticalConvolutionRowOperation : IRowOperation { private readonly Rectangle bounds; private readonly Buffer2D targetValues; @@ -148,7 +148,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private readonly int maxX; [MethodImpl(InliningOptions.ShortMethod)] - public ApplyVerticalConvolutionRowIntervalOperation( + public ApplyVerticalConvolutionRowOperation( Rectangle bounds, Buffer2D targetValues, Buffer2D sourcePixels, @@ -164,16 +164,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X); + Span targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X); - for (int x = 0; x < this.bounds.Width; x++) - { - Buffer2DUtils.Convolve4(this.kernel, this.sourcePixels, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX); - } + for (int x = 0; x < this.bounds.Width; x++) + { + Buffer2DUtils.Convolve4(this.kernel, this.sourcePixels, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX); } } } @@ -181,7 +178,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// A implementing the horizontal convolution logic for . /// - private readonly struct ApplyHorizontalConvolutionRowIntervalOperation : IRowIntervalOperation + private readonly struct ApplyHorizontalConvolutionRowOperation : IRowOperation { private readonly Rectangle bounds; private readonly Buffer2D targetValues; @@ -193,7 +190,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private readonly int maxX; [MethodImpl(InliningOptions.ShortMethod)] - public ApplyHorizontalConvolutionRowIntervalOperation( + public ApplyHorizontalConvolutionRowOperation( Rectangle bounds, Buffer2D targetValues, Buffer2D sourceValues, @@ -213,16 +210,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X); + Span targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X); - for (int x = 0; x < this.bounds.Width; x++) - { - Buffer2DUtils.Convolve4AndAccumulatePartials(this.kernel, this.sourceValues, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX, this.z, this.w); - } + for (int x = 0; x < this.bounds.Width; x++) + { + Buffer2DUtils.Convolve4AndAccumulatePartials(this.kernel, this.sourceValues, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX, this.z, this.w); } } } @@ -230,7 +224,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// A implementing the gamma exposure logic for . /// - private readonly struct ApplyGammaExposureRowIntervalOperation : IRowIntervalOperation + private readonly struct ApplyGammaExposureRowOperation : IRowOperation { private readonly Rectangle bounds; private readonly Buffer2D targetPixels; @@ -238,7 +232,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private readonly float gamma; [MethodImpl(InliningOptions.ShortMethod)] - public ApplyGammaExposureRowIntervalOperation( + public ApplyGammaExposureRowOperation( Rectangle bounds, Buffer2D targetPixels, Configuration configuration, @@ -252,31 +246,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { - for (int y = rows.Min; y < rows.Max; y++) + Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); + PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span, PixelConversionModifiers.Premultiply); + ref Vector4 baseRef = ref MemoryMarshal.GetReference(span); + + for (int x = 0; x < this.bounds.Width; x++) { - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span, PixelConversionModifiers.Premultiply); - ref Vector4 baseRef = ref MemoryMarshal.GetReference(span); - - for (int x = 0; x < this.bounds.Width; x++) - { - ref Vector4 v = ref Unsafe.Add(ref baseRef, x); - v.X = MathF.Pow(v.X, this.gamma); - v.Y = MathF.Pow(v.Y, this.gamma); - v.Z = MathF.Pow(v.Z, this.gamma); - } - - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); + ref Vector4 v = ref Unsafe.Add(ref baseRef, x); + v.X = MathF.Pow(v.X, this.gamma); + v.Y = MathF.Pow(v.Y, this.gamma); + v.Z = MathF.Pow(v.Z, this.gamma); } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); } } /// /// A implementing the inverse gamma exposure logic for . /// - private readonly struct ApplyInverseGammaExposureRowIntervalOperation : IRowIntervalOperation + private readonly struct ApplyInverseGammaExposureRowOperation : IRowOperation { private readonly Rectangle bounds; private readonly Buffer2D targetPixels; @@ -285,7 +276,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private readonly float inverseGamma; [MethodImpl(InliningOptions.ShortMethod)] - public ApplyInverseGammaExposureRowIntervalOperation( + public ApplyInverseGammaExposureRowOperation( Rectangle bounds, Buffer2D targetPixels, Buffer2D sourceValues, @@ -301,28 +292,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { Vector4 low = Vector4.Zero; var high = new Vector4(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); - for (int y = rows.Min; y < rows.Max; y++) + Span targetPixelSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); + Span sourceRowSpan = this.sourceValues.GetRowSpan(y).Slice(this.bounds.X); + ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); + + for (int x = 0; x < this.bounds.Width; x++) { - Span targetPixelSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); - Span sourceRowSpan = this.sourceValues.GetRowSpan(y).Slice(this.bounds.X); - ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); - - for (int x = 0; x < this.bounds.Width; x++) - { - ref Vector4 v = ref Unsafe.Add(ref sourceRef, x); - var clamp = Vector4.Clamp(v, low, high); - v.X = MathF.Pow(clamp.X, this.inverseGamma); - v.Y = MathF.Pow(clamp.Y, this.inverseGamma); - v.Z = MathF.Pow(clamp.Z, this.inverseGamma); - } - - PixelOperations.Instance.FromVector4Destructive(this.configuration, sourceRowSpan.Slice(0, this.bounds.Width), targetPixelSpan, PixelConversionModifiers.Premultiply); + ref Vector4 v = ref Unsafe.Add(ref sourceRef, x); + var clamp = Vector4.Clamp(v, low, high); + v.X = MathF.Pow(clamp.X, this.inverseGamma); + v.Y = MathF.Pow(clamp.Y, this.inverseGamma); + v.Z = MathF.Pow(clamp.Z, this.inverseGamma); } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, sourceRowSpan.Slice(0, this.bounds.Width), targetPixelSpan, PixelConversionModifiers.Premultiply); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs index b4902fbd7..f7439879e 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs @@ -65,9 +65,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution source.CopyTo(targetPixels); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowIntervalOperation(interest, targetPixels, source.PixelBuffer, this.KernelY, this.KernelX, this.Configuration, this.PreserveAlpha); + var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, this.KernelY, this.KernelX, this.Configuration, this.PreserveAlpha); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRows( this.Configuration, interest, in operation); @@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// A implementing the convolution logic for . /// - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly Rectangle bounds; private readonly int maxY; @@ -91,7 +91,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private readonly bool preserveAlpha; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( Rectangle bounds, Buffer2D targetPixels, Buffer2D sourcePixels, @@ -113,52 +113,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { ref Vector4 spanRef = ref MemoryMarshal.GetReference(span); + Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); + PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span); - for (int y = rows.Min; y < rows.Max; y++) + if (this.preserveAlpha) { - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span); - - if (this.preserveAlpha) + for (int x = 0; x < this.bounds.Width; x++) { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve2D3( - in this.kernelY, - in this.kernelX, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - this.maxY, - this.bounds.X, - this.maxX); - } + DenseMatrixUtils.Convolve2D3( + in this.kernelY, + in this.kernelX, + this.sourcePixels, + ref spanRef, + y, + x, + this.bounds.Y, + this.maxY, + this.bounds.X, + this.maxX); } - else + } + else + { + for (int x = 0; x < this.bounds.Width; x++) { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve2D4( - in this.kernelY, - in this.kernelX, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - this.maxY, - this.bounds.X, - this.maxX); - } + DenseMatrixUtils.Convolve2D4( + in this.kernelY, + in this.kernelX, + this.sourcePixels, + ref spanRef, + y, + x, + this.bounds.Y, + this.maxY, + this.bounds.X, + this.maxX); } - - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index 30f2c6b4e..4bbb15cba 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -64,15 +64,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); // Horizontal convolution - var horizontalOperation = new RowIntervalOperation(interest, firstPassPixels, source.PixelBuffer, this.KernelX, this.Configuration, this.PreserveAlpha); - ParallelRowIterator.IterateRows( + var horizontalOperation = new RowOperation(interest, firstPassPixels, source.PixelBuffer, this.KernelX, this.Configuration, this.PreserveAlpha); + ParallelRowIterator.IterateRows( this.Configuration, interest, in horizontalOperation); // Vertical convolution - var verticalOperation = new RowIntervalOperation(interest, source.PixelBuffer, firstPassPixels, this.KernelY, this.Configuration, this.PreserveAlpha); - ParallelRowIterator.IterateRows( + var verticalOperation = new RowOperation(interest, source.PixelBuffer, firstPassPixels, this.KernelY, this.Configuration, this.PreserveAlpha); + ParallelRowIterator.IterateRows( this.Configuration, interest, in verticalOperation); @@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// A implementing the convolution logic for . /// - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly Rectangle bounds; private readonly Buffer2D targetPixels; @@ -91,7 +91,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private readonly bool preserveAlpha; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( Rectangle bounds, Buffer2D targetPixels, Buffer2D sourcePixels, @@ -109,53 +109,50 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { ref Vector4 spanRef = ref MemoryMarshal.GetReference(span); int maxY = this.bounds.Bottom - 1; int maxX = this.bounds.Right - 1; - for (int y = rows.Min; y < rows.Max; y++) - { - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span); + Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); + PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span); - if (this.preserveAlpha) + if (this.preserveAlpha) + { + for (int x = 0; x < this.bounds.Width; x++) { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve3( - in this.kernel, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - maxY, - this.bounds.X, - maxX); - } + DenseMatrixUtils.Convolve3( + in this.kernel, + this.sourcePixels, + ref spanRef, + y, + x, + this.bounds.Y, + maxY, + this.bounds.X, + maxX); } - else + } + else + { + for (int x = 0; x < this.bounds.Width; x++) { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve4( - in this.kernel, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - maxY, - this.bounds.X, - maxX); - } + DenseMatrixUtils.Convolve4( + in this.kernel, + this.sourcePixels, + ref spanRef, + y, + x, + this.bounds.Y, + maxY, + this.bounds.X, + maxX); } - - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs index b6f9bb319..a2c8fc1fb 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -56,8 +56,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution source.CopyTo(targetPixels); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowIntervalOperation(interest, targetPixels, source.PixelBuffer, this.KernelXY, this.Configuration, this.PreserveAlpha); - ParallelRowIterator.IterateRows( + var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, this.KernelXY, this.Configuration, this.PreserveAlpha); + ParallelRowIterator.IterateRows( this.Configuration, interest, in operation); @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// A implementing the convolution logic for . /// - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly Rectangle bounds; private readonly int maxY; @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private readonly bool preserveAlpha; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( Rectangle bounds, Buffer2D targetPixels, Buffer2D sourcePixels, @@ -100,50 +100,47 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { ref Vector4 spanRef = ref MemoryMarshal.GetReference(span); - for (int y = rows.Min; y < rows.Max; y++) - { - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); - PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span); + Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); + PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span); - if (this.preserveAlpha) + if (this.preserveAlpha) + { + for (int x = 0; x < this.bounds.Width; x++) { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve3( - in this.kernel, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - this.maxY, - this.bounds.X, - this.maxX); - } + DenseMatrixUtils.Convolve3( + in this.kernel, + this.sourcePixels, + ref spanRef, + y, + x, + this.bounds.Y, + this.maxY, + this.bounds.X, + this.maxX); } - else + } + else + { + for (int x = 0; x < this.bounds.Width; x++) { - for (int x = 0; x < this.bounds.Width; x++) - { - DenseMatrixUtils.Convolve4( - in this.kernel, - this.sourcePixels, - ref spanRef, - y, - x, - this.bounds.Y, - this.maxY, - this.bounds.X, - this.maxX); - } + DenseMatrixUtils.Convolve4( + in this.kernel, + this.sourcePixels, + ref spanRef, + y, + x, + this.bounds.Y, + this.maxY, + this.bounds.X, + this.maxX); } - - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs index f93fe20b1..159c67b5c 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs @@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution processor.Apply(pass); } - var operation = new RowIntervalOperation(source.PixelBuffer, pass.PixelBuffer, interest); + var operation = new RowOperation(source.PixelBuffer, pass.PixelBuffer, interest); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -89,7 +89,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// A implementing the convolution logic for . /// - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly Buffer2D targetPixels; private readonly Buffer2D passPixels; @@ -97,7 +97,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private readonly int maxX; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( Buffer2D targetPixels, Buffer2D passPixels, Rectangle bounds) @@ -110,23 +110,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) - { - ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(this.passPixels.GetRowSpan(y)); - ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(this.targetPixels.GetRowSpan(y)); + ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(this.passPixels.GetRowSpan(y)); + ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(this.targetPixels.GetRowSpan(y)); - for (int x = this.minX; x < this.maxX; x++) - { - // Grab the max components of the two pixels - ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, x); - ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, x); + for (int x = this.minX; x < this.maxX; x++) + { + // Grab the max components of the two pixels + ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, x); + ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, x); - var pixelValue = Vector4.Max(currentPassPixel.ToVector4(), currentTargetPixel.ToVector4()); + var pixelValue = Vector4.Max(currentPassPixel.ToVector4(), currentTargetPixel.ToVector4()); - currentTargetPixel.FromVector4(pixelValue); - } + currentTargetPixel.FromVector4(pixelValue); } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 7b8e83585..7d30bada6 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -4,6 +4,7 @@ using System; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -89,29 +90,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public void ApplyQuantizationDither( ref TFrameQuantizer quantizer, - ReadOnlyMemory palette, ImageFrame source, - Memory output, + IndexedImageFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel { - Span outputSpan = output.Span; - ReadOnlySpan paletteSpan = palette.Span; - int width = bounds.Width; int offsetY = bounds.Top; int offsetX = bounds.Left; float scale = quantizer.Options.DitherScale; for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetPixelRowSpan(y); - int rowStart = (y - offsetY) * width; + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); + ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY)); for (int x = bounds.Left; x < bounds.Right; x++) { - TPixel sourcePixel = row[x]; - outputSpan[rowStart + x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); + TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, x); + Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); } } @@ -119,25 +116,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// [MethodImpl(InliningOptions.ShortMethod)] - public void ApplyPaletteDither( - Configuration configuration, - ReadOnlyMemory palette, + public void ApplyPaletteDither( + in TPaletteDitherImageProcessor processor, ImageFrame source, - Rectangle bounds, - float scale) + Rectangle bounds) + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { - var pixelMap = new EuclideanPixelMap(palette); - + float scale = processor.DitherScale; for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetPixelRowSpan(y); + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); for (int x = bounds.Left; x < bounds.Right; x++) { - TPixel sourcePixel = row[x]; - pixelMap.GetClosestColor(sourcePixel, out TPixel transformed); + ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x); + TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); - row[x] = transformed; + sourcePixel = transformed; } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs index 3d6edc9fa..8f9d82537 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -19,15 +18,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// The type of frame quantizer. /// The pixel format. /// The frame quantizer. - /// The quantized palette. /// The source image. - /// The output target + /// The destination quantized frame. /// The region of interest bounds. void ApplyQuantizationDither( ref TFrameQuantizer quantizer, - ReadOnlyMemory palette, ImageFrame source, - Memory output, + IndexedImageFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel; @@ -36,18 +33,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Transforms the image frame applying a dither matrix. /// This method should be treated as destructive, altering the input pixels. /// + /// The type of palette dithering processor. /// The pixel format. - /// The configuration. - /// The quantized palette. + /// The palette dithering processor. /// The source image. /// The region of interest bounds. - /// The dithering scale used to adjust the amount of dither. Range 0..1. - void ApplyPaletteDither( - Configuration configuration, - ReadOnlyMemory palette, + void ApplyPaletteDither( + in TPaletteDitherImageProcessor processor, ImageFrame source, - Rectangle bounds, - float scale) + Rectangle bounds) + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel; } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs new file mode 100644 index 000000000..a8e08fa3f --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering +{ + /// + /// Implements an algorithm to alter the pixels of an image via palette dithering. + /// + /// The pixel format. + public interface IPaletteDitherImageProcessor + where TPixel : unmanaged, IPixel + { + /// + /// Gets the configuration instance to use when performing operations. + /// + Configuration Configuration { get; } + + /// + /// Gets the dithering palette. + /// + ReadOnlyMemory Palette { get; } + + /// + /// Gets the dithering scale used to adjust the amount of dither. Range 0..1. + /// + float DitherScale { get; } + + /// + /// Returns the color from the dithering palette corresponding to the given color. + /// + /// The color to match. + /// The match. + TPixel GetPaletteColor(TPixel color); + } +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs index d3e710782..f6026a64f 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// /// An ordered dithering matrix with equal sides of arbitrary length /// - public readonly partial struct OrderedDither : IDither + public readonly partial struct OrderedDither { /// /// Applies order dithering using the 2x2 Bayer dithering matrix. diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 7803f92b4..6862cff00 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -105,21 +105,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public void ApplyQuantizationDither( ref TFrameQuantizer quantizer, - ReadOnlyMemory palette, ImageFrame source, - Memory output, + IndexedImageFrame destination, Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel { - var ditherOperation = new QuantizeDitherRowIntervalOperation( + var ditherOperation = new QuantizeDitherRowOperation( ref quantizer, in Unsafe.AsRef(this), source, - output, - bounds, - palette, - ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length)); + destination, + bounds); ParallelRowIterator.IterateRows( quantizer.Configuration, @@ -129,24 +126,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// [MethodImpl(InliningOptions.ShortMethod)] - public void ApplyPaletteDither( - Configuration configuration, - ReadOnlyMemory palette, + public void ApplyPaletteDither( + in TPaletteDitherImageProcessor processor, ImageFrame source, - Rectangle bounds, - float scale) + Rectangle bounds) + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { - var ditherOperation = new PaletteDitherRowIntervalOperation( + var ditherOperation = new PaletteDitherRowOperation( + in processor, in Unsafe.AsRef(this), source, - bounds, - palette, - scale, - ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length)); + bounds); ParallelRowIterator.IterateRows( - configuration, + processor.Configuration, bounds, in ditherOperation); } @@ -200,102 +194,87 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering public override int GetHashCode() => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); - private readonly struct QuantizeDitherRowIntervalOperation : IRowIntervalOperation + private readonly struct QuantizeDitherRowOperation : IRowOperation where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel { private readonly TFrameQuantizer quantizer; private readonly OrderedDither dither; private readonly ImageFrame source; - private readonly Memory output; + private readonly IndexedImageFrame destination; private readonly Rectangle bounds; - private readonly ReadOnlyMemory palette; private readonly int bitDepth; [MethodImpl(InliningOptions.ShortMethod)] - public QuantizeDitherRowIntervalOperation( + public QuantizeDitherRowOperation( ref TFrameQuantizer quantizer, in OrderedDither dither, ImageFrame source, - Memory output, - Rectangle bounds, - ReadOnlyMemory palette, - int bitDepth) + IndexedImageFrame destination, + Rectangle bounds) { this.quantizer = quantizer; this.dither = dither; this.source = source; - this.output = output; + this.destination = destination; this.bounds = bounds; - this.palette = palette; - this.bitDepth = bitDepth; + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Length); } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - ReadOnlySpan paletteSpan = this.palette.Span; - Span outputSpan = this.output.Span; - int width = this.bounds.Width; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; float scale = this.quantizer.Options.DitherScale; - for (int y = rows.Min; y < rows.Max; y++) + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY)); + + for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - Span row = this.source.GetPixelRowSpan(y); - int rowStart = (y - offsetY) * width; - - // TODO: This can be a bulk operation. - for (int x = this.bounds.Left; x < this.bounds.Right; x++) - { - TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, scale); - outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _); - } + TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale); + Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _); } } } - private readonly struct PaletteDitherRowIntervalOperation : IRowIntervalOperation + private readonly struct PaletteDitherRowOperation : IRowOperation + where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { + private readonly TPaletteDitherImageProcessor processor; private readonly OrderedDither dither; private readonly ImageFrame source; private readonly Rectangle bounds; - private readonly EuclideanPixelMap pixelMap; private readonly float scale; private readonly int bitDepth; [MethodImpl(InliningOptions.ShortMethod)] - public PaletteDitherRowIntervalOperation( + public PaletteDitherRowOperation( + in TPaletteDitherImageProcessor processor, in OrderedDither dither, ImageFrame source, - Rectangle bounds, - ReadOnlyMemory palette, - float scale, - int bitDepth) + Rectangle bounds) { + this.processor = processor; this.dither = dither; this.source = source; this.bounds = bounds; - this.pixelMap = new EuclideanPixelMap(palette); - this.scale = scale; - this.bitDepth = bitDepth; + this.scale = processor.DitherScale; + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length); } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + + for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - Span row = this.source.GetPixelRowSpan(y); - - for (int x = this.bounds.Left; x < this.bounds.Right; x++) - { - TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, this.scale); - this.pixelMap.GetClosestColor(dithered, out TPixel transformed); - row[x] = transformed; - } + ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x); + TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale); + sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered); } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index 254847f45..e0dd4eae1 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -3,7 +3,9 @@ using System; using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Processing.Processors.Dithering { @@ -14,11 +16,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering internal sealed class PaletteDitherProcessor : ImageProcessor where TPixel : unmanaged, IPixel { - private readonly int paletteLength; + private readonly DitherProcessor ditherProcessor; private readonly IDither dither; - private readonly float ditherScale; - private readonly ReadOnlyMemory sourcePalette; - private IMemoryOwner palette; + private IMemoryOwner paletteOwner; private bool isDisposed; /// @@ -31,37 +31,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) { - this.paletteLength = definition.Palette.Span.Length; this.dither = definition.Dither; - this.ditherScale = definition.DitherScale; - this.sourcePalette = definition.Palette; - } - /// - protected override void OnFrameApply(ImageFrame source) - { - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + ReadOnlySpan sourcePalette = definition.Palette.Span; + this.paletteOwner = this.Configuration.MemoryAllocator.Allocate(sourcePalette.Length); + Color.ToPixel(this.Configuration, sourcePalette, this.paletteOwner.Memory.Span); - this.dither.ApplyPaletteDither( + this.ditherProcessor = new DitherProcessor( this.Configuration, - this.palette.Memory, - source, - interest, - this.ditherScale); + this.paletteOwner.Memory, + definition.DitherScale); } /// - protected override void BeforeFrameApply(ImageFrame source) + protected override void OnFrameApply(ImageFrame source) { - // Lazy init palettes: - if (this.palette is null) - { - this.palette = this.Configuration.MemoryAllocator.Allocate(this.paletteLength); - ReadOnlySpan sourcePalette = this.sourcePalette.Span; - Color.ToPixel(this.Configuration, sourcePalette, this.palette.Memory.Span); - } - - base.BeforeFrameApply(source); + var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest); } /// @@ -72,15 +58,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering return; } + this.isDisposed = true; if (disposing) { - this.palette?.Dispose(); + this.paletteOwner.Dispose(); } - this.palette = null; - - this.isDisposed = true; + this.paletteOwner = null; base.Dispose(disposing); } + + /// + /// Used to allow inlining of calls to + /// . + /// + private readonly struct DitherProcessor : IPaletteDitherImageProcessor + { + private readonly EuclideanPixelMap pixelMap; + + [MethodImpl(InliningOptions.ShortMethod)] + public DitherProcessor( + Configuration configuration, + ReadOnlyMemory palette, + float ditherScale) + { + this.Configuration = configuration; + this.pixelMap = new EuclideanPixelMap(configuration, palette); + this.Palette = palette; + this.DitherScale = ditherScale; + } + + public Configuration Configuration { get; } + + public ReadOnlyMemory Palette { get; } + + public float DitherScale { get; } + + [MethodImpl(InliningOptions.ShortMethod)] + public TPixel GetPaletteColor(TPixel color) + { + this.pixelMap.GetClosestColor(color, out TPixel match); + return match; + } + } } } diff --git a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs index ae938aec1..fca896929 100644 --- a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs +++ b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs @@ -99,7 +99,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing "Cannot draw image because the source image does not overlap the target image."); } - var operation = new RowIntervalOperation(source, targetImage, blender, configuration, minX, width, locationY, targetX, this.Opacity); + var operation = new RowOperation(source, targetImage, blender, configuration, minX, width, locationY, targetX, this.Opacity); ParallelRowIterator.IterateRows( configuration, workingRect, @@ -109,7 +109,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing /// /// A implementing the draw logic for . /// - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly ImageFrame sourceFrame; private readonly Image targetImage; @@ -122,7 +122,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing private readonly float opacity; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( ImageFrame sourceFrame, Image targetImage, PixelBlender blender, @@ -146,14 +146,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span background = this.sourceFrame.GetPixelRowSpan(y).Slice(this.minX, this.width); - Span foreground = this.targetImage.GetPixelRowSpan(y - this.locationY).Slice(this.targetX, this.width); - this.blender.Blend(this.configuration, background, background, foreground, this.opacity); - } + Span background = this.sourceFrame.GetPixelRowSpan(y).Slice(this.minX, this.width); + Span foreground = this.targetImage.GetPixelRowSpan(y - this.locationY).Slice(this.targetX, this.width); + this.blender.Blend(this.configuration, background, background, foreground, this.opacity); } } } diff --git a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs index 270eb1343..5ee1e40de 100644 --- a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs @@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects source.CopyTo(targetPixels); var operation = new RowIntervalOperation(this.SourceRectangle, targetPixels, source, this.Configuration, brushSize >> 1, this.definition.Levels); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( this.Configuration, this.SourceRectangle, in operation); diff --git a/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs b/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs index 1907228c9..71259a618 100644 --- a/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs @@ -50,9 +50,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects protected override void OnFrameApply(ImageFrame source) { var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowIntervalOperation(interest.X, source, this.Configuration, this.modifiers, this.rowDelegate); + var operation = new RowOperation(interest.X, source, this.Configuration, this.modifiers, this.rowDelegate); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRows( this.Configuration, interest, in operation); @@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects /// /// A implementing the convolution logic for . /// - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly int startX; private readonly ImageFrame source; @@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects private readonly TDelegate rowProcessor; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( int startX, ImageFrame source, Configuration configuration, @@ -86,18 +86,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length); - PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, this.modifiers); + Span rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length); + PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, this.modifiers); - // Run the user defined pixel shader to the current row of pixels - Unsafe.AsRef(this.rowProcessor).Invoke(span, new Point(this.startX, y)); + // Run the user defined pixel shader to the current row of pixels + Unsafe.AsRef(this.rowProcessor).Invoke(span, new Point(this.startX, y)); - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan, this.modifiers); - } + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan, this.modifiers); } } } diff --git a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs index 545c53915..7da4eb1b1 100644 --- a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs @@ -36,9 +36,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters protected override void OnFrameApply(ImageFrame source) { var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowIntervalOperation(interest.X, source, this.definition.Matrix, this.Configuration); + var operation = new RowOperation(interest.X, source, this.definition.Matrix, this.Configuration); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRows( this.Configuration, interest, in operation); @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters /// /// A implementing the convolution logic for . /// - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly int startX; private readonly ImageFrame source; @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters private readonly Configuration configuration; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( int startX, ImageFrame source, ColorMatrix matrix, @@ -69,17 +69,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length); - PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span); + Span rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length); + PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span); - Vector4Utils.Transform(span, ref Unsafe.AsRef(this.matrix)); + Vector4Utils.Transform(span, ref Unsafe.AsRef(this.matrix)); - PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan); - } + PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan); } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs index 5fdedad6e..1547de8ac 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs @@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization } var operation = new RowIntervalOperation(cdfData, tileYStartPositions, tileWidth, tileHeight, tileCount, halfTileWidth, luminanceLevels, source); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( this.Configuration, new Rectangle(0, 0, sourceWidth, tileYStartPositions.Count), in operation); @@ -522,7 +522,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization this.luminanceLevels, source); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( this.configuration, new Rectangle(0, 0, this.sourceWidth, this.tileYStartPositions.Count), in operation); diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs index a99fb405e..209135deb 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -52,7 +52,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization using IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); // Build the histogram of the grayscale levels - var grayscaleOperation = new GrayscaleLevelsRowIntervalOperation(interest, histogramBuffer, source, this.LuminanceLevels); + var grayscaleOperation = new GrayscaleLevelsRowOperation(interest, histogramBuffer, source, this.LuminanceLevels); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; // Apply the cdf to each pixel of the image - var cdfOperation = new CdfApplicationRowIntervalOperation(interest, cdfBuffer, source, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + var cdfOperation = new CdfApplicationRowOperation(interest, cdfBuffer, source, this.LuminanceLevels, numberOfPixelsMinusCdfMin); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -85,7 +85,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// /// A implementing the grayscale levels logic for . /// - private readonly struct GrayscaleLevelsRowIntervalOperation : IRowIntervalOperation + private readonly struct GrayscaleLevelsRowOperation : IRowOperation { private readonly Rectangle bounds; private readonly IMemoryOwner histogramBuffer; @@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization private readonly int luminanceLevels; [MethodImpl(InliningOptions.ShortMethod)] - public GrayscaleLevelsRowIntervalOperation( + public GrayscaleLevelsRowOperation( Rectangle bounds, IMemoryOwner histogramBuffer, ImageFrame source, @@ -107,18 +107,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan()); - for (int y = rows.Min; y < rows.Max; y++) - { - ref TPixel pixelBase = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + ref TPixel pixelBase = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); - for (int x = 0; x < this.bounds.Width; x++) - { - int luminance = GetLuminance(Unsafe.Add(ref pixelBase, x), this.luminanceLevels); - Unsafe.Add(ref histogramBase, luminance)++; - } + for (int x = 0; x < this.bounds.Width; x++) + { + int luminance = GetLuminance(Unsafe.Add(ref pixelBase, x), this.luminanceLevels); + Unsafe.Add(ref histogramBase, luminance)++; } } } @@ -126,7 +123,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// /// A implementing the cdf application levels logic for . /// - private readonly struct CdfApplicationRowIntervalOperation : IRowIntervalOperation + private readonly struct CdfApplicationRowOperation : IRowOperation { private readonly Rectangle bounds; private readonly IMemoryOwner cdfBuffer; @@ -135,7 +132,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization private readonly float numberOfPixelsMinusCdfMin; [MethodImpl(InliningOptions.ShortMethod)] - public CdfApplicationRowIntervalOperation( + public CdfApplicationRowOperation( Rectangle bounds, IMemoryOwner cdfBuffer, ImageFrame source, @@ -151,20 +148,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); - for (int y = rows.Min; y < rows.Max; y++) + ref TPixel pixelBase = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); + + for (int x = 0; x < this.bounds.Width; x++) { - ref TPixel pixelBase = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y)); - - for (int x = 0; x < this.bounds.Width; x++) - { - ref TPixel pixel = ref Unsafe.Add(ref pixelBase, x); - int luminance = GetLuminance(pixel, this.luminanceLevels); - float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / this.numberOfPixelsMinusCdfMin; - pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); - } + ref TPixel pixel = ref Unsafe.Add(ref pixelBase, x); + int luminance = GetLuminance(pixel, this.luminanceLevels); + float luminanceEqualized = Unsafe.Add(ref cdfBase, luminance) / this.numberOfPixelsMinusCdfMin; + pixel.FromVector4(new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, pixel.ToVector4().W)); } } } diff --git a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs index a1e9d1c4e..727e72469 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs @@ -49,14 +49,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays PixelBlender blender = PixelOperations.Instance.GetPixelBlender(graphicsOptions); - var operation = new RowIntervalOperation(configuration, interest, blender, amount, colors, source); + var operation = new RowOperation(configuration, interest, blender, amount, colors, source); ParallelRowIterator.IterateRows( configuration, interest, in operation); } - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly Configuration configuration; private readonly Rectangle bounds; @@ -66,7 +66,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays private readonly ImageFrame source; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( Configuration configuration, Rectangle bounds, PixelBlender blender, @@ -83,23 +83,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span destination = - this.source.GetPixelRowSpan(y) - .Slice(this.bounds.X, this.bounds.Width); + Span destination = + this.source.GetPixelRowSpan(y) + .Slice(this.bounds.X, this.bounds.Width); - // Switch color & destination in the 2nd and 3rd places because we are - // applying the target color under the current one. - this.blender.Blend( - this.configuration, - destination, - this.colors.GetSpan(), - destination, - this.amount.GetSpan()); - } + // Switch color & destination in the 2nd and 3rd places because we are + // applying the target color under the current one. + this.blender.Blend( + this.configuration, + destination, + this.colors.GetSpan(), + destination, + this.amount.GetSpan()); } } } diff --git a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs index 33c4123e9..fbecbc37c 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs @@ -55,14 +55,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays using IMemoryOwner rowColors = allocator.Allocate(interest.Width); rowColors.GetSpan().Fill(glowColor); - var operation = new RowIntervalOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source); - ParallelRowIterator.IterateRows( + var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source); + ParallelRowIterator.IterateRows( configuration, interest, in operation); } - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly Configuration configuration; private readonly Rectangle bounds; @@ -74,7 +74,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays private readonly ImageFrame source; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( Configuration configuration, Rectangle bounds, IMemoryOwner colors, @@ -95,27 +95,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { Span colorSpan = this.colors.GetSpan(); - for (int y = rows.Min; y < rows.Max; y++) + for (int i = 0; i < this.bounds.Width; i++) { - for (int i = 0; i < this.bounds.Width; i++) - { - float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y)); - span[i] = (this.blendPercent * (1 - (.95F * (distance / this.maxDistance)))).Clamp(0, 1); - } + float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y)); + span[i] = (this.blendPercent * (1 - (.95F * (distance / this.maxDistance)))).Clamp(0, 1); + } - Span destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + Span destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width); - this.blender.Blend( - this.configuration, - destination, - destination, - colorSpan, - span); - } + this.blender.Blend( + this.configuration, + destination, + destination, + colorSpan, + span); } } } diff --git a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs index 0618873f2..378009c40 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs @@ -63,14 +63,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays using IMemoryOwner rowColors = allocator.Allocate(interest.Width); rowColors.GetSpan().Fill(vignetteColor); - var operation = new RowIntervalOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source); - ParallelRowIterator.IterateRows( + var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source); + ParallelRowIterator.IterateRows( configuration, interest, in operation); } - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly Configuration configuration; private readonly Rectangle bounds; @@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays private readonly ImageFrame source; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation( + public RowOperation( Configuration configuration, Rectangle bounds, IMemoryOwner colors, @@ -103,27 +103,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { Span colorSpan = this.colors.GetSpan(); - for (int y = rows.Min; y < rows.Max; y++) + for (int i = 0; i < this.bounds.Width; i++) { - for (int i = 0; i < this.bounds.Width; i++) - { - float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y)); - span[i] = (this.blendPercent * (.9F * (distance / this.maxDistance))).Clamp(0, 1); - } - - Span destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width); - - this.blender.Blend( - this.configuration, - destination, - destination, - colorSpan, - span); + float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y)); + span[i] = (this.blendPercent * (.9F * (distance / this.maxDistance))).Clamp(0, 1); } + + Span destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + + this.blender.Blend( + this.configuration, + destination, + destination, + colorSpan, + span); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 929a66674..775e0aa23 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -5,101 +5,100 @@ using System; using System.Collections.Concurrent; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// - /// Gets the closest color to the supplied color based upon the Eucladean distance. - /// TODO: Expose this somehow. + /// Gets the closest color to the supplied color based upon the Euclidean distance. /// /// The pixel format. - internal readonly struct EuclideanPixelMap : IPixelMap, IEquatable> + internal readonly struct EuclideanPixelMap where TPixel : unmanaged, IPixel { - private readonly ConcurrentDictionary vectorCache; + private readonly Vector4[] vectorCache; private readonly ConcurrentDictionary distanceCache; /// /// Initializes a new instance of the struct. /// + /// The configuration. /// The color palette to map from. - public EuclideanPixelMap(ReadOnlyMemory palette) + [MethodImpl(InliningOptions.ShortMethod)] + public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette) { - Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); - this.Palette = palette; - ReadOnlySpan paletteSpan = this.Palette.Span; - this.vectorCache = new ConcurrentDictionary(); - this.distanceCache = new ConcurrentDictionary(); + this.vectorCache = new Vector4[palette.Length]; - for (int i = 0; i < paletteSpan.Length; i++) - { - this.vectorCache[i] = paletteSpan[i].ToScaledVector4(); - } + // Use the same rules across all target frameworks. + this.distanceCache = new ConcurrentDictionary(Environment.ProcessorCount, 31); + PixelOperations.Instance.ToVector4(configuration, this.Palette.Span, this.vectorCache); } - /// - public ReadOnlyMemory Palette { get; } - - /// - public override bool Equals(object obj) - => obj is EuclideanPixelMap map && this.Equals(map); - - /// - public bool Equals(EuclideanPixelMap other) - => this.Palette.Equals(other.Palette); + /// + /// Gets the color palette of this . + /// The palette memory is owned by the palette source that created it. + /// + public ReadOnlyMemory Palette + { + [MethodImpl(InliningOptions.ShortMethod)] + get; + } - /// + /// + /// Returns the closest color in the palette and the index of that pixel. + /// The palette contents must match the one used in the constructor. + /// + /// The color to match. + /// The matched color. + /// The index. [MethodImpl(InliningOptions.ShortMethod)] public int GetClosestColor(TPixel color, out TPixel match) { - ReadOnlySpan paletteSpan = this.Palette.Span; + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); // Check if the color is in the lookup table - if (this.distanceCache.TryGetValue(color, out int index)) + if (!this.distanceCache.TryGetValue(color, out int index)) { - match = paletteSpan[index]; - return index; + return this.GetClosestColorSlow(color, ref paletteRef, out match); } - return this.GetClosestColorSlow(color, paletteSpan, out match); + match = Unsafe.Add(ref paletteRef, index); + return index; } - /// - public override int GetHashCode() - => this.vectorCache.GetHashCode(); - [MethodImpl(InliningOptions.ShortMethod)] - private int GetClosestColorSlow(TPixel color, ReadOnlySpan palette, out TPixel match) + private int GetClosestColorSlow(TPixel color, ref TPixel paletteRef, out TPixel match) { // Loop through the palette and find the nearest match. int index = 0; float leastDistance = float.MaxValue; - Vector4 vector = color.ToScaledVector4(); - - for (int i = 0; i < palette.Length; i++) + var vector = color.ToVector4(); + ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference(this.vectorCache); + for (int i = 0; i < this.Palette.Length; i++) { - Vector4 candidate = this.vectorCache[i]; + Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i); float distance = Vector4.DistanceSquared(vector, candidate); - // Less than... assign. + // If it's an exact match, exit the loop + if (distance == 0) + { + index = i; + break; + } + if (distance < leastDistance) { + // Less than... assign. index = i; leastDistance = distance; - - // And if it's an exact match, exit the loop - if (distance == 0) - { - break; - } } } // Now I have the index, pop it into the cache for next time this.distanceCache[color] = index; - match = palette[index]; + match = Unsafe.Add(ref paletteRef, index); return index; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs similarity index 55% rename from src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs rename to src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs index a88d2b1e2..4d75042ea 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs @@ -11,22 +11,40 @@ using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// - /// Contains extension methods for frame quantizers. + /// Contains utility methods for instances. /// - public static class FrameQuantizerExtensions + public static class FrameQuantizerUtilities { + /// + /// Helper method for throwing an exception when a frame quantizer palette has + /// been requested but not built yet. + /// + /// The pixel format. + /// The frame quantizer palette. + /// + /// The palette has not been built via + /// + public static void CheckPaletteState(in ReadOnlyMemory palette) + where TPixel : unmanaged, IPixel + { + if (palette.Equals(default)) + { + throw new InvalidOperationException("Frame Quantizer palette has not been built."); + } + } + /// /// Quantizes an image frame and return the resulting output pixels. /// /// The type of frame quantizer. /// The pixel format. - /// The frame + /// The frame quantizer. /// The source image frame to quantize. /// The bounds within the frame to quantize. /// - /// A representing a quantized version of the source frame pixels. + /// A representing a quantized version of the source frame pixels. /// - public static QuantizedFrame QuantizeFrame( + public static IndexedImageFrame QuantizeFrame( ref TFrameQuantizer quantizer, ImageFrame source, Rectangle bounds) @@ -37,35 +55,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization var interest = Rectangle.Intersect(source.Bounds(), bounds); // Collect the palette. Required before the second pass runs. - ReadOnlyMemory palette = quantizer.BuildPalette(source, interest); - MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator; + quantizer.BuildPalette(source, interest); - var quantizedFrame = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); - Memory output = quantizedFrame.GetWritablePixelMemory(); + var destination = new IndexedImageFrame( + quantizer.Configuration, + interest.Width, + interest.Height, + quantizer.Palette); if (quantizer.Options.Dither is null) { - SecondPass(ref quantizer, source, interest, output, palette); + SecondPass(ref quantizer, source, destination, interest); } else { // We clone the image as we don't want to alter the original via error diffusion based dithering. - using (ImageFrame clone = source.Clone()) - { - SecondPass(ref quantizer, clone, interest, output, palette); - } + using ImageFrame clone = source.Clone(); + SecondPass(ref quantizer, clone, destination, interest); } - return quantizedFrame; + return destination; } [MethodImpl(InliningOptions.ShortMethod)] private static void SecondPass( ref TFrameQuantizer quantizer, ImageFrame source, - Rectangle bounds, - Memory output, - ReadOnlyMemory palette) + IndexedImageFrame destination, + Rectangle bounds) where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel { @@ -73,8 +90,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization if (dither is null) { - var operation = new RowIntervalOperation(quantizer, source, output, bounds, palette); - ParallelRowIterator.IterateRows( + var operation = new RowIntervalOperation( + ref quantizer, + source, + destination, + bounds); + + ParallelRowIterator.IterateRowIntervals( quantizer.Configuration, bounds, in operation); @@ -82,7 +104,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization return; } - dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds); + dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds); } private readonly struct RowIntervalOperation : IRowIntervalOperation @@ -91,43 +113,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { private readonly TFrameQuantizer quantizer; private readonly ImageFrame source; - private readonly Memory output; + private readonly IndexedImageFrame destination; private readonly Rectangle bounds; - private readonly ReadOnlyMemory palette; [MethodImpl(InliningOptions.ShortMethod)] public RowIntervalOperation( - in TFrameQuantizer quantizer, + ref TFrameQuantizer quantizer, ImageFrame source, - Memory output, - Rectangle bounds, - ReadOnlyMemory palette) + IndexedImageFrame destination, + Rectangle bounds) { this.quantizer = quantizer; this.source = source; - this.output = output; + this.destination = destination; this.bounds = bounds; - this.palette = palette; } [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan paletteSpan = this.palette.Span; - Span outputSpan = this.output.Span; - int width = this.bounds.Width; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; for (int y = rows.Min; y < rows.Max; y++) { - Span row = this.source.GetPixelRowSpan(y); - int rowStart = (y - offsetY) * width; + Span sourceRow = this.source.GetPixelRowSpan(y); + Span destinationRow = this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY); - // TODO: This can be a bulk operation. for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(row[x], paletteSpan, out TPixel _); + destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], out TPixel _); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 5aaae9fa6..cc87715eb 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -24,33 +24,37 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization QuantizerOptions Options { get; } /// - /// Quantizes an image frame and return the resulting output pixels. + /// Gets the quantized color palette. /// - /// The source image frame to quantize. - /// The bounds within the frame to quantize. - /// - /// A representing a quantized version of the source frame pixels. - /// - QuantizedFrame QuantizeFrame( - ImageFrame source, - Rectangle bounds); + /// + /// The palette has not been built via . + /// + ReadOnlyMemory Palette { get; } /// /// Builds the quantized palette from the given image frame and bounds. /// /// The source image frame. /// The region of interest bounds. - /// The palette. - ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds); + void BuildPalette(ImageFrame source, Rectangle bounds); + + /// + /// Quantizes an image frame and return the resulting output pixels. + /// + /// The source image frame to quantize. + /// The bounds within the frame to quantize. + /// + /// A representing a quantized version of the source frame pixels. + /// + IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds); /// - /// Returns the index and color from the quantized palette corresponding to the give to the given color. + /// Returns the index and color from the quantized palette corresponding to the given color. /// /// The color to match. - /// The output color palette. /// The matched color. /// The index. - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match); + byte GetQuantizedColor(TPixel color, out TPixel match); // TODO: Enable bulk operations. // void GetQuantizedColors(ReadOnlySpan colors, ReadOnlySpan palette, Span indices, Span matches); diff --git a/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs deleted file mode 100644 index b421dce21..000000000 --- a/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Quantization -{ - /// - /// Allows the mapping of input colors to colors within a given palette. - /// TODO: Expose this somehow. - /// - /// The pixel format. - internal interface IPixelMap - where TPixel : unmanaged, IPixel - { - /// - /// Gets the color palette containing colors to match. - /// - ReadOnlyMemory Palette { get; } - - /// - /// Returns the closest color in the palette and the index of that pixel. - /// - /// The color to match. - /// The matched color. - /// The index. - int GetClosestColor(TPixel color, out TPixel match); - } -} diff --git a/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs new file mode 100644 index 000000000..ac737f452 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization +{ + /// + /// A pixel-specific image frame where each pixel buffer value represents an index in a color palette. + /// + /// The pixel format. + public sealed class IndexedImageFrame : IDisposable + where TPixel : unmanaged, IPixel + { + private IMemoryOwner pixelsOwner; + private IMemoryOwner paletteOwner; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The configuration which allows altering default behaviour or extending the library. + /// + /// The frame width. + /// The frame height. + /// The color palette. + internal IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory palette) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.MustBeLessThanOrEqualTo(palette.Length, QuantizerConstants.MaxColors, nameof(palette)); + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + this.Configuration = configuration; + this.Width = width; + this.Height = height; + this.pixelsOwner = configuration.MemoryAllocator.AllocateManagedByteBuffer(width * height); + + // Copy the palette over. We want the lifetime of this frame to be independant of any palette source. + this.paletteOwner = configuration.MemoryAllocator.Allocate(palette.Length); + palette.Span.CopyTo(this.paletteOwner.GetSpan()); + this.Palette = this.paletteOwner.Memory.Slice(0, palette.Length); + } + + /// + /// Gets the configuration which allows altering default behaviour or extending the library. + /// + public Configuration Configuration { get; } + + /// + /// Gets the width of this . + /// + public int Width { get; } + + /// + /// Gets the height of this . + /// + public int Height { get; } + + /// + /// Gets the color palette of this . + /// + public ReadOnlyMemory Palette { get; } + + /// + /// Gets the pixels of this . + /// + /// The + [MethodImpl(InliningOptions.ShortMethod)] + public ReadOnlySpan GetPixelBufferSpan() => this.pixelsOwner.GetSpan(); // TODO: Buffer2D + + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the the first pixel on that row. + /// + /// The row index in the pixel buffer. + /// The pixel row as a . + [MethodImpl(InliningOptions.ShortMethod)] + public ReadOnlySpan GetPixelRowSpan(int rowIndex) + => this.GetWritablePixelRowSpanUnsafe(rowIndex); + + /// + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the the first pixel on that row. + /// + /// + /// Note: Values written to this span are not sanitized against the palette length. + /// Care should be taken during assignment to prevent out-of-bounds errors. + /// + /// + /// The row index in the pixel buffer. + /// The pixel row as a . + [MethodImpl(InliningOptions.ShortMethod)] + public Span GetWritablePixelRowSpanUnsafe(int rowIndex) + => this.pixelsOwner.GetSpan().Slice(rowIndex * this.Width, this.Width); + + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.pixelsOwner.Dispose(); + this.paletteOwner.Dispose(); + this.pixelsOwner = null; + this.paletteOwner = null; + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index 1b7c9edd6..ce2e406d4 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -3,9 +3,9 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -20,10 +20,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public struct OctreeFrameQuantizer : IFrameQuantizer where TPixel : unmanaged, IPixel { - private readonly int colors; + private readonly int maxColors; private readonly Octree octree; + private IMemoryOwner paletteOwner; + private ReadOnlyMemory palette; private EuclideanPixelMap pixelMap; private readonly bool isDithering; + private bool isDisposed; /// /// Initializes a new instance of the struct. @@ -39,10 +42,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.Configuration = configuration; this.Options = options; - this.colors = this.Options.MaxColors; - this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); + this.maxColors = this.Options.MaxColors; + this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.maxColors).Clamp(1, 8)); + this.paletteOwner = configuration.MemoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); + this.palette = default; this.pixelMap = default; this.isDithering = !(this.Options.Dither is null); + this.isDisposed = false; } /// @@ -52,13 +58,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public QuantizerOptions Options { get; } /// - [MethodImpl(InliningOptions.ShortMethod)] - public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); + public ReadOnlyMemory Palette + { + get + { + FrameQuantizerUtilities.CheckPaletteState(in this.palette); + return this.palette; + } + } /// [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + public void BuildPalette(ImageFrame source, Rectangle bounds) { using IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(bounds.Width); Span bufferSpan = buffer.GetSpan(); @@ -78,32 +89,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } } - TPixel[] palette = this.octree.Palletize(this.colors); - this.pixelMap = new EuclideanPixelMap(palette); + Span paletteSpan = this.paletteOwner.GetSpan(); + int paletteIndex = 0; + this.octree.Palletize(paletteSpan, this.maxColors, ref paletteIndex); + + // Length of reduced palette + transparency. + ReadOnlyMemory result = this.paletteOwner.Memory.Slice(0, Math.Min(paletteIndex + 2, QuantizerConstants.MaxColors)); + this.pixelMap = new EuclideanPixelMap(this.Configuration, result); - return palette; + this.palette = result; } /// [MethodImpl(InliningOptions.ShortMethod)] - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public readonly byte GetQuantizedColor(TPixel color, out TPixel match) { // Octree only maps the RGB component of a color // so cannot tell the difference between a fully transparent // pixel and a black one. - if (!this.isDithering && !color.Equals(default)) + if (this.isDithering || color.Equals(default)) { - var index = (byte)this.octree.GetPaletteIndex(color); - match = palette[index]; - return index; + return (byte)this.pixelMap.GetClosestColor(color, out match); } - return (byte)this.pixelMap.GetClosestColor(color, out match); + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.Palette.Span); + var index = (byte)this.octree.GetPaletteIndex(color); + match = Unsafe.Add(ref paletteRef, index); + return index; } /// public void Dispose() { + if (!this.isDisposed) + { + this.isDisposed = true; + this.paletteOwner.Dispose(); + this.paletteOwner = null; + } } /// @@ -111,21 +139,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private sealed class Octree { - /// - /// Mask used when getting the appropriate pixels for a given node. - /// - private static readonly byte[] Mask = new byte[] - { - 0b10000000, - 0b1000000, - 0b100000, - 0b10000, - 0b1000, - 0b100, - 0b10, - 0b1 - }; - /// /// The root of the Octree /// @@ -162,6 +175,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.previousNode = null; } + /// + /// Gets the mask used when getting the appropriate pixels for a given node. + /// + private static ReadOnlySpan Mask => new byte[] + { + 0b10000000, + 0b1000000, + 0b100000, + 0b10000, + 0b1000, + 0b100, + 0b10, + 0b1 + }; + /// /// Gets or sets the number of leaves in the tree /// @@ -216,26 +244,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Convert the nodes in the Octree to a palette with a maximum of colorCount colors /// + /// The palette to fill. /// The maximum number of colors - /// - /// An with the palletized colors - /// + /// The palette index, used to calculate the final size of the palette. [MethodImpl(InliningOptions.ShortMethod)] - public TPixel[] Palletize(int colorCount) + public void Palletize(Span palette, int colorCount, ref int paletteIndex) { while (this.Leaves > colorCount - 1) { this.Reduce(); } - // Now palletize the nodes - var palette = new TPixel[colorCount]; - - int paletteIndex = 0; this.root.ConstructPalette(palette, ref paletteIndex); - - // And return the palette - return palette; } /// @@ -437,12 +457,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The palette /// The current palette index [MethodImpl(InliningOptions.ColdPath)] - public void ConstructPalette(TPixel[] palette, ref int index) + public void ConstructPalette(Span palette, ref int index) { if (this.leaf) { // Set the color of the palette entry - var vector = Vector3.Clamp(new Vector3(this.red, this.green, this.blue) / this.pixelCount, Vector3.Zero, new Vector3(255)); + var vector = Vector3.Clamp( + new Vector3(this.red, this.green, this.blue) / this.pixelCount, + Vector3.Zero, + new Vector3(255)); + TPixel pixel = default; pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue)); palette[index] = pixel; @@ -513,11 +537,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization [MethodImpl(InliningOptions.ShortMethod)] private static int GetColorIndex(ref Rgba32 color, int level) { + DebugGuard.MustBeLessThan(level, Mask.Length, nameof(level)); + int shift = 7 - level; - byte mask = Mask[level]; + ref byte maskRef = ref MemoryMarshal.GetReference(Mask); + byte mask = Unsafe.Add(ref maskRef, level); + return ((color.R & mask) >> shift) - | ((color.G & mask) >> (shift - 1)) - | ((color.B & mask) >> (shift - 2)); + | ((color.G & mask) >> (shift - 1)) + | ((color.B & mask) >> (shift - 2)); } /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index 3328fd6c7..9e04edef0 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -11,12 +11,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class OctreeQuantizer : IQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); + /// /// Initializes a new instance of the class /// using the default . /// public OctreeQuantizer() - : this(new QuantizerOptions()) + : this(DefaultOptions) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 3200dfab8..ade73e2d0 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -15,7 +15,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization internal struct PaletteFrameQuantizer : IFrameQuantizer where TPixel : unmanaged, IPixel { - private readonly ReadOnlyMemory palette; private readonly EuclideanPixelMap pixelMap; /// @@ -23,18 +22,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The configuration which allows altering default behaviour or extending the library. /// The quantizer options defining quantization rules. - /// A containing all colors in the palette. + /// The pixel map for looking up color matches from a predefined palette. [MethodImpl(InliningOptions.ShortMethod)] - public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory colors) + public PaletteFrameQuantizer( + Configuration configuration, + QuantizerOptions options, + EuclideanPixelMap pixelMap) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(options, nameof(options)); this.Configuration = configuration; this.Options = options; - - this.palette = colors; - this.pixelMap = new EuclideanPixelMap(colors); + this.pixelMap = pixelMap; } /// @@ -43,19 +43,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public QuantizerOptions Options { get; } + /// + public ReadOnlyMemory Palette => this.pixelMap.Palette; + /// [MethodImpl(InliningOptions.ShortMethod)] - public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) - => this.palette; + public void BuildPalette(ImageFrame source, Rectangle bounds) + { + } /// [MethodImpl(InliningOptions.ShortMethod)] - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, out TPixel match) => (byte)this.pixelMap.GetClosestColor(color, out match); /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index 07fa6e41e..c14ea6153 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -11,12 +11,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class PaletteQuantizer : IQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); + private readonly ReadOnlyMemory colorPalette; + /// /// Initializes a new instance of the class. /// /// The color palette. public PaletteQuantizer(ReadOnlyMemory palette) - : this(palette, new QuantizerOptions()) + : this(palette, DefaultOptions) { } @@ -30,15 +33,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); Guard.NotNull(options, nameof(options)); - this.Palette = palette; + this.colorPalette = palette; this.Options = options; } - /// - /// Gets the color palette. - /// - public ReadOnlyMemory Palette { get; } - /// public QuantizerOptions Options { get; } @@ -53,11 +51,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { Guard.NotNull(options, nameof(options)); - int length = Math.Min(this.Palette.Span.Length, options.MaxColors); + // The palette quantizer can reuse the same pixel map across multiple frames + // since the palette is unchanging. This allows a reduction of memory usage across + // multi frame gifs using a global palette. + int length = Math.Min(this.colorPalette.Length, options.MaxColors); var palette = new TPixel[length]; - Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); - return new PaletteFrameQuantizer(configuration, options, palette); + Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan()); + + var pixelMap = new EuclideanPixelMap(configuration, palette); + return new PaletteFrameQuantizer(configuration, options, pixelMap); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs index 5a0116a03..4583b7cff 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -39,10 +39,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Configuration configuration = this.Configuration; using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(configuration); - using QuantizedFrame quantized = frameQuantizer.QuantizeFrame(source, interest); + using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(source, interest); var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( configuration, interest, in operation); @@ -52,13 +52,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { private readonly Rectangle bounds; private readonly ImageFrame source; - private readonly QuantizedFrame quantized; + private readonly IndexedImageFrame quantized; [MethodImpl(InliningOptions.ShortMethod)] public RowIntervalOperation( Rectangle bounds, ImageFrame source, - QuantizedFrame quantized) + IndexedImageFrame quantized) { this.bounds = bounds; this.source = source; @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan quantizedPixelSpan = this.quantized.GetPixelSpan(); + ReadOnlySpan quantizedPixelSpan = this.quantized.GetPixelBufferSpan(); ReadOnlySpan paletteSpan = this.quantized.Palette.Span; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs deleted file mode 100644 index a7c67a868..000000000 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Quantization -{ - /// - /// Represents a quantized image frame where the pixels indexed by a color palette. - /// - /// The pixel format. - public sealed class QuantizedFrame : IDisposable - where TPixel : unmanaged, IPixel - { - private IMemoryOwner pixels; - private bool isDisposed; - - /// - /// Initializes a new instance of the class. - /// - /// Used to allocated memory for image processing operations. - /// The image width. - /// The image height. - /// The color palette. - internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, ReadOnlyMemory palette) - { - Guard.MustBeGreaterThan(width, 0, nameof(width)); - Guard.MustBeGreaterThan(height, 0, nameof(height)); - - this.Width = width; - this.Height = height; - this.Palette = palette; - this.pixels = memoryAllocator.AllocateManagedByteBuffer(width * height, AllocationOptions.Clean); - } - - /// - /// Gets the width of this . - /// - public int Width { get; } - - /// - /// Gets the height of this . - /// - public int Height { get; } - - /// - /// Gets the color palette of this . - /// - public ReadOnlyMemory Palette { get; private set; } - - /// - /// Gets the pixels of this . - /// - /// The - [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlySpan 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 pixel row as a . - [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlySpan GetRowSpan(int rowIndex) - => this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width); - - /// - public void Dispose() - { - if (this.isDisposed) - { - return; - } - - this.isDisposed = true; - this.pixels?.Dispose(); - this.pixels = null; - this.Palette = null; - } - - /// - /// Get the non-readonly memory of pixel data so can fill it. - /// - internal Memory GetWritablePixelMemory() => this.pixels.Memory; - } -} diff --git a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs index 8aa634b9f..d95ed5aab 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs @@ -10,11 +10,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WebSafePaletteQuantizer : PaletteQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); + /// /// Initializes a new instance of the class. /// public WebSafePaletteQuantizer() - : this(new QuantizerOptions()) + : this(DefaultOptions) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs index 168c837d5..8f8e38dd9 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs @@ -9,11 +9,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WernerPaletteQuantizer : PaletteQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); + /// /// Initializes a new instance of the class. /// public WernerPaletteQuantizer() - : this(new QuantizerOptions()) + : this(DefaultOptions) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 177f7e859..d15db74e6 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -65,30 +66,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; - /// - /// Color moments. - /// - private IMemoryOwner moments; - - /// - /// Color space tag. - /// - private IMemoryOwner tag; - - /// - /// Maximum allowed color depth - /// - private int colors; - - /// - /// The color cube representing the image palette - /// + private IMemoryOwner momentsOwner; + private IMemoryOwner tagsOwner; + private IMemoryOwner paletteOwner; + private ReadOnlyMemory palette; + private int maxColors; private readonly Box[] colorCube; - private EuclideanPixelMap pixelMap; - private readonly bool isDithering; - private bool isDisposed; /// @@ -104,11 +89,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.Configuration = configuration; this.Options = options; + this.maxColors = this.Options.MaxColors; this.memoryAllocator = this.Configuration.MemoryAllocator; - this.moments = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.tag = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.colors = this.Options.MaxColors; - this.colorCube = new Box[this.colors]; + this.momentsOwner = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.tagsOwner = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.paletteOwner = this.memoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); + this.palette = default; + this.colorCube = new Box[this.maxColors]; this.isDisposed = false; this.pixelMap = default; this.isDithering = this.isDithering = !(this.Options.Dither is null); @@ -121,21 +108,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public QuantizerOptions Options { get; } /// - [MethodImpl(InliningOptions.ShortMethod)] - public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); + public ReadOnlyMemory Palette + { + get + { + FrameQuantizerUtilities.CheckPaletteState(in this.palette); + return this.palette; + } + } /// - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + public void BuildPalette(ImageFrame source, Rectangle bounds) { this.Build3DHistogram(source, bounds); this.Get3DMoments(this.memoryAllocator); this.BuildCube(); - var palette = new TPixel[this.colors]; - ReadOnlySpan momentsSpan = this.moments.GetSpan(); - - for (int k = 0; k < this.colors; k++) + ReadOnlySpan momentsSpan = this.momentsOwner.GetSpan(); + Span paletteSpan = this.paletteOwner.GetSpan(); + for (int k = 0; k < this.maxColors; k++) { this.Mark(ref this.colorCube[k], (byte)k); @@ -143,50 +134,57 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization if (moment.Weight > 0) { - ref TPixel color = ref palette[k]; + ref TPixel color = ref paletteSpan[k]; color.FromScaledVector4(moment.Normalize()); } } - this.pixelMap = new EuclideanPixelMap(palette); - return palette; + ReadOnlyMemory result = this.paletteOwner.Memory.Slice(0, this.maxColors); + this.pixelMap = new EuclideanPixelMap(this.Configuration, result); + this.palette = result; } /// - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + [MethodImpl(InliningOptions.ShortMethod)] + public readonly IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); + + /// + public readonly byte GetQuantizedColor(TPixel color, out TPixel match) { - if (!this.isDithering) + if (this.isDithering) { - Rgba32 rgba = default; - color.ToRgba32(ref rgba); - - int r = rgba.R >> (8 - IndexBits); - int g = rgba.G >> (8 - IndexBits); - int b = rgba.B >> (8 - IndexBits); - int a = rgba.A >> (8 - IndexAlphaBits); - - ReadOnlySpan tagSpan = this.tag.GetSpan(); - byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; - match = palette[index]; - return index; + return (byte)this.pixelMap.GetClosestColor(color, out match); } - return (byte)this.pixelMap.GetClosestColor(color, out match); + Rgba32 rgba = default; + color.ToRgba32(ref rgba); + + int r = rgba.R >> (8 - IndexBits); + int g = rgba.G >> (8 - IndexBits); + int b = rgba.B >> (8 - IndexBits); + int a = rgba.A >> (8 - IndexAlphaBits); + + ReadOnlySpan tagSpan = this.tagsOwner.GetSpan(); + byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.Palette.Span); + match = Unsafe.Add(ref paletteRef, index); + return index; } /// public void Dispose() { - if (this.isDisposed) + if (!this.isDisposed) { - return; + this.isDisposed = true; + this.momentsOwner?.Dispose(); + this.tagsOwner?.Dispose(); + this.paletteOwner?.Dispose(); + this.momentsOwner = null; + this.tagsOwner = null; + this.paletteOwner = null; } - - this.isDisposed = true; - this.moments?.Dispose(); - this.tag?.Dispose(); - this.moments = null; - this.tag = null; } /// @@ -364,7 +362,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The bounds within the source image to quantize. private void Build3DHistogram(ImageFrame source, Rectangle bounds) { - Span momentSpan = this.moments.GetSpan(); + Span momentSpan = this.momentsOwner.GetSpan(); // Build up the 3-D color histogram using IMemoryOwner buffer = this.memoryAllocator.Allocate(bounds.Width); @@ -392,13 +390,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// - /// The memory allocator used for allocating buffers. - private void Get3DMoments(MemoryAllocator memoryAllocator) + /// The memory allocator used for allocating buffers. + private void Get3DMoments(MemoryAllocator allocator) { - using IMemoryOwner volume = memoryAllocator.Allocate(IndexCount * IndexAlphaCount); - using IMemoryOwner area = memoryAllocator.Allocate(IndexAlphaCount); + using IMemoryOwner volume = allocator.Allocate(IndexCount * IndexAlphaCount); + using IMemoryOwner area = allocator.Allocate(IndexAlphaCount); - Span momentSpan = this.moments.GetSpan(); + Span momentSpan = this.momentsOwner.GetSpan(); Span volumeSpan = volume.GetSpan(); Span areaSpan = area.GetSpan(); int baseIndex = GetPaletteIndex(1, 0, 0, 0); @@ -440,7 +438,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The . private double Variance(ref Box cube) { - ReadOnlySpan momentSpan = this.moments.GetSpan(); + ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); Moment volume = Volume(ref cube, momentSpan); Moment variance = @@ -481,7 +479,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The . private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole) { - ReadOnlySpan momentSpan = this.moments.GetSpan(); + ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); Moment bottom = Bottom(ref cube, direction, momentSpan); float max = 0F; @@ -527,7 +525,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Returns a value indicating whether the box has been split. private bool Cut(ref Box set1, ref Box set2) { - ReadOnlySpan momentSpan = this.moments.GetSpan(); + ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); Moment whole = Volume(ref set1, momentSpan); float maxR = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutR, whole); @@ -612,7 +610,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// A label. private void Mark(ref Box cube, byte label) { - Span tagSpan = this.tag.GetSpan(); + Span tagSpan = this.tagsOwner.GetSpan(); for (int r = cube.RMin + 1; r <= cube.RMax; r++) { @@ -634,7 +632,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private void BuildCube() { - Span vv = stackalloc double[this.colors]; + // Store the volume variance. + using IMemoryOwner vvOwner = this.Configuration.MemoryAllocator.Allocate(this.maxColors); + Span vv = vvOwner.GetSpan(); ref Box cube = ref this.colorCube[0]; cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0; @@ -643,7 +643,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization int next = 0; - for (int i = 1; i < this.colors; i++) + for (int i = 1; i < this.maxColors; i++) { ref Box nextCube = ref this.colorCube[next]; ref Box currentCube = ref this.colorCube[i]; @@ -672,7 +672,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization if (temp <= 0D) { - this.colors = i + 1; + this.maxColors = i + 1; break; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs index 872e6d5bd..d2e33aa1f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -10,12 +10,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WuQuantizer : IQuantizer { + private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); + /// /// Initializes a new instance of the class /// using the default . /// public WuQuantizer() - : this(new QuantizerOptions()) + : this(DefaultOptions) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs index a4bcb3d71..a366fd51d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs @@ -51,7 +51,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(this.Configuration).MultiplyMinimumPixelsPerTask(4); - var operation = new RowIntervalOperation(bounds, source, destination); + var operation = new RowOperation(bounds, source, destination); ParallelRowIterator.IterateRows( bounds, @@ -62,20 +62,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// A implementing the processor logic for . /// - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly Rectangle bounds; private readonly ImageFrame source; private readonly ImageFrame destination; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The target processing bounds for the current instance. /// The source for the current instance. /// The destination for the current instance. [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation(Rectangle bounds, ImageFrame source, ImageFrame destination) + public RowOperation(Rectangle bounds, ImageFrame source, ImageFrame destination) { this.bounds = bounds; this.source = source; @@ -84,14 +84,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.Left); - Span targetRow = this.destination.GetPixelRowSpan(y - this.bounds.Top); - sourceRow.Slice(0, this.bounds.Width).CopyTo(targetRow); - } + Span sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.Left); + Span targetRow = this.destination.GetPixelRowSpan(y - this.bounds.Top); + sourceRow.Slice(0, this.bounds.Width).CopyTo(targetRow); } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs index ac18c67cd..a3c8f7108 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs @@ -111,7 +111,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms in operation); } - private readonly struct NNAffineOperation : IRowIntervalOperation + private readonly struct NNAffineOperation : IRowOperation { private readonly ImageFrame source; private readonly ImageFrame destination; @@ -133,28 +133,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) + Span destRow = this.destination.GetPixelRowSpan(y); + + for (int x = 0; x < this.maxX; x++) { - Span destRow = this.destination.GetPixelRowSpan(y); + var point = Vector2.Transform(new Vector2(x, y), this.matrix); + int px = (int)MathF.Round(point.X); + int py = (int)MathF.Round(point.Y); - for (int x = 0; x < this.maxX; x++) + if (this.bounds.Contains(px, py)) { - var point = Vector2.Transform(new Vector2(x, y), this.matrix); - int px = (int)MathF.Round(point.X); - int py = (int)MathF.Round(point.Y); - - if (this.bounds.Contains(px, py)) - { - destRow[x] = this.source[px, py]; - } + destRow[x] = this.source[px, py]; } } } } - private readonly struct AffineOperation : IRowIntervalOperation + private readonly struct AffineOperation : IRowOperation where TResampler : struct, IResampler { private readonly Configuration configuration; @@ -193,41 +190,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { Buffer2D sourceBuffer = this.source.PixelBuffer; - for (int y = rows.Min; y < rows.Max; y++) - { - PixelOperations.Instance.ToVector4( - this.configuration, - this.destination.GetPixelRowSpan(y), - span); - ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y)); - ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y)); + PixelOperations.Instance.ToVector4( + this.configuration, + this.destination.GetPixelRowSpan(y), + span); - for (int x = 0; x < this.maxX; x++) - { - // 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), this.matrix); - LinearTransformUtilities.Convolve( - in this.sampler, - point, - sourceBuffer, - span, - x, - ref yKernelSpanRef, - ref xKernelSpanRef, - this.radialExtents, - this.maxSourceExtents); - } + ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y)); + ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y)); - PixelOperations.Instance.FromVector4Destructive( - this.configuration, + for (int x = 0; x < this.maxX; x++) + { + // 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), this.matrix); + LinearTransformUtilities.Convolve( + in this.sampler, + point, + sourceBuffer, span, - this.destination.GetPixelRowSpan(y)); + x, + ref yKernelSpanRef, + ref xKernelSpanRef, + this.radialExtents, + this.maxSourceExtents); } + + PixelOperations.Instance.FromVector4Destructive( + this.configuration, + span, + this.destination.GetPixelRowSpan(y)); } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs index 8db2d42c6..470eafcd8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs @@ -5,7 +5,6 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -76,28 +75,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The configuration. private void FlipY(ImageFrame source, Configuration configuration) { - var operation = new RowIntervalOperation(source); + var operation = new RowOperation(source); ParallelRowIterator.IterateRows( configuration, source.Bounds(), in operation); } - private readonly struct RowIntervalOperation : IRowIntervalOperation + private readonly struct RowOperation : IRowOperation { private readonly ImageFrame source; [MethodImpl(InliningOptions.ShortMethod)] - public RowIntervalOperation(ImageFrame source) => this.source = source; + public RowOperation(ImageFrame source) => this.source = source; [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) - { - for (int y = rows.Min; y < rows.Max; y++) - { - this.source.GetPixelRowSpan(y).Reverse(); - } - } + public void Invoke(int y) => this.source.GetPixelRowSpan(y).Reverse(); } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs index 89ebb50f2..f348721d7 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs @@ -111,7 +111,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms in operation); } - private readonly struct NNProjectiveOperation : IRowIntervalOperation + private readonly struct NNProjectiveOperation : IRowOperation { private readonly ImageFrame source; private readonly ImageFrame destination; @@ -133,28 +133,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) + Span destRow = this.destination.GetPixelRowSpan(y); + + for (int x = 0; x < this.maxX; x++) { - Span destRow = this.destination.GetPixelRowSpan(y); + Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix); + int px = (int)MathF.Round(point.X); + int py = (int)MathF.Round(point.Y); - for (int x = 0; x < this.maxX; x++) + if (this.bounds.Contains(px, py)) { - Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix); - int px = (int)MathF.Round(point.X); - int py = (int)MathF.Round(point.Y); - - if (this.bounds.Contains(px, py)) - { - destRow[x] = this.source[px, py]; - } + destRow[x] = this.source[px, py]; } } } } - private readonly struct ProjectiveOperation : IRowIntervalOperation + private readonly struct ProjectiveOperation : IRowOperation where TResampler : struct, IResampler { private readonly Configuration configuration; @@ -193,41 +190,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows, Span span) + public void Invoke(int y, Span span) { Buffer2D sourceBuffer = this.source.PixelBuffer; - for (int y = rows.Min; y < rows.Max; y++) - { - PixelOperations.Instance.ToVector4( - this.configuration, - this.destination.GetPixelRowSpan(y), - span); - ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y)); - ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y)); + PixelOperations.Instance.ToVector4( + this.configuration, + this.destination.GetPixelRowSpan(y), + span); - for (int x = 0; x < this.maxX; x++) - { - // Use the single precision position to calculate correct bounding pixels - // otherwise we get rogue pixels outside of the bounds. - Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix); - LinearTransformUtilities.Convolve( - in this.sampler, - point, - sourceBuffer, - span, - x, - ref yKernelSpanRef, - ref xKernelSpanRef, - this.radialExtents, - this.maxSourceExtents); - } + ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y)); + ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y)); - PixelOperations.Instance.FromVector4Destructive( - this.configuration, + for (int x = 0; x < this.maxX; x++) + { + // Use the single precision position to calculate correct bounding pixels + // otherwise we get rogue pixels outside of the bounds. + Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix); + LinearTransformUtilities.Convolve( + in this.sampler, + point, + sourceBuffer, span, - this.destination.GetPixelRowSpan(y)); + x, + ref yKernelSpanRef, + ref xKernelSpanRef, + this.radialExtents, + this.maxSourceExtents); } + + PixelOperations.Instance.FromVector4Destructive( + this.configuration, + span, + this.destination.GetPixelRowSpan(y)); } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs index 6ce5c71f9..43f67f791 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs @@ -131,7 +131,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The configuration. private void Rotate180(ImageFrame source, ImageFrame destination, Configuration configuration) { - var operation = new Rotate180RowIntervalOperation(source.Width, source.Height, source, destination); + var operation = new Rotate180RowOperation(source.Width, source.Height, source, destination); ParallelRowIterator.IterateRows( configuration, source.Bounds(), @@ -147,7 +147,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private void Rotate270(ImageFrame source, ImageFrame destination, Configuration configuration) { var operation = new Rotate270RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source, destination); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( configuration, source.Bounds(), in operation); @@ -161,14 +161,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// The configuration. private void Rotate90(ImageFrame source, ImageFrame destination, Configuration configuration) { - var operation = new Rotate90RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source, destination); + var operation = new Rotate90RowOperation(destination.Bounds(), source.Width, source.Height, source, destination); ParallelRowIterator.IterateRows( configuration, source.Bounds(), in operation); } - private readonly struct Rotate180RowIntervalOperation : IRowIntervalOperation + private readonly struct Rotate180RowOperation : IRowOperation { private readonly int width; private readonly int height; @@ -176,7 +176,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly ImageFrame destination; [MethodImpl(InliningOptions.ShortMethod)] - public Rotate180RowIntervalOperation( + public Rotate180RowOperation( int width, int height, ImageFrame source, @@ -189,17 +189,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) - { - Span sourceRow = this.source.GetPixelRowSpan(y); - Span targetRow = this.destination.GetPixelRowSpan(this.height - y - 1); + Span sourceRow = this.source.GetPixelRowSpan(y); + Span targetRow = this.destination.GetPixelRowSpan(this.height - y - 1); - for (int x = 0; x < this.width; x++) - { - targetRow[this.width - x - 1] = sourceRow[x]; - } + for (int x = 0; x < this.width; x++) + { + targetRow[this.width - x - 1] = sourceRow[x]; } } } @@ -248,7 +245,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } } - private readonly struct Rotate90RowIntervalOperation : IRowIntervalOperation + private readonly struct Rotate90RowOperation : IRowOperation { private readonly Rectangle bounds; private readonly int width; @@ -257,7 +254,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly ImageFrame destination; [MethodImpl(InliningOptions.ShortMethod)] - public Rotate90RowIntervalOperation( + public Rotate90RowOperation( Rectangle bounds, int width, int height, @@ -272,18 +269,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { - for (int y = rows.Min; y < rows.Max; y++) + Span sourceRow = this.source.GetPixelRowSpan(y); + int newX = this.height - y - 1; + for (int x = 0; x < this.width; x++) { - Span sourceRow = this.source.GetPixelRowSpan(y); - int newX = this.height - y - 1; - for (int x = 0; x < this.width; x++) + if (this.bounds.Contains(newX, x)) { - if (this.bounds.Contains(newX, x)) - { - this.destination[newX, x] = sourceRow[x]; - } + this.destination[newX, x] = sourceRow[x]; } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs index 89bd94e06..6eafbda89 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs @@ -144,7 +144,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float widthFactor = sourceRectangle.Width / (float)destinationRectangle.Width; float heightFactor = sourceRectangle.Height / (float)destinationRectangle.Height; - var operation = new NNRowIntervalOperation( + var operation = new NNRowOperation( sourceRectangle, destinationRectangle, widthFactor, @@ -193,7 +193,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } } - private readonly struct NNRowIntervalOperation : IRowIntervalOperation + private readonly struct NNRowOperation : IRowOperation { private readonly Rectangle sourceBounds; private readonly Rectangle destinationBounds; @@ -203,7 +203,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly ImageFrame destination; [MethodImpl(InliningOptions.ShortMethod)] - public NNRowIntervalOperation( + public NNRowOperation( Rectangle sourceBounds, Rectangle destinationBounds, float widthFactor, @@ -220,7 +220,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(in RowInterval rows) + public void Invoke(int y) { int sourceX = this.sourceBounds.X; int sourceY = this.sourceBounds.Y; @@ -229,17 +229,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int destLeft = this.destinationBounds.Left; int destRight = this.destinationBounds.Right; - for (int y = rows.Min; y < rows.Max; y++) + // Y coordinates of source points + Span sourceRow = this.source.GetPixelRowSpan((int)(((y - destY) * this.heightFactor) + sourceY)); + Span targetRow = this.destination.GetPixelRowSpan(y); + + for (int x = destLeft; x < destRight; x++) { - // Y coordinates of source points - Span sourceRow = this.source.GetPixelRowSpan((int)(((y - destY) * this.heightFactor) + sourceY)); - Span targetRow = this.destination.GetPixelRowSpan(y); - - for (int x = destLeft; x < destRight; x++) - { - // X coordinates of source points - targetRow[x] = sourceRow[(int)(((x - destX) * this.widthFactor) + sourceX)]; - } + // X coordinates of source points + targetRow[x] = sourceRow[(int)(((x - destX) * this.widthFactor) + sourceX)]; } } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs index 5e91f98eb..70c85ef02 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs @@ -21,12 +21,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs private SDImage bmpDrawing; private Image bmpCore; + // Try to get as close to System.Drawing's output as possible + private readonly GifEncoder encoder = new GifEncoder + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) + }; + + [Params(TestImages.Bmp.Car, TestImages.Png.Rgb48Bpp)] + public string TestImage { get; set; } + [GlobalSetup] public void ReadImages() { if (this.bmpStream == null) { - this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Bmp.Car)); + this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage)); this.bmpCore = Image.Load(this.bmpStream); this.bmpStream.Position = 0; this.bmpDrawing = SDImage.FromStream(this.bmpStream); @@ -53,15 +62,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs [Benchmark(Description = "ImageSharp Gif")] public void GifCore() { - // Try to get as close to System.Drawing's output as possible - var options = new GifEncoder - { - Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) - }; - using (var memoryStream = new MemoryStream()) { - this.bmpCore.SaveAsGif(memoryStream, options); + this.bmpCore.SaveAsGif(memoryStream, this.encoder); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 103c9d077..25cf5dd37 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -27,11 +27,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg private const float ProgressiveTolerance = 0.2F / 100; - static JpegDecoderTests() - { - TestEnvironment.PrepareRemoteExecutor(); - } - private static ImageComparer GetImageComparer(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ProfileResolverTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ProfileResolverTests.cs index c908abc50..fb09065b0 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ProfileResolverTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ProfileResolverTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System.Text; @@ -19,25 +19,25 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Fact] public void ProfileResolverHasCorrectJFifMarker() { - Assert.Equal(JFifMarker, ProfileResolver.JFifMarker); + Assert.Equal(JFifMarker, ProfileResolver.JFifMarker.ToArray()); } [Fact] public void ProfileResolverHasCorrectExifMarker() { - Assert.Equal(ExifMarker, ProfileResolver.ExifMarker); + Assert.Equal(ExifMarker, ProfileResolver.ExifMarker.ToArray()); } [Fact] public void ProfileResolverHasCorrectIccMarker() { - Assert.Equal(IccMarker, ProfileResolver.IccMarker); + Assert.Equal(IccMarker, ProfileResolver.IccMarker.ToArray()); } [Fact] public void ProfileResolverHasCorrectAdobeMarker() { - Assert.Equal(AdobeMarker, ProfileResolver.AdobeMarker); + Assert.Equal(AdobeMarker, ProfileResolver.AdobeMarker.ToArray()); } [Fact] @@ -76,4 +76,4 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.False(ProfileResolver.IsProfile(AdobeMarker, ProfileResolver.IccMarker)); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs index 332a141e9..08d64a738 100644 --- a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs @@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers var operation = new TestRowIntervalOperation(RowAction); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( rectangle, in parallelSettings, in operation); @@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers var operation = new TestRowIntervalOperation(RowAction); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( rectangle, in parallelSettings, in operation); @@ -157,7 +157,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers var operation = new TestRowIntervalOperation(RowAction); - ParallelRowIterator.IterateRows, Vector4>( + ParallelRowIterator.IterateRowIntervals, Vector4>( rectangle, in parallelSettings, in operation); @@ -195,7 +195,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers var operation = new TestRowIntervalOperation(RowAction); - ParallelRowIterator.IterateRows, Vector4>( + ParallelRowIterator.IterateRowIntervals, Vector4>( rectangle, in parallelSettings, in operation); @@ -249,7 +249,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers var operation = new TestRowIntervalOperation(RowAction); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( rectangle, in parallelSettings, in operation); @@ -291,7 +291,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers var operation = new TestRowIntervalOperation(RowAction); - ParallelRowIterator.IterateRows, Vector4>( + ParallelRowIterator.IterateRowIntervals, Vector4>( rectangle, in parallelSettings, in operation); @@ -355,7 +355,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers var operation = new TestRowIntervalOperation(RowAction); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( rect, settings, in operation); @@ -383,7 +383,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers var operation = new TestRowIntervalOperation(RowAction); ArgumentOutOfRangeException ex = Assert.Throws( - () => ParallelRowIterator.IterateRows(rect, in parallelSettings, in operation)); + () => ParallelRowIterator.IterateRowIntervals(rect, in parallelSettings, in operation)); Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); } @@ -406,7 +406,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers var operation = new TestRowIntervalOperation(RowAction); ArgumentOutOfRangeException ex = Assert.Throws( - () => ParallelRowIterator.IterateRows, Rgba32>(rect, in parallelSettings, in operation)); + () => ParallelRowIterator.IterateRowIntervals, Rgba32>(rect, in parallelSettings, in operation)); Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); } diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs index 0b2274581..9ea6a305c 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs @@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp.Tests this.Collection.AddFrame((ImageFrame)null); }); - Assert.StartsWith("Value cannot be null.", ex.Message); + Assert.StartsWith("Parameter \"frame\" must be not null.", ex.Message); } [Fact] @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Tests this.Collection.AddFrame(data); }); - Assert.StartsWith("Value cannot be null.", ex.Message); + Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message); } [Fact] @@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Tests this.Collection.AddFrame(new Rgba32[0]); }); - Assert.StartsWith("Value 0 must be greater than or equal to 100.", ex.Message); + Assert.StartsWith($"Parameter \"data\" ({typeof(int)}) must be greater than or equal to {100}, was {0}", ex.Message); } [Fact] @@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Tests this.Collection.InsertFrame(1, null); }); - Assert.StartsWith("Value cannot be null.", ex.Message); + Assert.StartsWith("Parameter \"frame\" must be not null.", ex.Message); } [Fact] diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs index b0008d394..08e6f8e1f 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs @@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Tests this.Collection.AddFrame(null); }); - Assert.StartsWith("Value cannot be null.", ex.Message); + Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message); } [Fact] @@ -114,7 +114,7 @@ namespace SixLabors.ImageSharp.Tests this.Collection.InsertFrame(1, null); }); - Assert.StartsWith("Value cannot be null.", ex.Message); + Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message); } [Fact] diff --git a/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs index 1e079fcf5..8db79fca0 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs @@ -28,11 +28,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators /// private static MemoryAllocatorFixture StaticFixture { get; } = new MemoryAllocatorFixture(); - static ArrayPoolMemoryAllocatorTests() - { - TestEnvironment.PrepareRemoteExecutor(); - } - public class BufferTests : BufferTestSuite { public BufferTests() diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 79bf76545..7069b0346 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -394,7 +394,7 @@ namespace SixLabors.ImageSharp.Tests public void ProfileToByteArray() { // Arrange - byte[] exifBytesWithoutExifCode = ExifConstants.LittleEndianByteOrderMarker; + byte[] exifBytesWithoutExifCode = ExifConstants.LittleEndianByteOrderMarker.ToArray(); ExifProfile expectedProfile = CreateExifProfile(); var expectedProfileTags = expectedProfile.Values.Select(x => x.Tag).ToList(); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs index ebbecab93..9dc135016 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs @@ -19,11 +19,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution { public class BokehBlurTest { - static BokehBlurTest() - { - TestEnvironment.PrepareRemoteExecutor(); - } - private static readonly string Components10x2 = @" [[ 0.00451261+0.0165137j 0.02161237-0.00299122j 0.00387479-0.02682816j -0.02752798-0.01788438j -0.03553877+0.0154543j -0.01428268+0.04224722j diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index cd93ab0cf..7e4eced8f 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -71,10 +71,10 @@ namespace SixLabors.ImageSharp.Tests foreach (ImageFrame frame in image.Frames) { using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) - using (QuantizedFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + using (IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) { int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelSpan()[0]); + Assert.Equal(index, quantized.GetPixelBufferSpan()[0]); } } } @@ -101,27 +101,27 @@ namespace SixLabors.ImageSharp.Tests foreach (ImageFrame frame in image.Frames) { using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) - using (QuantizedFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + using (IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) { int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelSpan()[0]); + Assert.Equal(index, quantized.GetPixelBufferSpan()[0]); } } } } - private int GetTransparentIndex(QuantizedFrame quantized) + private int GetTransparentIndex(IndexedImageFrame quantized) where TPixel : unmanaged, IPixel { // Transparent pixels are much more likely to be found at the end of a palette int index = -1; - Rgba32 trans = default; ReadOnlySpan paletteSpan = quantized.Palette.Span; - for (int i = paletteSpan.Length - 1; i >= 0; i--) - { - paletteSpan[i].ToRgba32(ref trans); + Span colorSpan = stackalloc Rgba32[QuantizerConstants.MaxColors].Slice(0, paletteSpan.Length); - if (trans.Equals(default)) + PixelOperations.Instance.ToRgba32(quantized.Configuration, paletteSpan, colorSpan); + for (int i = colorSpan.Length - 1; i >= 0; i--) + { + if (colorSpan[i].Equals(default)) { index = i; } diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index f3bcd0b95..2a0a02d94 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -21,13 +21,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization ImageFrame frame = image.Frames.RootFrame; using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); Assert.Equal(1, result.Palette.Length); - Assert.Equal(1, result.GetPixelSpan().Length); + Assert.Equal(1, result.GetPixelBufferSpan().Length); Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelSpan()[0]); + Assert.Equal(0, result.GetPixelBufferSpan()[0]); } [Fact] @@ -40,13 +40,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization ImageFrame frame = image.Frames.RootFrame; using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); Assert.Equal(1, result.Palette.Length); - Assert.Equal(1, result.GetPixelSpan().Length); + Assert.Equal(1, result.GetPixelBufferSpan().Length); Assert.Equal(default, result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelSpan()[0]); + Assert.Equal(0, result.GetPixelBufferSpan()[0]); } [Fact] @@ -85,19 +85,19 @@ namespace SixLabors.ImageSharp.Tests.Quantization ImageFrame frame = image.Frames.RootFrame; using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); Assert.Equal(256, result.Palette.Length); - Assert.Equal(256, result.GetPixelSpan().Length); + Assert.Equal(256, result.GetPixelBufferSpan().Length); var actualImage = new Image(1, 256); ReadOnlySpan paletteSpan = result.Palette.Span; - int paletteCount = result.Palette.Length - 1; + int paletteCount = paletteSpan.Length - 1; for (int y = 0; y < actualImage.Height; y++) { Span row = actualImage.GetPixelRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); + ReadOnlySpan quantizedPixelSpan = result.GetPixelBufferSpan(); int yy = y * actualImage.Width; for (int x = 0; x < actualImage.Width; x++) @@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization ImageFrame frame = image.Frames.RootFrame; using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); - using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + using IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); Assert.Equal(48, result.Palette.Length); } @@ -152,17 +152,17 @@ namespace SixLabors.ImageSharp.Tests.Quantization ImageFrame frame = image.Frames.RootFrame; using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) - using (QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + using (IndexedImageFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) { Assert.Equal(4 * 8, result.Palette.Length); - Assert.Equal(256, result.GetPixelSpan().Length); + Assert.Equal(256, result.GetPixelBufferSpan().Length); ReadOnlySpan paletteSpan = result.Palette.Span; - int paletteCount = result.Palette.Length - 1; + int paletteCount = paletteSpan.Length - 1; for (int y = 0; y < actualImage.Height; y++) { Span row = actualImage.GetPixelRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); + ReadOnlySpan quantizedPixelSpan = result.GetPixelBufferSpan(); int yy = y * actualImage.Width; for (int x = 0; x < actualImage.Width; x++) diff --git a/tests/ImageSharp.Tests/TestUtilities/Attributes/ImageDataAttributeBase.cs b/tests/ImageSharp.Tests/TestUtilities/Attributes/ImageDataAttributeBase.cs index 976bb9a96..85b178c73 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Attributes/ImageDataAttributeBase.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Attributes/ImageDataAttributeBase.cs @@ -18,6 +18,13 @@ namespace SixLabors.ImageSharp.Tests protected readonly PixelTypes PixelTypes; + static ImageDataAttributeBase() + { + // ImageDataAttributes are used in almost all tests, thus a good place to enforce the execution of + // TestEnvironment static constructor before anything else is done. + TestEnvironment.EnsureSharedInitializersDone(); + } + /// /// Initializes a new instance of the class. /// diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs index ea880cb38..4152d3bc6 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs @@ -34,6 +34,11 @@ namespace SixLabors.ImageSharp.Tests private static readonly Lazy NetCoreVersionLazy = new Lazy(GetNetCoreVersion); + static TestEnvironment() + { + PrepareRemoteExecutor(); + } + /// /// Gets the .NET Core version, if running on .NET Core, otherwise returns an empty string. /// @@ -111,6 +116,13 @@ namespace SixLabors.ImageSharp.Tests internal static bool IsFramework => string.IsNullOrEmpty(NetCoreVersion); + /// + /// A dummy operation to enforce the execution of the static constructor. + /// + internal static void EnsureSharedInitializersDone() + { + } + /// /// Creates the image output directory. /// @@ -141,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests /// When running in 32 bits, enforces 32 bit execution of Microsoft.DotNet.RemoteExecutor.exe /// with the help of CorFlags.exe found in Windows SDK. /// - internal static void PrepareRemoteExecutor() + private static void PrepareRemoteExecutor() { if (!IsFramework) { @@ -153,12 +165,11 @@ namespace SixLabors.ImageSharp.Tests if (File.Exists(remoteExecutorConfigPath)) { - // already prepared + // Already initialized return; } string testProjectConfigPath = TestAssemblyFile.FullName + ".config"; - File.Copy(testProjectConfigPath, remoteExecutorConfigPath); if (Is64BitProcess) @@ -184,7 +195,17 @@ namespace SixLabors.ImageSharp.Tests string remoteExecutorPath = Path.Combine(TestAssemblyFile.DirectoryName, "Microsoft.DotNet.RemoteExecutor.exe"); - string args = $"{remoteExecutorPath} /32Bit+ /Force"; + string remoteExecutorTmpPath = $"{remoteExecutorPath}._tmp"; + + if (File.Exists(remoteExecutorTmpPath)) + { + // Already initialized + return; + } + + File.Copy(remoteExecutorPath, remoteExecutorTmpPath); + + string args = $"{remoteExecutorTmpPath} /32Bit+ /Force"; var si = new ProcessStartInfo() { @@ -206,6 +227,9 @@ namespace SixLabors.ImageSharp.Tests $@"Failed to run {si.FileName} {si.Arguments}:\n STDOUT: {standardOutput}\n STDERR: {standardError}"); } + File.Delete(remoteExecutorPath); + File.Copy(remoteExecutorTmpPath, remoteExecutorPath); + static FileInfo Find(DirectoryInfo root, string name) { FileInfo fi = root.EnumerateFiles(name).FirstOrDefault(); diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 0b78138ac..8ecb2ee6c 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -713,7 +713,7 @@ namespace SixLabors.ImageSharp.Tests var operation = new RowOperation(configuration, sourceRectangle, source); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRowIntervals( configuration, sourceRectangle, in operation); diff --git a/tests/Images/External b/tests/Images/External index f8a76fd3a..1fea1ceab 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit f8a76fd3a900b90c98df67ac896574383a4d09f3 +Subproject commit 1fea1ceab89e87cc5f11376fa46164d3d27566c0