Browse Source

Merge remote-tracking branch 'origin/master' into af/block-scale-optimization

pull/1574/head
Anton Firszov 6 years ago
parent
commit
502a8abb18
  1. 4
      .editorconfig
  2. 1
      ImageSharp.sln
  3. 2
      shared-infrastructure
  4. 2
      src/Directory.Build.props
  5. 20
      src/Directory.Build.targets
  6. 2
      src/ImageSharp/Advanced/IRowIntervalOperation.cs
  7. 2
      src/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs
  8. 17
      src/ImageSharp/Advanced/IRowOperation.cs
  9. 22
      src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
  10. 138
      src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs
  11. 154
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  12. 4
      src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs
  13. 4
      src/ImageSharp/Common/Extensions/StreamExtensions.cs
  14. 2
      src/ImageSharp/Common/Helpers/Guard.cs
  15. 6
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  16. 31
      src/ImageSharp/Formats/Gif/GifConstants.cs
  17. 3
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  18. 112
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  19. 2
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  20. 2
      src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs
  21. 35
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
  22. 16
      src/ImageSharp/Formats/Jpeg/Components/ZigZag.cs
  23. 31
      src/ImageSharp/Formats/Png/PngConstants.cs
  24. 90
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  25. 4
      src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
  26. 43
      src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs
  27. 2
      src/ImageSharp/ImageFrame{TPixel}.cs
  28. 3
      src/ImageSharp/ImageSharp.csproj
  29. 2
      src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs
  30. 4
      src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
  31. 10
      src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs
  32. 2
      src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
  33. 9
      src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs
  34. 6
      src/ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs
  35. 30
      src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs
  36. 108
      src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
  37. 78
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs
  38. 79
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
  39. 75
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
  40. 29
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs
  41. 35
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  42. 21
      src/ImageSharp/Processing/Processors/Dithering/IDither.cs
  43. 38
      src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs
  44. 2
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs
  45. 107
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  46. 81
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  47. 17
      src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
  48. 2
      src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs
  49. 23
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs
  50. 21
      src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs
  51. 4
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
  52. 46
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs
  53. 33
      src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs
  54. 35
      src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs
  55. 37
      src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs
  56. 93
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  57. 87
      src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs
  58. 32
      src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
  59. 30
      src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs
  60. 116
      src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs
  61. 124
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  62. 4
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
  63. 26
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  64. 23
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  65. 10
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  66. 91
      src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs
  67. 4
      src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs
  68. 4
      src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs
  69. 144
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  70. 4
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs
  71. 19
      src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs
  72. 79
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  73. 15
      src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs
  74. 79
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  75. 44
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs
  76. 25
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs
  77. 19
      tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs
  78. 5
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  79. 12
      tests/ImageSharp.Tests/Formats/Jpg/ProfileResolverTests.cs
  80. 18
      tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
  81. 8
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs
  82. 4
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
  83. 5
      tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs
  84. 2
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs
  85. 5
      tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs
  86. 20
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  87. 30
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs
  88. 7
      tests/ImageSharp.Tests/TestUtilities/Attributes/ImageDataAttributeBase.cs
  89. 32
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs
  90. 2
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  91. 2
      tests/Images/External

4
.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_expression_statement_preference = discard_variable:silent
csharp_style_unused_value_assignment_preference = discard_variable:suggestion 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_when_type_is_apparent = true:warning
csharp_style_var_elsewhere = false:warning csharp_style_var_elsewhere = false:warning
csharp_prefer_simple_using_statement = false:silent

1
ImageSharp.sln

@ -334,6 +334,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Tests.ProfilingS
EndProject EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution 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 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13
EndGlobalSection EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 36b2d55f5bb0d91024955bd26ba220ee41cc96e5 Subproject commit ea561c249ba86352fe3b69e612b8072f3652eacb

2
src/Directory.Build.props

@ -34,4 +34,6 @@
<InternalsVisibleTo Include="SixLabors.ImageSharp.Tests" PublicKey="$(SixLaborsPublicKey)" /> <InternalsVisibleTo Include="SixLabors.ImageSharp.Tests" PublicKey="$(SixLaborsPublicKey)" />
</ItemGroup> </ItemGroup>
</Project> </Project>

20
src/Directory.Build.targets

@ -52,4 +52,24 @@
<!-- https://github.com/Microsoft/vstest/issues/411 --> <!-- https://github.com/Microsoft/vstest/issues/411 -->
<Target Name="VSTest" Condition="'$(IsTestProject)' == 'true'"/> <Target Name="VSTest" Condition="'$(IsTestProject)' == 'true'"/>
<ItemGroup>
<!--Shared config files that have to exist at root level.-->
<ConfigFilesToCopy Include="..\..\shared-infrastructure\.editorconfig;..\..\shared-infrastructure\.gitattributes" />
</ItemGroup>
<!--Ensures our config files are up to date.-->
<Target Name="CopyFiles" BeforeTargets="Build">
<Copy SourceFiles="@(ConfigFilesToCopy)"
SkipUnchangedFiles = "true"
DestinationFolder="..\..\" />
</Target>
<!-- Allows regenerating T4-generated files at build time using MsBuild -->
<!-- Enable on Windows OS to build all T4 templates. TODO: XPlat
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TextTemplating\Microsoft.TextTemplating.targets" />
<PropertyGroup>
<TransformOnBuild>true</TransformOnBuild>
</PropertyGroup>
-->
</Project> </Project>

2
src/ImageSharp/Advanced/IRowIntervalOperation.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Advanced namespace SixLabors.ImageSharp.Advanced

2
src/ImageSharp/Advanced/IRowIntervalOperation{TBuffer}.cs

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Advanced namespace SixLabors.ImageSharp.Advanced

17
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
{
/// <summary>
/// Defines the contract for an action that operates on a row.
/// </summary>
public interface IRowOperation
{
/// <summary>
/// Invokes the method passing the row y coordinate.
/// </summary>
/// <param name="y">The row y coordinate.</param>
void Invoke(int y);
}
}

22
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
{
/// <summary>
/// Defines the contract for an action that operates on a row with a temporary buffer.
/// </summary>
/// <typeparam name="TBuffer">The type of buffer elements.</typeparam>
public interface IRowOperation<TBuffer>
where TBuffer : unmanaged
{
/// <summary>
/// Invokes the method passing the row and a buffer.
/// </summary>
/// <param name="y">The row y coordinate.</param>
/// <param name="span">The contiguous region of memory.</param>
void Invoke(int y, Span<TBuffer> span);
}
}

138
src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs

@ -17,51 +17,130 @@ namespace SixLabors.ImageSharp.Advanced
/// </content> /// </content>
public static partial class ParallelRowIterator public static partial class ParallelRowIterator
{ {
private readonly struct IterationParameters private readonly struct RowOperationWrapper<T>
where T : struct, IRowOperation
{ {
public readonly int MinY; private readonly int minY;
public readonly int MaxY; private readonly int maxY;
public readonly int StepY; private readonly int stepY;
public readonly int Width; private readonly T action;
public IterationParameters(int minY, int maxY, int stepY) [MethodImpl(InliningOptions.ShortMethod)]
: this(minY, maxY, stepY, 0) 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; int yMin = this.minY + (i * this.stepY);
this.MaxY = maxY;
this.StepY = stepY; if (yMin >= this.maxY)
this.Width = width; {
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<T, TBuffer>
where T : struct, IRowOperation<TBuffer>
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<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.width);
Span<TBuffer> span = buffer.Memory.Span;
for (int y = yMin; y < yMax; y++)
{
Unsafe.AsRef(this.action).Invoke(y, span);
}
} }
} }
private readonly struct RowIntervalOperationWrapper<T> private readonly struct RowIntervalOperationWrapper<T>
where T : struct, IRowIntervalOperation where T : struct, IRowIntervalOperation
{ {
private readonly IterationParameters info; private readonly int minY;
private readonly int maxY;
private readonly int stepY;
private readonly T operation; private readonly T operation;
[MethodImpl(InliningOptions.ShortMethod)] [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; this.operation = operation;
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int i) 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; 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); var rows = new RowInterval(yMin, yMax);
// Skip the safety copy when invoking a potentially impure method on a readonly field // 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<TBuffer> where T : struct, IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged 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 MemoryAllocator allocator;
private readonly T operation; private readonly T operation;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperationWrapper( public RowIntervalOperationWrapper(
in IterationParameters info, int minY,
int maxY,
int stepY,
int width,
MemoryAllocator allocator, MemoryAllocator allocator,
in T operation) in T operation)
{ {
this.info = info; this.minY = minY;
this.maxY = maxY;
this.stepY = stepY;
this.width = width;
this.allocator = allocator; this.allocator = allocator;
this.operation = operation; this.operation = operation;
} }
@ -91,17 +179,17 @@ namespace SixLabors.ImageSharp.Advanced
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int i) 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; 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); var rows = new RowInterval(yMin, yMax);
using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.info.Width); using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.width);
Unsafe.AsRef(in this.operation).Invoke(in rows, buffer.Memory.Span); Unsafe.AsRef(in this.operation).Invoke(in rows, buffer.Memory.Span);
} }

154
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -17,6 +17,135 @@ namespace SixLabors.ImageSharp.Advanced
/// </summary> /// </summary>
public static partial class ParallelRowIterator public static partial class ParallelRowIterator
{ {
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches.
/// </summary>
/// <typeparam name="T">The type of row operation to perform.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/> to get the parallel settings from.</param>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single row.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void IterateRows<T>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowOperation
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows(rectangle, in parallelSettings, in operation);
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches.
/// </summary>
/// <typeparam name="T">The type of row operation to perform.</typeparam>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="parallelSettings">The <see cref="ParallelExecutionSettings"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single row.</param>
public static void IterateRows<T>(
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<T>(top, bottom, verticalStep, in operation);
Parallel.For(
0,
numOfSteps,
parallelOptions,
wrappingOperation.Invoke);
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches.
/// instantiating a temporary buffer for each <paramref name="operation"/> invocation.
/// </summary>
/// <typeparam name="T">The type of row operation to perform.</typeparam>
/// <typeparam name="TBuffer">The type of buffer elements.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/> to get the parallel settings from.</param>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single row.</param>
public static void IterateRows<T, TBuffer>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowOperation<TBuffer>
where TBuffer : unmanaged
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows<T, TBuffer>(rectangle, in parallelSettings, in operation);
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches.
/// instantiating a temporary buffer for each <paramref name="operation"/> invocation.
/// </summary>
/// <typeparam name="T">The type of row operation to perform.</typeparam>
/// <typeparam name="TBuffer">The type of buffer elements.</typeparam>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="parallelSettings">The <see cref="ParallelExecutionSettings"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single row.</param>
public static void IterateRows<T, TBuffer>(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
in T operation)
where T : struct, IRowOperation<TBuffer>
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<TBuffer> buffer = allocator.Allocate<TBuffer>(width);
Span<TBuffer> 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<T, TBuffer>(top, bottom, verticalStep, width, allocator, in operation);
Parallel.For(
0,
numOfSteps,
parallelOptions,
wrappingOperation.Invoke);
}
/// <summary> /// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s. /// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary> /// </summary>
@ -25,11 +154,11 @@ namespace SixLabors.ImageSharp.Advanced
/// <param name="rectangle">The <see cref="Rectangle"/>.</param> /// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param> /// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public static void IterateRows<T>(Configuration configuration, Rectangle rectangle, in T operation) public static void IterateRowIntervals<T>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowIntervalOperation where T : struct, IRowIntervalOperation
{ {
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows(rectangle, in parallelSettings, in operation); IterateRowIntervals(rectangle, in parallelSettings, in operation);
} }
/// <summary> /// <summary>
@ -39,7 +168,7 @@ namespace SixLabors.ImageSharp.Advanced
/// <param name="rectangle">The <see cref="Rectangle"/>.</param> /// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="parallelSettings">The <see cref="ParallelExecutionSettings"/>.</param> /// <param name="parallelSettings">The <see cref="ParallelExecutionSettings"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param> /// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param>
public static void IterateRows<T>( public static void IterateRowIntervals<T>(
Rectangle rectangle, Rectangle rectangle,
in ParallelExecutionSettings parallelSettings, in ParallelExecutionSettings parallelSettings,
in T operation) in T operation)
@ -65,8 +194,7 @@ namespace SixLabors.ImageSharp.Advanced
int verticalStep = DivideCeil(rectangle.Height, numOfSteps); int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var info = new IterationParameters(top, bottom, verticalStep); var wrappingOperation = new RowIntervalOperationWrapper<T>(top, bottom, verticalStep, in operation);
var wrappingOperation = new RowIntervalOperationWrapper<T>(in info, in operation);
Parallel.For( Parallel.For(
0, 0,
@ -84,12 +212,12 @@ namespace SixLabors.ImageSharp.Advanced
/// <param name="configuration">The <see cref="Configuration"/> to get the parallel settings from.</param> /// <param name="configuration">The <see cref="Configuration"/> to get the parallel settings from.</param>
/// <param name="rectangle">The <see cref="Rectangle"/>.</param> /// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param> /// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param>
public static void IterateRows<T, TBuffer>(Configuration configuration, Rectangle rectangle, in T operation) public static void IterateRowIntervals<T, TBuffer>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowIntervalOperation<TBuffer> where T : struct, IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged where TBuffer : unmanaged
{ {
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows<T, TBuffer>(rectangle, in parallelSettings, in operation); IterateRowIntervals<T, TBuffer>(rectangle, in parallelSettings, in operation);
} }
/// <summary> /// <summary>
@ -101,7 +229,7 @@ namespace SixLabors.ImageSharp.Advanced
/// <param name="rectangle">The <see cref="Rectangle"/>.</param> /// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="parallelSettings">The <see cref="ParallelExecutionSettings"/>.</param> /// <param name="parallelSettings">The <see cref="ParallelExecutionSettings"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param> /// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param>
public static void IterateRows<T, TBuffer>( public static void IterateRowIntervals<T, TBuffer>(
Rectangle rectangle, Rectangle rectangle,
in ParallelExecutionSettings parallelSettings, in ParallelExecutionSettings parallelSettings,
in T operation) in T operation)
@ -123,18 +251,16 @@ namespace SixLabors.ImageSharp.Advanced
if (numOfSteps == 1) if (numOfSteps == 1)
{ {
var rows = new RowInterval(top, bottom); var rows = new RowInterval(top, bottom);
using (IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(width)) using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(width);
{
Unsafe.AsRef(operation).Invoke(in rows, buffer.Memory.Span); Unsafe.AsRef(operation).Invoke(in rows, buffer.Memory.Span);
}
return; return;
} }
int verticalStep = DivideCeil(height, numOfSteps); int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var info = new IterationParameters(top, bottom, verticalStep, width); var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(top, bottom, verticalStep, width, allocator, in operation);
var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(in info, allocator, in operation);
Parallel.For( Parallel.For(
0, 0,

4
src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs

@ -248,7 +248,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion
} }
/// <summary> /// <summary>
/// Performs the bulk conversion from <see cref="Lms"/> into <see cref="LinearRgb"/>. /// Performs the bulk conversion from <see cref="Rgb"/> into <see cref="LinearRgb"/>.
/// </summary> /// </summary>
/// <param name="source">The span to the source colors</param> /// <param name="source">The span to the source colors</param>
/// <param name="destination">The span to the destination colors</param> /// <param name="destination">The span to the destination colors</param>
@ -435,4 +435,4 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion
return this.ToLinearRgb(rgb); return this.ToLinearRgb(rgb);
} }
} }
} }

4
src/ImageSharp/Common/Extensions/StreamExtensions.cs

@ -2,9 +2,11 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
#if !SUPPORTS_SPAN_STREAM
using System.Buffers;
#endif
namespace SixLabors.ImageSharp namespace SixLabors.ImageSharp
{ {

2
src/ImageSharp/Common/Helpers/Guard.cs

@ -22,7 +22,7 @@ namespace SixLabors
{ {
if (!value.GetType().GetTypeInfo().IsValueType) if (!value.GetType().GetTypeInfo().IsValueType)
{ {
ThrowArgumentException("Type must be a struct.", parameterName); ThrowHelper.ThrowArgumentException("Type must be a struct.", parameterName);
} }
} }
} }

6
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -336,8 +336,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette) private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IFrameQuantizer<TPixel> quantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration); using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using QuantizedFrame<TPixel> quantized = quantizer.QuantizeFrame(image, image.Bounds()); using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image, image.Bounds());
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span; ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
var color = default(Rgba32); var color = default(Rgba32);
@ -360,7 +360,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
for (int y = image.Height - 1; y >= 0; y--) for (int y = image.Height - 1; y >= 0; y--)
{ {
ReadOnlySpan<byte> pixelSpan = quantized.GetRowSpan(y); ReadOnlySpan<byte> pixelSpan = quantized.GetPixelRowSpan(y);
stream.Write(pixelSpan); stream.Write(pixelSpan);
for (int i = 0; i < this.padding; i++) for (int i = 0; i < this.padding; i++)

31
src/ImageSharp/Formats/Gif/GifConstants.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@ -21,11 +22,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
public const string FileVersion = "89a"; public const string FileVersion = "89a";
/// <summary>
/// The ASCII encoded bytes used to identify the GIF file.
/// </summary>
internal static readonly byte[] MagicNumber = Encoding.ASCII.GetBytes(FileType + FileVersion);
/// <summary> /// <summary>
/// The extension block introducer <value>!</value>. /// The extension block introducer <value>!</value>.
/// </summary> /// </summary>
@ -51,11 +47,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
public const string NetscapeApplicationIdentification = "NETSCAPE2.0"; public const string NetscapeApplicationIdentification = "NETSCAPE2.0";
/// <summary>
/// The ASCII encoded application identification bytes.
/// </summary>
internal static readonly byte[] NetscapeApplicationIdentificationBytes = Encoding.ASCII.GetBytes(NetscapeApplicationIdentification);
/// <summary> /// <summary>
/// The Netscape looping application sub block size. /// The Netscape looping application sub block size.
/// </summary> /// </summary>
@ -110,5 +101,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// The collection of file extensions that equate to a Gif. /// The collection of file extensions that equate to a Gif.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "gif" }; public static readonly IEnumerable<string> FileExtensions = new[] { "gif" };
/// <summary>
/// Gets the ASCII encoded bytes used to identify the GIF file (combining <see cref="FileType"/> and <see cref="FileVersion"/>).
/// </summary>
internal static ReadOnlySpan<byte> MagicNumber => new[]
{
(byte)'G', (byte)'I', (byte)'F',
(byte)'8', (byte)'9', (byte)'a'
};
/// <summary>
/// Gets the ASCII encoded application identification bytes (representing <see cref="NetscapeApplicationIdentification"/>).
/// </summary>
internal static ReadOnlySpan<byte> NetscapeApplicationIdentificationBytes => new[]
{
(byte)'N', (byte)'E', (byte)'T',
(byte)'S', (byte)'C', (byte)'A',
(byte)'P', (byte)'E',
(byte)'2', (byte)'.', (byte)'0'
};
} }
} }

3
src/ImageSharp/Formats/Gif/GifEncoder.cs

@ -4,6 +4,7 @@
using System.IO; using System.IO;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif
@ -17,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Gets or sets the quantizer for reducing the color count. /// Gets or sets the quantizer for reducing the color count.
/// Defaults to the <see cref="OctreeQuantizer"/> /// Defaults to the <see cref="OctreeQuantizer"/>
/// </summary> /// </summary>
public IQuantizer Quantizer { get; set; } = new OctreeQuantizer(); public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree;
/// <summary> /// <summary>
/// Gets or sets the color table mode: Global or local. /// Gets or sets the color table mode: Global or local.

112
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -79,14 +79,14 @@ namespace SixLabors.ImageSharp.Formats.Gif
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette. // Quantize the image returning a palette.
QuantizedFrame<TPixel> quantized; IndexedImageFrame<TPixel> quantized;
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration)) using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{ {
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
} }
// Get the number of bits. // 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. // Write the header.
this.WriteHeader(stream); this.WriteHeader(stream);
@ -119,15 +119,20 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
// Clean up. // Clean up.
quantized?.Dispose(); quantized.Dispose();
// TODO: Write extension etc // TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer); stream.WriteByte(GifConstants.EndIntroducer);
} }
private void EncodeGlobal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream) private void EncodeGlobal<TPixel>(Image<TPixel> image, IndexedImageFrame<TPixel> quantized, int transparencyIndex, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// 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<TPixel> pixelMap = default;
bool pixelMapSet = false;
for (int i = 0; i < image.Frames.Count; i++) for (int i = 0; i < image.Frames.Count; i++)
{ {
ImageFrame<TPixel> frame = image.Frames[i]; ImageFrame<TPixel> frame = image.Frames[i];
@ -142,22 +147,27 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
else else
{ {
using (var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette)) if (!pixelMapSet)
using (QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{ {
this.WriteImageData(paletteQuantized, stream); pixelMapSet = true;
pixelMap = new EuclideanPixelMap<TPixel>(this.configuration, quantized.Palette);
} }
using var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, pixelMap);
using IndexedImageFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.WriteImageData(paletteQuantized, stream);
} }
} }
} }
private void EncodeLocal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, Stream stream) private void EncodeLocal<TPixel>(Image<TPixel> image, IndexedImageFrame<TPixel> quantized, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageFrame<TPixel> previousFrame = null; ImageFrame<TPixel> previousFrame = null;
GifFrameMetadata previousMeta = null; GifFrameMetadata previousMeta = null;
foreach (ImageFrame<TPixel> frame in image.Frames) for (int i = 0; i < image.Frames.Count; i++)
{ {
ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata; ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata(); GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
if (quantized is null) if (quantized is null)
@ -173,27 +183,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
MaxColors = frameMetadata.ColorTableLength MaxColors = frameMetadata.ColorTableLength
}; };
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options)) using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options);
{ quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
} }
else else
{ {
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration)) using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
{ quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
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.WriteGraphicalControlExtension(frameMetadata, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream); this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream); this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream); this.WriteImageData(quantized, stream);
quantized?.Dispose(); quantized.Dispose();
quantized = null; // So next frame can regenerate it quantized = null; // So next frame can regenerate it
previousFrame = frame; previousFrame = frame;
previousMeta = frameMetadata; previousMeta = frameMetadata;
@ -208,25 +214,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <returns> /// <returns>
/// The <see cref="int"/>. /// The <see cref="int"/>.
/// </returns> /// </returns>
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized) private int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// 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 index = -1;
int length = quantized.Palette.Length; ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
using (IMemoryOwner<Rgba32> rgbaBuffer = this.memoryAllocator.Allocate<Rgba32>(length)) using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteSpan.Length);
{ Span<Rgba32> rgbaSpan = rgbaOwner.GetSpan();
Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan(); PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan); ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette.Span, 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
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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);
/// <summary> /// <summary>
/// Writes the logical screen descriptor to the stream. /// Writes the logical screen descriptor to the stream.
@ -326,8 +330,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
return; 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[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.CommentLabel; this.buffer[1] = GifConstants.CommentLabel;
stream.Write(this.buffer, 0, 2); 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. // Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan(); ReadOnlySpan<char> commentSpan = comment.AsSpan();
int idx = 0; 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); WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength);
} }
@ -391,7 +398,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
/// <param name="extension">The extension to write to the stream.</param> /// <param name="extension">The extension to write to the stream.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
public void WriteExtension(IGifExtension extension, Stream stream) private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension
{ {
this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = extension.Label; this.buffer[1] = extension.Label;
@ -437,37 +445,33 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param> /// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteColorTable<TPixel>(QuantizedFrame<TPixel> image, Stream stream) private void WriteColorTable<TPixel>(IndexedImageFrame<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// The maximum number of colors for the bit depth // The maximum number of colors for the bit depth
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3; int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf<Rgb24>();
int pixelCount = image.Palette.Length;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength, AllocationOptions.Clean);
{ PixelOperations<TPixel>.Instance.ToRgb24Bytes(
PixelOperations<TPixel>.Instance.ToRgb24Bytes( this.configuration,
this.configuration, image.Palette.Span,
image.Palette.Span, colorTable.GetSpan(),
colorTable.GetSpan(), image.Palette.Length);
pixelCount);
stream.Write(colorTable.Array, 0, colorTableLength); stream.Write(colorTable.Array, 0, colorTableLength);
}
} }
/// <summary> /// <summary>
/// Writes the image pixel data to the stream. /// Writes the image pixel data to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="QuantizedFrame{TPixel}"/> containing indexed pixels.</param> /// <param name="image">The <see cref="IndexedImageFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteImageData<TPixel>(QuantizedFrame<TPixel> image, Stream stream) private void WriteImageData<TPixel>(IndexedImageFrame<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth)) using var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth);
{ encoder.Encode(image.GetPixelBufferSpan(), stream);
encoder.Encode(image.GetPixelSpan(), stream);
}
} }
} }
} }

2
src/ImageSharp/Formats/Gif/LzwEncoder.cs

@ -274,7 +274,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
ent = this.NextPixel(indexedPixels); 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; hshift = 0;
for (fcode = this.hsize; fcode < 65536; fcode *= 2) for (fcode = this.hsize; fcode < 65536; fcode *= 2)
{ {

2
src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs

@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
buffer[0] = GifConstants.ApplicationBlockSize; buffer[0] = GifConstants.ApplicationBlockSize;
// Write NETSCAPE2.0 // Write NETSCAPE2.0
GifConstants.NetscapeApplicationIdentificationBytes.AsSpan().CopyTo(buffer.Slice(1, 11)); GifConstants.NetscapeApplicationIdentificationBytes.CopyTo(buffer.Slice(1, 11));
// Application Data ---- // Application Data ----
buffer[12] = 3; // Application block length (always 3) buffer[12] = 3; // Application block length (always 3)

35
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. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Text;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{ {
@ -12,24 +11,38 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
internal static class ProfileResolver internal static class ProfileResolver
{ {
/// <summary> /// <summary>
/// Describes the JFIF specific markers. /// Gets the JFIF specific markers.
/// </summary> /// </summary>
public static readonly byte[] JFifMarker = Encoding.ASCII.GetBytes("JFIF\0"); public static ReadOnlySpan<byte> JFifMarker => new[]
{
(byte)'J', (byte)'F', (byte)'I', (byte)'F', (byte)'\0'
};
/// <summary> /// <summary>
/// Describes the ICC specific markers. /// Gets the ICC specific markers.
/// </summary> /// </summary>
public static readonly byte[] IccMarker = Encoding.ASCII.GetBytes("ICC_PROFILE\0"); public static ReadOnlySpan<byte> 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'
};
/// <summary> /// <summary>
/// Describes the EXIF specific markers. /// Gets the EXIF specific markers.
/// </summary> /// </summary>
public static readonly byte[] ExifMarker = Encoding.ASCII.GetBytes("Exif\0\0"); public static ReadOnlySpan<byte> ExifMarker => new[]
{
(byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0'
};
/// <summary> /// <summary>
/// Describes Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>. /// Gets the Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>.
/// </summary> /// </summary>
public static readonly byte[] AdobeMarker = Encoding.ASCII.GetBytes("Adobe"); public static ReadOnlySpan<byte> AdobeMarker => new[]
{
(byte)'A', (byte)'d', (byte)'o', (byte)'b', (byte)'e'
};
/// <summary> /// <summary>
/// Returns a value indicating whether the passed bytes are a match to the profile identifier. /// 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); && bytesToCheck.Slice(0, profileIdentifier.Length).SequenceEqual(profileIdentifier);
} }
} }
} }

16
src/ImageSharp/Formats/Jpeg/Components/ZigZag.cs

@ -34,12 +34,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
public fixed byte Data[Size]; public fixed byte Data[Size];
/// <summary> /// <summary>
/// Unzig maps from the zigzag ordering to the natural ordering. For example, /// Gets the unzigs map, which maps from the zigzag ordering to the natural ordering.
/// unzig[3] is the column and row of the fourth element in zigzag order. The /// For example, unzig[3] is the column and row of the fourth element in zigzag order.
/// value is 16, which means first column (16%8 == 0) and third row (16/8 == 2). /// The value is 16, which means first column (16%8 == 0) and third row (16/8 == 2).
/// </summary> /// </summary>
private static readonly byte[] Unzig = private static ReadOnlySpan<byte> Unzig => new byte[]
new byte[Size]
{ {
0, 1, 8, 16, 9, 2, 3, 10, 0, 1, 8, 16, 9, 2, 3, 10,
17, 24, 32, 25, 18, 11, 4, 5, 17, 24, 32, 25, 18, 11, 4, 5,
@ -75,8 +74,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
public static ZigZag CreateUnzigTable() public static ZigZag CreateUnzigTable()
{ {
ZigZag result = default; ZigZag result = default;
byte* unzigPtr = result.Data; ref byte sourceRef = ref MemoryMarshal.GetReference(Unzig);
Marshal.Copy(Unzig, 0, (IntPtr)unzigPtr, Size); ref byte destinationRef = ref Unsafe.AsRef<byte>(result.Data);
Unzig.CopyTo(new Span<byte>(result.Data, Size));
return result; return result;
} }

31
src/ImageSharp/Formats/Png/PngConstants.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@ -36,21 +37,6 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "png" }; public static readonly IEnumerable<string> FileExtensions = new[] { "png" };
/// <summary>
/// The header bytes identifying a Png.
/// </summary>
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
};
/// <summary> /// <summary>
/// The header bytes as a big-endian coded ulong. /// The header bytes as a big-endian coded ulong.
/// </summary> /// </summary>
@ -77,5 +63,20 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The minimum length of a keyword in a text chunk is 1 byte. /// The minimum length of a keyword in a text chunk is 1 byte.
/// </summary> /// </summary>
public const int MinTextKeywordLength = 1; public const int MinTextKeywordLength = 1;
/// <summary>
/// Gets the header bytes identifying a Png.
/// </summary>
public static ReadOnlySpan<byte> 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
};
} }
} }

90
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -146,10 +146,10 @@ namespace SixLabors.ImageSharp.Formats.Png
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetPngMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata();
PngEncoderOptionsHelpers.AdjustOptions<TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); PngEncoderOptionsHelpers.AdjustOptions<TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
QuantizedFrame<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); IndexedImageFrame<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized); this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); stream.Write(PngConstants.HeaderBytes);
this.WriteHeaderChunk(stream); this.WriteHeaderChunk(stream);
this.WritePaletteChunk(stream, quantized); this.WritePaletteChunk(stream, quantized);
@ -371,7 +371,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="rowSpan">The row span.</param> /// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param> /// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param> /// <param name="row">The row.</param>
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, QuantizedFrame<TPixel> quantized, int row) private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
switch (this.options.ColorType) switch (this.options.ColorType)
@ -380,12 +380,11 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.bitDepth < 8) 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 else
{ {
int stride = this.currentScanline.Length(); quantized.GetPixelRowSpan(row).CopyTo(this.currentScanline.GetSpan());
quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan());
} }
break; break;
@ -440,7 +439,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="quantized">The quantized pixels. Can be null.</param> /// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param> /// <param name="row">The row.</param>
/// <returns>The <see cref="IManagedByteBuffer"/></returns> /// <returns>The <see cref="IManagedByteBuffer"/></returns>
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, QuantizedFrame<TPixel> quantized, int row) private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.CollectPixelBytes(rowSpan, quantized, row); this.CollectPixelBytes(rowSpan, quantized, row);
@ -546,59 +545,54 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="quantized">The quantized frame.</param> /// <param name="quantized">The quantized frame.</param>
private void WritePaletteChunk<TPixel>(Stream stream, QuantizedFrame<TPixel> quantized) private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel> quantized)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (quantized == null) if (quantized is null)
{ {
return; return;
} }
// Grab the palette and write it to the stream. // Grab the palette and write it to the stream.
ReadOnlySpan<TPixel> palette = quantized.Palette.Span; ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
int paletteLength = Math.Min(palette.Length, 256); int paletteLength = palette.Length;
int colorTableLength = paletteLength * 3; int colorTableLength = paletteLength * Unsafe.SizeOf<Rgb24>();
bool anyAlpha = false; bool hasAlpha = false;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength)) using IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength);
{
ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan());
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
ReadOnlySpan<byte> 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);
byte alpha = rgba.A; ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, Rgb24>(colorTable.GetSpan()));
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
Unsafe.Add(ref colorTableRef, offset) = rgba.R; // Bulk convert our palette to RGBA to allow assignment to tables.
Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G; using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteLength);
Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B; Span<Rgba32> rgbaPaletteSpan = rgbaOwner.GetSpan();
PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, quantized.Palette.Span, rgbaPaletteSpan);
ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan);
if (alpha > this.options.Threshold) // Loop, assign, and extract alpha values from the palette.
{ for (int i = 0; i < paletteLength; i++)
alpha = byte.MaxValue; {
} Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, i);
byte alpha = rgba.A;
anyAlpha = anyAlpha || alpha < byte.MaxValue; Unsafe.Add(ref colorTableRef, i) = rgba.Rgb;
Unsafe.Add(ref alphaTableRef, i) = alpha; 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 this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength);
if (anyAlpha)
{ // Write the transparency data
this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength); if (hasAlpha)
} {
this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength);
} }
} }
@ -783,7 +777,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The image.</param> /// <param name="pixels">The image.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param> /// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, QuantizedFrame<TPixel> quantized, Stream stream) private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
byte[] buffer; byte[] buffer;
@ -881,7 +875,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The pixels.</param> /// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param> /// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</param> /// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, QuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream) private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesPerScanline = this.CalculateScanlineLength(this.width); int bytesPerScanline = this.CalculateScanlineLength(this.width);
@ -960,7 +954,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="quantized">The quantized.</param> /// <param name="quantized">The quantized.</param>
/// <param name="deflateStream">The deflate stream.</param> /// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7IndexedPixels<TPixel>(QuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream) private void EncodeAdam7IndexedPixels<TPixel>(IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = quantized.Width; int width = quantized.Width;
@ -987,7 +981,7 @@ namespace SixLabors.ImageSharp.Formats.Png
row += Adam7.RowIncrement[pass]) row += Adam7.RowIncrement[pass])
{ {
// collect data // collect data
ReadOnlySpan<byte> srcRow = quantized.GetRowSpan(row); ReadOnlySpan<byte> srcRow = quantized.GetPixelRowSpan(row);
for (int col = startCol, i = 0; for (int col = startCol, i = 0;
col < width; col < width;
col += Adam7.ColumnIncrement[pass]) col += Adam7.ColumnIncrement[pass])

4
src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs

@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
/// <param name="image">The image.</param> /// <param name="image">The image.</param>
public static QuantizedFrame<TPixel> CreateQuantizedFrame<TPixel>( public static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
PngEncoderOptions options, PngEncoderOptions options,
Image<TPixel> image) Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Png
public static byte CalculateBitDepth<TPixel>( public static byte CalculateBitDepth<TPixel>(
PngEncoderOptions options, PngEncoderOptions options,
Image<TPixel> image, Image<TPixel> image,
QuantizedFrame<TPixel> quantizedFrame) IndexedImageFrame<TPixel> quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
byte bitDepth; byte bitDepth;

43
src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs

@ -36,12 +36,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
private const int EofSymbol = 256; 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 short[] StaticLCodes;
private static readonly byte[] StaticLLength; private static readonly byte[] StaticLLength;
private static readonly short[] StaticDCodes; private static readonly short[] StaticDCodes;
@ -128,6 +122,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer;
} }
/// <summary>
/// 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.
/// </summary>
private static ReadOnlySpan<byte> 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<byte> Bit4Reverse => new byte[] { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 };
/// <summary> /// <summary>
/// Gets the pending buffer to use. /// Gets the pending buffer to use.
/// </summary> /// </summary>
@ -158,6 +159,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5); this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5);
this.Pending.WriteBits(this.distTree.NumCodes - 1, 5); this.Pending.WriteBits(this.distTree.NumCodes - 1, 5);
this.Pending.WriteBits(blTreeCodes - 4, 4); this.Pending.WriteBits(blTreeCodes - 4, 4);
for (int rank = 0; rank < blTreeCodes; rank++) for (int rank = 0; rank < blTreeCodes; rank++)
{ {
this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3); this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3);
@ -250,6 +252,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.blTree.BuildTree(); this.blTree.BuildTree();
int blTreeCodes = 4; int blTreeCodes = 4;
for (int i = 18; i > blTreeCodes; i--) for (int i = 18; i > blTreeCodes; i--)
{ {
if (this.blTree.Length[BitLengthOrder[i]] > 0) if (this.blTree.Length[BitLengthOrder[i]] > 0)
@ -363,10 +366,30 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public static short BitReverse(int toReverse) public static short BitReverse(int toReverse)
{ {
return (short)(Bit4Reverse[toReverse & 0xF] << 12 /* Use unsafe offsetting and manually validate the input index to reduce the
| Bit4Reverse[(toReverse >> 4) & 0xF] << 8 * total number of conditional branches. There are two main cases to test here:
| Bit4Reverse[(toReverse >> 8) & 0xF] << 4 * 1. In the first 3, the input value (or some combination of it) is combined
| Bit4Reverse[toReverse >> 12]); * 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>((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));
} }
/// <inheritdoc/> /// <inheritdoc/>

2
src/ImageSharp/ImageFrame{TPixel}.cs

@ -277,7 +277,7 @@ namespace SixLabors.ImageSharp
var target = new ImageFrame<TPixel2>(configuration, this.Width, this.Height, this.Metadata.DeepClone()); var target = new ImageFrame<TPixel2>(configuration, this.Width, this.Height, this.Metadata.DeepClone());
var operation = new RowIntervalOperation<TPixel2>(this, target, configuration); var operation = new RowIntervalOperation<TPixel2>(this, target, configuration);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
configuration, configuration,
this.Bounds(), this.Bounds(),
in operation); in operation);

3
src/ImageSharp/ImageSharp.csproj

@ -36,7 +36,7 @@
<PackageReference Include="System.ValueTuple" /> <PackageReference Include="System.ValueTuple" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Formats\Jpeg\Components\Block8x8F.Generated.cs"> <Compile Update="Formats\Jpeg\Components\Block8x8F.Generated.cs">
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
@ -209,5 +209,4 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems" Label="Shared" /> <Import Project="..\..\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems" Label="Shared" />
</Project> </Project>

2
src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs

@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Memory
/// <inheritdoc /> /// <inheritdoc />
public override Span<T> GetSpan() public override Span<T> GetSpan()
{ {
if (this.Data == null) if (this.Data is null)
{ {
ThrowObjectDisposedException(); ThrowObjectDisposedException();
} }

4
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. /// Gets the number of elements per contiguous sub-buffer preceding the last buffer.
/// The last buffer is allowed to be smaller. /// The last buffer is allowed to be smaller.
/// </summary> /// </summary>
public int BufferLength { get; } int BufferLength { get; }
/// <summary> /// <summary>
/// Gets the aggregate number of elements in the group. /// Gets the aggregate number of elements in the group.
/// </summary> /// </summary>
public long TotalLength { get; } long TotalLength { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the group has been invalidated. /// Gets a value indicating whether the group has been invalidated.

10
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. // Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{ {
internal static class ExifConstants internal static class ExifConstants
{ {
public static readonly byte[] LittleEndianByteOrderMarker = public static ReadOnlySpan<byte> LittleEndianByteOrderMarker => new byte[]
{ {
(byte)'I', (byte)'I',
(byte)'I', (byte)'I',
@ -13,7 +15,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
0x00, 0x00,
}; };
public static readonly byte[] BigEndianByteOrderMarker = public static ReadOnlySpan<byte> BigEndianByteOrderMarker => new byte[]
{ {
(byte)'M', (byte)'M',
(byte)'M', (byte)'M',
@ -21,4 +23,4 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
0x2A 0x2A
}; };
} }
} }

2
src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs

@ -77,7 +77,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
int i = 0; int i = 0;
// The byte order marker for little-endian, followed by the number 42 and a 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; i += ExifConstants.LittleEndianByteOrderMarker.Length;
uint ifdOffset = ((uint)i - startIndex) + 4U; uint ifdOffset = ((uint)i - startIndex) + 4U;

9
src/ImageSharp/PixelFormats/PixelBlenders/PorterDuffFunctions.Generated.cs

@ -13,7 +13,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders
/// <summary> /// <summary>
/// Returns the result of the "NormalSrc" compositing equation. /// Returns the result of the "NormalSrc" compositing equation.
/// </summary> /// </summary>
@ -419,7 +418,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders
return dest; return dest;
} }
/// <summary> /// <summary>
/// Returns the result of the "MultiplySrc" compositing equation. /// Returns the result of the "MultiplySrc" compositing equation.
/// </summary> /// </summary>
@ -825,7 +823,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders
return dest; return dest;
} }
/// <summary> /// <summary>
/// Returns the result of the "AddSrc" compositing equation. /// Returns the result of the "AddSrc" compositing equation.
/// </summary> /// </summary>
@ -1231,7 +1228,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders
return dest; return dest;
} }
/// <summary> /// <summary>
/// Returns the result of the "SubtractSrc" compositing equation. /// Returns the result of the "SubtractSrc" compositing equation.
/// </summary> /// </summary>
@ -1637,7 +1633,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders
return dest; return dest;
} }
/// <summary> /// <summary>
/// Returns the result of the "ScreenSrc" compositing equation. /// Returns the result of the "ScreenSrc" compositing equation.
/// </summary> /// </summary>
@ -2043,7 +2038,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders
return dest; return dest;
} }
/// <summary> /// <summary>
/// Returns the result of the "DarkenSrc" compositing equation. /// Returns the result of the "DarkenSrc" compositing equation.
/// </summary> /// </summary>
@ -2449,7 +2443,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders
return dest; return dest;
} }
/// <summary> /// <summary>
/// Returns the result of the "LightenSrc" compositing equation. /// Returns the result of the "LightenSrc" compositing equation.
/// </summary> /// </summary>
@ -2855,7 +2848,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders
return dest; return dest;
} }
/// <summary> /// <summary>
/// Returns the result of the "OverlaySrc" compositing equation. /// Returns the result of the "OverlaySrc" compositing equation.
/// </summary> /// </summary>
@ -3261,7 +3253,6 @@ namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders
return dest; return dest;
} }
/// <summary> /// <summary>
/// Returns the result of the "HardLightSrc" compositing equation. /// Returns the result of the "HardLightSrc" compositing equation.
/// </summary> /// </summary>

6
src/ImageSharp/Processing/Extensions/Filters/FilterExtensions.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing
public static class FilterExtensions public static class FilterExtensions
{ {
/// <summary> /// <summary>
/// Filters an image but the given color matrix /// Filters an image by the given color matrix
/// </summary> /// </summary>
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="matrix">The filter color matrix</param> /// <param name="matrix">The filter color matrix</param>
@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Processing
=> source.ApplyProcessor(new FilterProcessor(matrix)); => source.ApplyProcessor(new FilterProcessor(matrix));
/// <summary> /// <summary>
/// Filters an image but the given color matrix /// Filters an image by the given color matrix
/// </summary> /// </summary>
/// <param name="source">The image this method extends.</param> /// <param name="source">The image this method extends.</param>
/// <param name="matrix">The filter color matrix</param> /// <param name="matrix">The filter color matrix</param>
@ -32,4 +32,4 @@ namespace SixLabors.ImageSharp.Processing
public static IImageProcessingContext Filter(this IImageProcessingContext source, ColorMatrix matrix, Rectangle rectangle) public static IImageProcessingContext Filter(this IImageProcessingContext source, ColorMatrix matrix, Rectangle rectangle)
=> source.ApplyProcessor(new FilterProcessor(matrix), rectangle); => source.ApplyProcessor(new FilterProcessor(matrix), rectangle);
} }
} }

30
src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs

@ -3,8 +3,8 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Binarization namespace SixLabors.ImageSharp.Processing.Processors.Binarization
@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); var interest = Rectangle.Intersect(sourceRectangle, source.Bounds());
bool isAlphaOnly = typeof(TPixel) == typeof(A8); 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( ParallelRowIterator.IterateRows(
configuration, configuration,
interest, interest,
@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the clone logic for <see cref="BinaryThresholdProcessor{TPixel}"/>. /// A <see langword="struct"/> implementing the clone logic for <see cref="BinaryThresholdProcessor{TPixel}"/>.
/// </summary> /// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation private readonly struct RowOperation : IRowOperation
{ {
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly TPixel upper; private readonly TPixel upper;
@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
private readonly bool isAlphaOnly; private readonly bool isAlphaOnly;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
Rectangle bounds, Rectangle bounds,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
TPixel upper, TPixel upper,
@ -84,22 +84,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
Rgba32 rgba = default; Rgba32 rgba = default;
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> row = this.source.GetPixelRowSpan(y);
{ ref TPixel rowRef = ref MemoryMarshal.GetReference(row);
Span<TPixel> row = this.source.GetPixelRowSpan(y);
for (int x = this.minX; x < this.maxX; x++) for (int x = this.minX; x < this.maxX; x++)
{ {
ref TPixel color = ref row[x]; ref TPixel color = ref Unsafe.Add(ref rowRef, x);
color.ToRgba32(ref rgba); color.ToRgba32(ref rgba);
// Convert to grayscale using ITU-R Recommendation BT.709 if required // 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); byte luminance = this.isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B);
color = luminance >= this.threshold ? this.upper : this.lower; color = luminance >= this.threshold ? this.upper : this.lower;
}
} }
} }
} }

108
src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs

@ -72,8 +72,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
// Preliminary gamma highlight pass // Preliminary gamma highlight pass
var gammaOperation = new ApplyGammaExposureRowIntervalOperation(this.SourceRectangle, source.PixelBuffer, this.Configuration, this.gamma); var gammaOperation = new ApplyGammaExposureRowOperation(this.SourceRectangle, source.PixelBuffer, this.Configuration, this.gamma);
ParallelRowIterator.IterateRows<ApplyGammaExposureRowIntervalOperation, Vector4>( ParallelRowIterator.IterateRows<ApplyGammaExposureRowOperation, Vector4>(
this.Configuration, this.Configuration,
this.SourceRectangle, this.SourceRectangle,
in gammaOperation); in gammaOperation);
@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
float inverseGamma = 1 / this.gamma; float inverseGamma = 1 / this.gamma;
// Apply the inverse gamma exposure pass, and write the final pixel data // 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( ParallelRowIterator.IterateRows(
this.Configuration, this.Configuration,
this.SourceRectangle, this.SourceRectangle,
@ -120,14 +120,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
Vector4 parameters = Unsafe.Add(ref paramsRef, i); Vector4 parameters = Unsafe.Add(ref paramsRef, i);
// Compute the vertical 1D convolution // 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( ParallelRowIterator.IterateRows(
configuration, configuration,
sourceRectangle, sourceRectangle,
in verticalOperation); in verticalOperation);
// Compute the horizontal 1D convolutions and accumulate the partial results on the target buffer // 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( ParallelRowIterator.IterateRows(
configuration, configuration,
sourceRectangle, sourceRectangle,
@ -138,7 +138,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the vertical convolution logic for <see cref="BokehBlurProcessor{T}"/>. /// A <see langword="struct"/> implementing the vertical convolution logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary> /// </summary>
private readonly struct ApplyVerticalConvolutionRowIntervalOperation : IRowIntervalOperation private readonly struct ApplyVerticalConvolutionRowOperation : IRowOperation
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly Buffer2D<ComplexVector4> targetValues; private readonly Buffer2D<ComplexVector4> targetValues;
@ -148,7 +148,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly int maxX; private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public ApplyVerticalConvolutionRowIntervalOperation( public ApplyVerticalConvolutionRowOperation(
Rectangle bounds, Rectangle bounds,
Buffer2D<ComplexVector4> targetValues, Buffer2D<ComplexVector4> targetValues,
Buffer2D<TPixel> sourcePixels, Buffer2D<TPixel> sourcePixels,
@ -164,16 +164,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<ComplexVector4> targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X);
{
Span<ComplexVector4> targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X);
for (int x = 0; x < this.bounds.Width; 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); 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
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the horizontal convolution logic for <see cref="BokehBlurProcessor{T}"/>. /// A <see langword="struct"/> implementing the horizontal convolution logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary> /// </summary>
private readonly struct ApplyHorizontalConvolutionRowIntervalOperation : IRowIntervalOperation private readonly struct ApplyHorizontalConvolutionRowOperation : IRowOperation
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly Buffer2D<Vector4> targetValues; private readonly Buffer2D<Vector4> targetValues;
@ -193,7 +190,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly int maxX; private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public ApplyHorizontalConvolutionRowIntervalOperation( public ApplyHorizontalConvolutionRowOperation(
Rectangle bounds, Rectangle bounds,
Buffer2D<Vector4> targetValues, Buffer2D<Vector4> targetValues,
Buffer2D<ComplexVector4> sourceValues, Buffer2D<ComplexVector4> sourceValues,
@ -213,16 +210,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<Vector4> targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X);
{
Span<Vector4> targetRowSpan = this.targetValues.GetRowSpan(y).Slice(this.bounds.X);
for (int x = 0; x < this.bounds.Width; 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); 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
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the gamma exposure logic for <see cref="BokehBlurProcessor{T}"/>. /// A <see langword="struct"/> implementing the gamma exposure logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary> /// </summary>
private readonly struct ApplyGammaExposureRowIntervalOperation : IRowIntervalOperation<Vector4> private readonly struct ApplyGammaExposureRowOperation : IRowOperation<Vector4>
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> targetPixels; private readonly Buffer2D<TPixel> targetPixels;
@ -238,7 +232,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly float gamma; private readonly float gamma;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public ApplyGammaExposureRowIntervalOperation( public ApplyGammaExposureRowOperation(
Rectangle bounds, Rectangle bounds,
Buffer2D<TPixel> targetPixels, Buffer2D<TPixel> targetPixels,
Configuration configuration, Configuration configuration,
@ -252,31 +246,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span) public void Invoke(int y, Span<Vector4> span)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
PixelOperations<TPixel>.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<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); ref Vector4 v = ref Unsafe.Add(ref baseRef, x);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span, PixelConversionModifiers.Premultiply); v.X = MathF.Pow(v.X, this.gamma);
ref Vector4 baseRef = ref MemoryMarshal.GetReference(span); v.Y = MathF.Pow(v.Y, this.gamma);
v.Z = MathF.Pow(v.Z, this.gamma);
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<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
} }
} }
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the inverse gamma exposure logic for <see cref="BokehBlurProcessor{T}"/>. /// A <see langword="struct"/> implementing the inverse gamma exposure logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary> /// </summary>
private readonly struct ApplyInverseGammaExposureRowIntervalOperation : IRowIntervalOperation private readonly struct ApplyInverseGammaExposureRowOperation : IRowOperation
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> targetPixels; private readonly Buffer2D<TPixel> targetPixels;
@ -285,7 +276,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly float inverseGamma; private readonly float inverseGamma;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public ApplyInverseGammaExposureRowIntervalOperation( public ApplyInverseGammaExposureRowOperation(
Rectangle bounds, Rectangle bounds,
Buffer2D<TPixel> targetPixels, Buffer2D<TPixel> targetPixels,
Buffer2D<Vector4> sourceValues, Buffer2D<Vector4> sourceValues,
@ -301,28 +292,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
Vector4 low = Vector4.Zero; Vector4 low = Vector4.Zero;
var high = new Vector4(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); var high = new Vector4(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> targetPixelSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
Span<Vector4> 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<TPixel> targetPixelSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); ref Vector4 v = ref Unsafe.Add(ref sourceRef, x);
Span<Vector4> sourceRowSpan = this.sourceValues.GetRowSpan(y).Slice(this.bounds.X); var clamp = Vector4.Clamp(v, low, high);
ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); v.X = MathF.Pow(clamp.X, this.inverseGamma);
v.Y = MathF.Pow(clamp.Y, this.inverseGamma);
for (int x = 0; x < this.bounds.Width; x++) v.Z = MathF.Pow(clamp.Z, this.inverseGamma);
{
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<TPixel>.Instance.FromVector4Destructive(this.configuration, sourceRowSpan.Slice(0, this.bounds.Width), targetPixelSpan, PixelConversionModifiers.Premultiply);
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, sourceRowSpan.Slice(0, this.bounds.Width), targetPixelSpan, PixelConversionModifiers.Premultiply);
} }
} }
} }

78
src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs

@ -65,9 +65,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
source.CopyTo(targetPixels); source.CopyTo(targetPixels);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); 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<RowIntervalOperation, Vector4>( ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
in operation); in operation);
@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="Convolution2DProcessor{T}"/>. /// A <see langword="struct"/> implementing the convolution logic for <see cref="Convolution2DProcessor{T}"/>.
/// </summary> /// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4> private readonly struct RowOperation : IRowOperation<Vector4>
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly int maxY; private readonly int maxY;
@ -91,7 +91,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly bool preserveAlpha; private readonly bool preserveAlpha;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
Rectangle bounds, Rectangle bounds,
Buffer2D<TPixel> targetPixels, Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> sourcePixels, Buffer2D<TPixel> sourcePixels,
@ -113,52 +113,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span) public void Invoke(int y, Span<Vector4> span)
{ {
ref Vector4 spanRef = ref MemoryMarshal.GetReference(span); ref Vector4 spanRef = ref MemoryMarshal.GetReference(span);
Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span);
for (int y = rows.Min; y < rows.Max; y++) if (this.preserveAlpha)
{ {
Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); for (int x = 0; x < this.bounds.Width; x++)
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span);
if (this.preserveAlpha)
{ {
for (int x = 0; x < this.bounds.Width; x++) DenseMatrixUtils.Convolve2D3(
{ in this.kernelY,
DenseMatrixUtils.Convolve2D3( in this.kernelX,
in this.kernelY, this.sourcePixels,
in this.kernelX, ref spanRef,
this.sourcePixels, y,
ref spanRef, x,
y, this.bounds.Y,
x, this.maxY,
this.bounds.Y, this.bounds.X,
this.maxY, this.maxX);
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,
DenseMatrixUtils.Convolve2D4( in this.kernelX,
in this.kernelY, this.sourcePixels,
in this.kernelX, ref spanRef,
this.sourcePixels, y,
ref spanRef, x,
y, this.bounds.Y,
x, this.maxY,
this.bounds.Y, this.bounds.X,
this.maxY, this.maxX);
this.bounds.X,
this.maxX);
}
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
} }
} }
} }

79
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()); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
// Horizontal convolution // Horizontal convolution
var horizontalOperation = new RowIntervalOperation(interest, firstPassPixels, source.PixelBuffer, this.KernelX, this.Configuration, this.PreserveAlpha); var horizontalOperation = new RowOperation(interest, firstPassPixels, source.PixelBuffer, this.KernelX, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>( ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
in horizontalOperation); in horizontalOperation);
// Vertical convolution // Vertical convolution
var verticalOperation = new RowIntervalOperation(interest, source.PixelBuffer, firstPassPixels, this.KernelY, this.Configuration, this.PreserveAlpha); var verticalOperation = new RowOperation(interest, source.PixelBuffer, firstPassPixels, this.KernelY, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>( ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
in verticalOperation); in verticalOperation);
@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="Convolution2PassProcessor{T}"/>. /// A <see langword="struct"/> implementing the convolution logic for <see cref="Convolution2PassProcessor{T}"/>.
/// </summary> /// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4> private readonly struct RowOperation : IRowOperation<Vector4>
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> targetPixels; private readonly Buffer2D<TPixel> targetPixels;
@ -91,7 +91,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly bool preserveAlpha; private readonly bool preserveAlpha;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
Rectangle bounds, Rectangle bounds,
Buffer2D<TPixel> targetPixels, Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> sourcePixels, Buffer2D<TPixel> sourcePixels,
@ -109,53 +109,50 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span) public void Invoke(int y, Span<Vector4> span)
{ {
ref Vector4 spanRef = ref MemoryMarshal.GetReference(span); ref Vector4 spanRef = ref MemoryMarshal.GetReference(span);
int maxY = this.bounds.Bottom - 1; int maxY = this.bounds.Bottom - 1;
int maxX = this.bounds.Right - 1; int maxX = this.bounds.Right - 1;
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
{ PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span);
Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
PixelOperations<TPixel>.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,
DenseMatrixUtils.Convolve3( this.sourcePixels,
in this.kernel, ref spanRef,
this.sourcePixels, y,
ref spanRef, x,
y, this.bounds.Y,
x, maxY,
this.bounds.Y, this.bounds.X,
maxY, maxX);
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,
DenseMatrixUtils.Convolve4( this.sourcePixels,
in this.kernel, ref spanRef,
this.sourcePixels, y,
ref spanRef, x,
y, this.bounds.Y,
x, maxY,
this.bounds.Y, this.bounds.X,
maxY, maxX);
this.bounds.X,
maxX);
}
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
} }
} }
} }

75
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs

@ -56,8 +56,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
source.CopyTo(targetPixels); source.CopyTo(targetPixels);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
var operation = new RowIntervalOperation(interest, targetPixels, source.PixelBuffer, this.KernelXY, this.Configuration, this.PreserveAlpha); var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, this.KernelXY, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>( ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
in operation); in operation);
@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="ConvolutionProcessor{T}"/>. /// A <see langword="struct"/> implementing the convolution logic for <see cref="ConvolutionProcessor{T}"/>.
/// </summary> /// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4> private readonly struct RowOperation : IRowOperation<Vector4>
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly int maxY; private readonly int maxY;
@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly bool preserveAlpha; private readonly bool preserveAlpha;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
Rectangle bounds, Rectangle bounds,
Buffer2D<TPixel> targetPixels, Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> sourcePixels, Buffer2D<TPixel> sourcePixels,
@ -100,50 +100,47 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span) public void Invoke(int y, Span<Vector4> span)
{ {
ref Vector4 spanRef = ref MemoryMarshal.GetReference(span); ref Vector4 spanRef = ref MemoryMarshal.GetReference(span);
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
{ PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span);
Span<TPixel> targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X);
PixelOperations<TPixel>.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,
DenseMatrixUtils.Convolve3( this.sourcePixels,
in this.kernel, ref spanRef,
this.sourcePixels, y,
ref spanRef, x,
y, this.bounds.Y,
x, this.maxY,
this.bounds.Y, this.bounds.X,
this.maxY, this.maxX);
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,
DenseMatrixUtils.Convolve4( this.sourcePixels,
in this.kernel, ref spanRef,
this.sourcePixels, y,
ref spanRef, x,
y, this.bounds.Y,
x, this.maxY,
this.bounds.Y, this.bounds.X,
this.maxY, this.maxX);
this.bounds.X,
this.maxX);
}
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, targetRowSpan);
} }
} }
} }

29
src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs

@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
processor.Apply(pass); processor.Apply(pass);
} }
var operation = new RowIntervalOperation(source.PixelBuffer, pass.PixelBuffer, interest); var operation = new RowOperation(source.PixelBuffer, pass.PixelBuffer, interest);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRows(
this.Configuration, this.Configuration,
interest, interest,
@ -89,7 +89,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="EdgeDetectorCompassProcessor{T}"/>. /// A <see langword="struct"/> implementing the convolution logic for <see cref="EdgeDetectorCompassProcessor{T}"/>.
/// </summary> /// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation private readonly struct RowOperation : IRowOperation
{ {
private readonly Buffer2D<TPixel> targetPixels; private readonly Buffer2D<TPixel> targetPixels;
private readonly Buffer2D<TPixel> passPixels; private readonly Buffer2D<TPixel> passPixels;
@ -97,7 +97,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly int maxX; private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
Buffer2D<TPixel> targetPixels, Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> passPixels, Buffer2D<TPixel> passPixels,
Rectangle bounds) Rectangle bounds)
@ -110,23 +110,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [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++) for (int x = this.minX; x < this.maxX; x++)
{ {
// Grab the max components of the two pixels // Grab the max components of the two pixels
ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, x); ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, x);
ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, 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);
}
} }
} }
} }

35
src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -89,29 +90,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>( public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds) Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Span<byte> outputSpan = output.Span;
ReadOnlySpan<TPixel> paletteSpan = palette.Span;
int width = bounds.Width;
int offsetY = bounds.Top; int offsetY = bounds.Top;
int offsetX = bounds.Left; int offsetX = bounds.Left;
float scale = quantizer.Options.DitherScale; float scale = quantizer.Options.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++) for (int y = bounds.Top; y < bounds.Bottom; y++)
{ {
Span<TPixel> row = source.GetPixelRowSpan(y); ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
int rowStart = (y - offsetY) * width; ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
for (int x = bounds.Left; x < bounds.Right; x++) for (int x = bounds.Left; x < bounds.Right; x++)
{ {
TPixel sourcePixel = row[x]; TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, x);
outputSpan[rowStart + x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
} }
} }
@ -119,25 +116,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPixel>( public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
Configuration configuration, in TPaletteDitherImageProcessor processor,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, Rectangle bounds)
float scale) where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var pixelMap = new EuclideanPixelMap<TPixel>(palette); float scale = processor.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++) for (int y = bounds.Top; y < bounds.Bottom; y++)
{ {
Span<TPixel> row = source.GetPixelRowSpan(y); ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
for (int x = bounds.Left; x < bounds.Right; x++) for (int x = bounds.Left; x < bounds.Right; x++)
{ {
TPixel sourcePixel = row[x]; ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
pixelMap.GetClosestColor(sourcePixel, out TPixel transformed); TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
row[x] = transformed; sourcePixel = transformed;
} }
} }
} }

21
src/ImageSharp/Processing/Processors/Dithering/IDither.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -19,15 +18,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam> /// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The frame quantizer.</param> /// <param name="quantizer">The frame quantizer.</param>
/// <param name="palette">The quantized palette.</param>
/// <param name="source">The source image.</param> /// <param name="source">The source image.</param>
/// <param name="output">The output target</param> /// <param name="destination">The destination quantized frame.</param>
/// <param name="bounds">The region of interest bounds.</param> /// <param name="bounds">The region of interest bounds.</param>
void ApplyQuantizationDither<TFrameQuantizer, TPixel>( void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds) Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>; where TPixel : unmanaged, IPixel<TPixel>;
@ -36,18 +33,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Transforms the image frame applying a dither matrix. /// Transforms the image frame applying a dither matrix.
/// This method should be treated as destructive, altering the input pixels. /// This method should be treated as destructive, altering the input pixels.
/// </summary> /// </summary>
/// <typeparam name="TPaletteDitherImageProcessor">The type of palette dithering processor.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The configuration.</param> /// <param name="processor">The palette dithering processor.</param>
/// <param name="palette">The quantized palette.</param>
/// <param name="source">The source image.</param> /// <param name="source">The source image.</param>
/// <param name="bounds">The region of interest bounds.</param> /// <param name="bounds">The region of interest bounds.</param>
/// <param name="scale">The dithering scale used to adjust the amount of dither. Range 0..1.</param> void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
void ApplyPaletteDither<TPixel>( in TPaletteDitherImageProcessor processor,
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, Rectangle bounds)
float scale) where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>; where TPixel : unmanaged, IPixel<TPixel>;
} }
} }

38
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
{
/// <summary>
/// Implements an algorithm to alter the pixels of an image via palette dithering.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public interface IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Gets the configuration instance to use when performing operations.
/// </summary>
Configuration Configuration { get; }
/// <summary>
/// Gets the dithering palette.
/// </summary>
ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Gets the dithering scale used to adjust the amount of dither. Range 0..1.
/// </summary>
float DitherScale { get; }
/// <summary>
/// Returns the color from the dithering palette corresponding to the given color.
/// </summary>
/// <param name="color">The color to match.</param>
/// <returns>The <typeparamref name="TPixel"/> match.</returns>
TPixel GetPaletteColor(TPixel color);
}
}

2
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <content> /// <content>
/// An ordered dithering matrix with equal sides of arbitrary length /// An ordered dithering matrix with equal sides of arbitrary length
/// </content> /// </content>
public readonly partial struct OrderedDither : IDither public readonly partial struct OrderedDither
{ {
/// <summary> /// <summary>
/// Applies order dithering using the 2x2 Bayer dithering matrix. /// Applies order dithering using the 2x2 Bayer dithering matrix.

107
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs

@ -3,8 +3,8 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -105,21 +105,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>( public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds) Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var ditherOperation = new QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel>( var ditherOperation = new QuantizeDitherRowOperation<TFrameQuantizer, TPixel>(
ref quantizer, ref quantizer,
in Unsafe.AsRef(this), in Unsafe.AsRef(this),
source, source,
output, destination,
bounds, bounds);
palette,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRows(
quantizer.Configuration, quantizer.Configuration,
@ -129,24 +126,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPixel>( public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
Configuration configuration, in TPaletteDitherImageProcessor processor,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, Rectangle bounds)
float scale) where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var ditherOperation = new PaletteDitherRowIntervalOperation<TPixel>( var ditherOperation = new PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel>(
in processor,
in Unsafe.AsRef(this), in Unsafe.AsRef(this),
source, source,
bounds, bounds);
palette,
scale,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRows(
configuration, processor.Configuration,
bounds, bounds,
in ditherOperation); in ditherOperation);
} }
@ -200,102 +194,87 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public override int GetHashCode() public override int GetHashCode()
=> HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY);
private readonly struct QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation private readonly struct QuantizeDitherRowOperation<TFrameQuantizer, TPixel> : IRowOperation
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly TFrameQuantizer quantizer; private readonly TFrameQuantizer quantizer;
private readonly OrderedDither dither; private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output; private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
private readonly int bitDepth; private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public QuantizeDitherRowIntervalOperation( public QuantizeDitherRowOperation(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
in OrderedDither dither, in OrderedDither dither,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds, Rectangle bounds)
ReadOnlyMemory<TPixel> palette,
int bitDepth)
{ {
this.quantizer = quantizer; this.quantizer = quantizer;
this.dither = dither; this.dither = dither;
this.source = source; this.source = source;
this.output = output; this.destination = destination;
this.bounds = bounds; this.bounds = bounds;
this.palette = palette; this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Length);
this.bitDepth = bitDepth;
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
Span<byte> outputSpan = this.output.Span;
int width = this.bounds.Width;
int offsetY = this.bounds.Top; int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left; int offsetX = this.bounds.Left;
float scale = this.quantizer.Options.DitherScale; 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<TPixel> row = this.source.GetPixelRowSpan(y); TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale);
int rowStart = (y - offsetY) * width; Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _);
// 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 _);
}
} }
} }
} }
private readonly struct PaletteDitherRowIntervalOperation<TPixel> : IRowIntervalOperation private readonly struct PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel> : IRowOperation
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly TPaletteDitherImageProcessor processor;
private readonly OrderedDither dither; private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly EuclideanPixelMap<TPixel> pixelMap;
private readonly float scale; private readonly float scale;
private readonly int bitDepth; private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public PaletteDitherRowIntervalOperation( public PaletteDitherRowOperation(
in TPaletteDitherImageProcessor processor,
in OrderedDither dither, in OrderedDither dither,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, Rectangle bounds)
ReadOnlyMemory<TPixel> palette,
float scale,
int bitDepth)
{ {
this.processor = processor;
this.dither = dither; this.dither = dither;
this.source = source; this.source = source;
this.bounds = bounds; this.bounds = bounds;
this.pixelMap = new EuclideanPixelMap<TPixel>(palette); this.scale = processor.DitherScale;
this.scale = scale; this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length);
this.bitDepth = bitDepth;
} }
[MethodImpl(InliningOptions.ShortMethod)] [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<TPixel> row = this.source.GetPixelRowSpan(y); ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale);
for (int x = this.bounds.Left; x < this.bounds.Right; x++) sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered);
{
TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, this.scale);
this.pixelMap.GetClosestColor(dithered, out TPixel transformed);
row[x] = transformed;
}
} }
} }
} }

81
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs

@ -3,7 +3,9 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{ {
@ -14,11 +16,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
internal sealed class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel> internal sealed class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly int paletteLength; private readonly DitherProcessor ditherProcessor;
private readonly IDither dither; private readonly IDither dither;
private readonly float ditherScale; private IMemoryOwner<TPixel> paletteOwner;
private readonly ReadOnlyMemory<Color> sourcePalette;
private IMemoryOwner<TPixel> palette;
private bool isDisposed; private bool isDisposed;
/// <summary> /// <summary>
@ -31,37 +31,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle) : base(configuration, source, sourceRectangle)
{ {
this.paletteLength = definition.Palette.Span.Length;
this.dither = definition.Dither; this.dither = definition.Dither;
this.ditherScale = definition.DitherScale;
this.sourcePalette = definition.Palette;
}
/// <inheritdoc/> ReadOnlySpan<Color> sourcePalette = definition.Palette.Span;
protected override void OnFrameApply(ImageFrame<TPixel> source) this.paletteOwner = this.Configuration.MemoryAllocator.Allocate<TPixel>(sourcePalette.Length);
{ Color.ToPixel(this.Configuration, sourcePalette, this.paletteOwner.Memory.Span);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
this.dither.ApplyPaletteDither( this.ditherProcessor = new DitherProcessor(
this.Configuration, this.Configuration,
this.palette.Memory, this.paletteOwner.Memory,
source, definition.DitherScale);
interest,
this.ditherScale);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void BeforeFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
// Lazy init palettes: var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
if (this.palette is null) this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest);
{
this.palette = this.Configuration.MemoryAllocator.Allocate<TPixel>(this.paletteLength);
ReadOnlySpan<Color> sourcePalette = this.sourcePalette.Span;
Color.ToPixel(this.Configuration, sourcePalette, this.palette.Memory.Span);
}
base.BeforeFrameApply(source);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -72,15 +58,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
return; return;
} }
this.isDisposed = true;
if (disposing) if (disposing)
{ {
this.palette?.Dispose(); this.paletteOwner.Dispose();
} }
this.palette = null; this.paletteOwner = null;
this.isDisposed = true;
base.Dispose(disposing); base.Dispose(disposing);
} }
/// <summary>
/// Used to allow inlining of calls to
/// <see cref="IPaletteDitherImageProcessor{TPixel}.GetPaletteColor(TPixel)"/>.
/// </summary>
private readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel>
{
private readonly EuclideanPixelMap<TPixel> pixelMap;
[MethodImpl(InliningOptions.ShortMethod)]
public DitherProcessor(
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
float ditherScale)
{
this.Configuration = configuration;
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
this.Palette = palette;
this.DitherScale = ditherScale;
}
public Configuration Configuration { get; }
public ReadOnlyMemory<TPixel> Palette { get; }
public float DitherScale { get; }
[MethodImpl(InliningOptions.ShortMethod)]
public TPixel GetPaletteColor(TPixel color)
{
this.pixelMap.GetClosestColor(color, out TPixel match);
return match;
}
}
} }
} }

17
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."); "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( ParallelRowIterator.IterateRows(
configuration, configuration,
workingRect, workingRect,
@ -109,7 +109,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the draw logic for <see cref="DrawImageProcessor{TPixelBg,TPixelFg}"/>. /// A <see langword="struct"/> implementing the draw logic for <see cref="DrawImageProcessor{TPixelBg,TPixelFg}"/>.
/// </summary> /// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation private readonly struct RowOperation : IRowOperation
{ {
private readonly ImageFrame<TPixelBg> sourceFrame; private readonly ImageFrame<TPixelBg> sourceFrame;
private readonly Image<TPixelFg> targetImage; private readonly Image<TPixelFg> targetImage;
@ -122,7 +122,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
private readonly float opacity; private readonly float opacity;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
ImageFrame<TPixelBg> sourceFrame, ImageFrame<TPixelBg> sourceFrame,
Image<TPixelFg> targetImage, Image<TPixelFg> targetImage,
PixelBlender<TPixelBg> blender, PixelBlender<TPixelBg> blender,
@ -146,14 +146,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixelBg> background = this.sourceFrame.GetPixelRowSpan(y).Slice(this.minX, this.width);
{ Span<TPixelFg> foreground = this.targetImage.GetPixelRowSpan(y - this.locationY).Slice(this.targetX, this.width);
Span<TPixelBg> background = this.sourceFrame.GetPixelRowSpan(y).Slice(this.minX, this.width); this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity);
Span<TPixelFg> foreground = this.targetImage.GetPixelRowSpan(y - this.locationY).Slice(this.targetX, this.width);
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity);
}
} }
} }
} }

2
src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs

@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
source.CopyTo(targetPixels); source.CopyTo(targetPixels);
var operation = new RowIntervalOperation(this.SourceRectangle, targetPixels, source, this.Configuration, brushSize >> 1, this.definition.Levels); var operation = new RowIntervalOperation(this.SourceRectangle, targetPixels, source, this.Configuration, brushSize >> 1, this.definition.Levels);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
this.Configuration, this.Configuration,
this.SourceRectangle, this.SourceRectangle,
in operation); in operation);

23
src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs

@ -50,9 +50,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); 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<RowIntervalOperation, Vector4>( ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
in operation); in operation);
@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="PixelRowDelegateProcessor{TPixel,TDelegate}"/>. /// A <see langword="struct"/> implementing the convolution logic for <see cref="PixelRowDelegateProcessor{TPixel,TDelegate}"/>.
/// </summary> /// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4> private readonly struct RowOperation : IRowOperation<Vector4>
{ {
private readonly int startX; private readonly int startX;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
private readonly TDelegate rowProcessor; private readonly TDelegate rowProcessor;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
int startX, int startX,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Configuration configuration, Configuration configuration,
@ -86,18 +86,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span) public void Invoke(int y, Span<Vector4> span)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length);
{ PixelOperations<TPixel>.Instance.ToVector4(this.configuration, rowSpan, span, this.modifiers);
Span<TPixel> rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, rowSpan, span, this.modifiers);
// Run the user defined pixel shader to the current row of pixels // Run the user defined pixel shader to the current row of pixels
Unsafe.AsRef(this.rowProcessor).Invoke(span, new Point(this.startX, y)); Unsafe.AsRef(this.rowProcessor).Invoke(span, new Point(this.startX, y));
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, rowSpan, this.modifiers); PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, rowSpan, this.modifiers);
}
} }
} }
} }

21
src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs

@ -36,9 +36,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); 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<RowIntervalOperation, Vector4>( ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
in operation); in operation);
@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="FilterProcessor{TPixel}"/>. /// A <see langword="struct"/> implementing the convolution logic for <see cref="FilterProcessor{TPixel}"/>.
/// </summary> /// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4> private readonly struct RowOperation : IRowOperation<Vector4>
{ {
private readonly int startX; private readonly int startX;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
private readonly Configuration configuration; private readonly Configuration configuration;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
int startX, int startX,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
ColorMatrix matrix, ColorMatrix matrix,
@ -69,17 +69,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span) public void Invoke(int y, Span<Vector4> span)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length);
{ PixelOperations<TPixel>.Instance.ToVector4(this.configuration, rowSpan, span);
Span<TPixel> rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, rowSpan, span);
Vector4Utils.Transform(span, ref Unsafe.AsRef(this.matrix)); Vector4Utils.Transform(span, ref Unsafe.AsRef(this.matrix));
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, rowSpan); PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, span, rowSpan);
}
} }
} }
} }

4
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); var operation = new RowIntervalOperation(cdfData, tileYStartPositions, tileWidth, tileHeight, tileCount, halfTileWidth, luminanceLevels, source);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
this.Configuration, this.Configuration,
new Rectangle(0, 0, sourceWidth, tileYStartPositions.Count), new Rectangle(0, 0, sourceWidth, tileYStartPositions.Count),
in operation); in operation);
@ -522,7 +522,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
this.luminanceLevels, this.luminanceLevels,
source); source);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
this.configuration, this.configuration,
new Rectangle(0, 0, this.sourceWidth, this.tileYStartPositions.Count), new Rectangle(0, 0, this.sourceWidth, this.tileYStartPositions.Count),
in operation); in operation);

46
src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs

@ -52,7 +52,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean); using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Build the histogram of the grayscale levels // 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( ParallelRowIterator.IterateRows(
this.Configuration, this.Configuration,
interest, interest,
@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
// Apply the cdf to each pixel of the image // 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( ParallelRowIterator.IterateRows(
this.Configuration, this.Configuration,
interest, interest,
@ -85,7 +85,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the grayscale levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>. /// A <see langword="struct"/> implementing the grayscale levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>.
/// </summary> /// </summary>
private readonly struct GrayscaleLevelsRowIntervalOperation : IRowIntervalOperation private readonly struct GrayscaleLevelsRowOperation : IRowOperation
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly IMemoryOwner<int> histogramBuffer; private readonly IMemoryOwner<int> histogramBuffer;
@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
private readonly int luminanceLevels; private readonly int luminanceLevels;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public GrayscaleLevelsRowIntervalOperation( public GrayscaleLevelsRowOperation(
Rectangle bounds, Rectangle bounds,
IMemoryOwner<int> histogramBuffer, IMemoryOwner<int> histogramBuffer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
@ -107,18 +107,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan()); 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++) for (int x = 0; x < this.bounds.Width; x++)
{ {
int luminance = GetLuminance(Unsafe.Add(ref pixelBase, x), this.luminanceLevels); int luminance = GetLuminance(Unsafe.Add(ref pixelBase, x), this.luminanceLevels);
Unsafe.Add(ref histogramBase, luminance)++; Unsafe.Add(ref histogramBase, luminance)++;
}
} }
} }
} }
@ -126,7 +123,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the cdf application levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>. /// A <see langword="struct"/> implementing the cdf application levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>.
/// </summary> /// </summary>
private readonly struct CdfApplicationRowIntervalOperation : IRowIntervalOperation private readonly struct CdfApplicationRowOperation : IRowOperation
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly IMemoryOwner<int> cdfBuffer; private readonly IMemoryOwner<int> cdfBuffer;
@ -135,7 +132,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
private readonly float numberOfPixelsMinusCdfMin; private readonly float numberOfPixelsMinusCdfMin;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public CdfApplicationRowIntervalOperation( public CdfApplicationRowOperation(
Rectangle bounds, Rectangle bounds,
IMemoryOwner<int> cdfBuffer, IMemoryOwner<int> cdfBuffer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
@ -151,20 +148,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); 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)); ref TPixel pixel = ref Unsafe.Add(ref pixelBase, x);
int luminance = GetLuminance(pixel, this.luminanceLevels);
for (int x = 0; x < this.bounds.Width; x++) 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));
}
} }
} }
} }

33
src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs

@ -49,14 +49,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(graphicsOptions); PixelBlender<TPixel> blender = PixelOperations<TPixel>.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( ParallelRowIterator.IterateRows(
configuration, configuration,
interest, interest,
in operation); in operation);
} }
private readonly struct RowIntervalOperation : IRowIntervalOperation private readonly struct RowOperation : IRowOperation
{ {
private readonly Configuration configuration; private readonly Configuration configuration;
private readonly Rectangle bounds; private readonly Rectangle bounds;
@ -66,7 +66,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
Configuration configuration, Configuration configuration,
Rectangle bounds, Rectangle bounds,
PixelBlender<TPixel> blender, PixelBlender<TPixel> blender,
@ -83,23 +83,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> destination =
{ this.source.GetPixelRowSpan(y)
Span<TPixel> destination = .Slice(this.bounds.X, this.bounds.Width);
this.source.GetPixelRowSpan(y)
.Slice(this.bounds.X, this.bounds.Width);
// Switch color & destination in the 2nd and 3rd places because we are // Switch color & destination in the 2nd and 3rd places because we are
// applying the target color under the current one. // applying the target color under the current one.
this.blender.Blend( this.blender.Blend(
this.configuration, this.configuration,
destination, destination,
this.colors.GetSpan(), this.colors.GetSpan(),
destination, destination,
this.amount.GetSpan()); this.amount.GetSpan());
}
} }
} }
} }

35
src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs

@ -55,14 +55,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width); using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width);
rowColors.GetSpan().Fill(glowColor); rowColors.GetSpan().Fill(glowColor);
var operation = new RowIntervalOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source); var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source);
ParallelRowIterator.IterateRows<RowIntervalOperation, float>( ParallelRowIterator.IterateRows<RowOperation, float>(
configuration, configuration,
interest, interest,
in operation); in operation);
} }
private readonly struct RowIntervalOperation : IRowIntervalOperation<float> private readonly struct RowOperation : IRowOperation<float>
{ {
private readonly Configuration configuration; private readonly Configuration configuration;
private readonly Rectangle bounds; private readonly Rectangle bounds;
@ -74,7 +74,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
Configuration configuration, Configuration configuration,
Rectangle bounds, Rectangle bounds,
IMemoryOwner<TPixel> colors, IMemoryOwner<TPixel> colors,
@ -95,27 +95,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<float> span) public void Invoke(int y, Span<float> span)
{ {
Span<TPixel> colorSpan = this.colors.GetSpan(); Span<TPixel> 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<TPixel> destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width); Span<TPixel> destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
this.blender.Blend( this.blender.Blend(
this.configuration, this.configuration,
destination, destination,
destination, destination,
colorSpan, colorSpan,
span); span);
}
} }
} }
} }

37
src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs

@ -63,14 +63,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width); using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width);
rowColors.GetSpan().Fill(vignetteColor); rowColors.GetSpan().Fill(vignetteColor);
var operation = new RowIntervalOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source); var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source);
ParallelRowIterator.IterateRows<RowIntervalOperation, float>( ParallelRowIterator.IterateRows<RowOperation, float>(
configuration, configuration,
interest, interest,
in operation); in operation);
} }
private readonly struct RowIntervalOperation : IRowIntervalOperation<float> private readonly struct RowOperation : IRowOperation<float>
{ {
private readonly Configuration configuration; private readonly Configuration configuration;
private readonly Rectangle bounds; private readonly Rectangle bounds;
@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowOperation(
Configuration configuration, Configuration configuration,
Rectangle bounds, Rectangle bounds,
IMemoryOwner<TPixel> colors, IMemoryOwner<TPixel> colors,
@ -103,27 +103,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<float> span) public void Invoke(int y, Span<float> span)
{ {
Span<TPixel> colorSpan = this.colors.GetSpan(); Span<TPixel> 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);
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<TPixel> destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
this.blender.Blend(
this.configuration,
destination,
destination,
colorSpan,
span);
} }
Span<TPixel> destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
this.blender.Blend(
this.configuration,
destination,
destination,
colorSpan,
span);
} }
} }
} }

93
src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs

@ -5,101 +5,100 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
/// <summary> /// <summary>
/// Gets the closest color to the supplied color based upon the Eucladean distance. /// Gets the closest color to the supplied color based upon the Euclidean distance.
/// TODO: Expose this somehow.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
internal readonly struct EuclideanPixelMap<TPixel> : IPixelMap<TPixel>, IEquatable<EuclideanPixelMap<TPixel>> internal readonly struct EuclideanPixelMap<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly ConcurrentDictionary<int, Vector4> vectorCache; private readonly Vector4[] vectorCache;
private readonly ConcurrentDictionary<TPixel, int> distanceCache; private readonly ConcurrentDictionary<TPixel, int> distanceCache;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct. /// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct.
/// </summary> /// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param> /// <param name="palette">The color palette to map from.</param>
public EuclideanPixelMap(ReadOnlyMemory<TPixel> palette) [MethodImpl(InliningOptions.ShortMethod)]
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette)
{ {
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
this.Palette = palette; this.Palette = palette;
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span; this.vectorCache = new Vector4[palette.Length];
this.vectorCache = new ConcurrentDictionary<int, Vector4>();
this.distanceCache = new ConcurrentDictionary<TPixel, int>();
for (int i = 0; i < paletteSpan.Length; i++) // Use the same rules across all target frameworks.
{ this.distanceCache = new ConcurrentDictionary<TPixel, int>(Environment.ProcessorCount, 31);
this.vectorCache[i] = paletteSpan[i].ToScaledVector4(); PixelOperations<TPixel>.Instance.ToVector4(configuration, this.Palette.Span, this.vectorCache);
}
} }
/// <inheritdoc/> /// <summary>
public ReadOnlyMemory<TPixel> Palette { get; } /// Gets the color palette of this <see cref="EuclideanPixelMap{TPixel}"/>.
/// The palette memory is owned by the palette source that created it.
/// <inheritdoc/> /// </summary>
public override bool Equals(object obj) public ReadOnlyMemory<TPixel> Palette
=> obj is EuclideanPixelMap<TPixel> map && this.Equals(map); {
[MethodImpl(InliningOptions.ShortMethod)]
/// <inheritdoc/> get;
public bool Equals(EuclideanPixelMap<TPixel> other) }
=> this.Palette.Equals(other.Palette);
/// <inheritdoc/> /// <summary>
/// Returns the closest color in the palette and the index of that pixel.
/// The palette contents must match the one used in the constructor.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="int"/> index.</returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public int GetClosestColor(TPixel color, out TPixel match) public int GetClosestColor(TPixel color, out TPixel match)
{ {
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span; ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span);
// Check if the color is in the lookup table // 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 this.GetClosestColorSlow(color, ref paletteRef, out match);
return index;
} }
return this.GetClosestColorSlow(color, paletteSpan, out match); match = Unsafe.Add(ref paletteRef, index);
return index;
} }
/// <inheritdoc/>
public override int GetHashCode()
=> this.vectorCache.GetHashCode();
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match) private int GetClosestColorSlow(TPixel color, ref TPixel paletteRef, out TPixel match)
{ {
// Loop through the palette and find the nearest match. // Loop through the palette and find the nearest match.
int index = 0; int index = 0;
float leastDistance = float.MaxValue; float leastDistance = float.MaxValue;
Vector4 vector = color.ToScaledVector4(); var vector = color.ToVector4();
ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference<Vector4>(this.vectorCache);
for (int i = 0; i < palette.Length; i++) 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); 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) if (distance < leastDistance)
{ {
// Less than... assign.
index = i; index = i;
leastDistance = distance; 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 // Now I have the index, pop it into the cache for next time
this.distanceCache[color] = index; this.distanceCache[color] = index;
match = palette[index]; match = Unsafe.Add(ref paletteRef, index);
return index; return index;
} }
} }

87
src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs → src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs

@ -11,22 +11,40 @@ using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
/// <summary> /// <summary>
/// Contains extension methods for frame quantizers. /// Contains utility methods for <see cref="IFrameQuantizer{TPixel}"/> instances.
/// </summary> /// </summary>
public static class FrameQuantizerExtensions public static class FrameQuantizerUtilities
{ {
/// <summary>
/// Helper method for throwing an exception when a frame quantizer palette has
/// been requested but not built yet.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="palette">The frame quantizer palette.</param>
/// <exception cref="InvalidOperationException">
/// The palette has not been built via <see cref="IFrameQuantizer{TPixel}.BuildPalette(ImageFrame{TPixel}, Rectangle)"/>
/// </exception>
public static void CheckPaletteState<TPixel>(in ReadOnlyMemory<TPixel> palette)
where TPixel : unmanaged, IPixel<TPixel>
{
if (palette.Equals(default))
{
throw new InvalidOperationException("Frame Quantizer palette has not been built.");
}
}
/// <summary> /// <summary>
/// Quantizes an image frame and return the resulting output pixels. /// Quantizes an image frame and return the resulting output pixels.
/// </summary> /// </summary>
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam> /// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The frame </param> /// <param name="quantizer">The frame quantizer.</param>
/// <param name="source">The source image frame to quantize.</param> /// <param name="source">The source image frame to quantize.</param>
/// <param name="bounds">The bounds within the frame to quantize.</param> /// <param name="bounds">The bounds within the frame to quantize.</param>
/// <returns> /// <returns>
/// A <see cref="QuantizedFrame{TPixel}"/> representing a quantized version of the source frame pixels. /// A <see cref="IndexedImageFrame{TPixel}"/> representing a quantized version of the source frame pixels.
/// </returns> /// </returns>
public static QuantizedFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>( public static IndexedImageFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds) Rectangle bounds)
@ -37,35 +55,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
var interest = Rectangle.Intersect(source.Bounds(), bounds); var interest = Rectangle.Intersect(source.Bounds(), bounds);
// Collect the palette. Required before the second pass runs. // Collect the palette. Required before the second pass runs.
ReadOnlyMemory<TPixel> palette = quantizer.BuildPalette(source, interest); quantizer.BuildPalette(source, interest);
MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator;
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette); var destination = new IndexedImageFrame<TPixel>(
Memory<byte> output = quantizedFrame.GetWritablePixelMemory(); quantizer.Configuration,
interest.Width,
interest.Height,
quantizer.Palette);
if (quantizer.Options.Dither is null) if (quantizer.Options.Dither is null)
{ {
SecondPass(ref quantizer, source, interest, output, palette); SecondPass(ref quantizer, source, destination, interest);
} }
else else
{ {
// We clone the image as we don't want to alter the original via error diffusion based dithering. // We clone the image as we don't want to alter the original via error diffusion based dithering.
using (ImageFrame<TPixel> clone = source.Clone()) using ImageFrame<TPixel> clone = source.Clone();
{ SecondPass(ref quantizer, clone, destination, interest);
SecondPass(ref quantizer, clone, interest, output, palette);
}
} }
return quantizedFrame; return destination;
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private static void SecondPass<TFrameQuantizer, TPixel>( private static void SecondPass<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, IndexedImageFrame<TPixel> destination,
Memory<byte> output, Rectangle bounds)
ReadOnlyMemory<TPixel> palette)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -73,8 +90,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (dither is null) if (dither is null)
{ {
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(quantizer, source, output, bounds, palette); var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(
ParallelRowIterator.IterateRows( ref quantizer,
source,
destination,
bounds);
ParallelRowIterator.IterateRowIntervals(
quantizer.Configuration, quantizer.Configuration,
bounds, bounds,
in operation); in operation);
@ -82,7 +104,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return; return;
} }
dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds); dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds);
} }
private readonly struct RowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation private readonly struct RowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation
@ -91,43 +113,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
private readonly TFrameQuantizer quantizer; private readonly TFrameQuantizer quantizer;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output; private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowIntervalOperation(
in TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds, Rectangle bounds)
ReadOnlyMemory<TPixel> palette)
{ {
this.quantizer = quantizer; this.quantizer = quantizer;
this.source = source; this.source = source;
this.output = output; this.destination = destination;
this.bounds = bounds; this.bounds = bounds;
this.palette = palette;
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(in RowInterval rows)
{ {
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
Span<byte> outputSpan = this.output.Span;
int width = this.bounds.Width;
int offsetY = this.bounds.Top; int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left; int offsetX = this.bounds.Left;
for (int y = rows.Min; y < rows.Max; y++) for (int y = rows.Min; y < rows.Max; y++)
{ {
Span<TPixel> row = this.source.GetPixelRowSpan(y); Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
int rowStart = (y - offsetY) * width; Span<byte> destinationRow = this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY);
// TODO: This can be a bulk operation.
for (int x = this.bounds.Left; x < this.bounds.Right; x++) 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 _);
} }
} }
} }

32
src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs

@ -24,33 +24,37 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
QuantizerOptions Options { get; } QuantizerOptions Options { get; }
/// <summary> /// <summary>
/// Quantizes an image frame and return the resulting output pixels. /// Gets the quantized color palette.
/// </summary> /// </summary>
/// <param name="source">The source image frame to quantize.</param> /// <exception cref="InvalidOperationException">
/// <param name="bounds">The bounds within the frame to quantize.</param> /// The palette has not been built via <see cref="BuildPalette(ImageFrame{TPixel}, Rectangle)"/>.
/// <returns> /// </exception>
/// A <see cref="QuantizedFrame{TPixel}"/> representing a quantized version of the source frame pixels. ReadOnlyMemory<TPixel> Palette { get; }
/// </returns>
QuantizedFrame<TPixel> QuantizeFrame(
ImageFrame<TPixel> source,
Rectangle bounds);
/// <summary> /// <summary>
/// Builds the quantized palette from the given image frame and bounds. /// Builds the quantized palette from the given image frame and bounds.
/// </summary> /// </summary>
/// <param name="source">The source image frame.</param> /// <param name="source">The source image frame.</param>
/// <param name="bounds">The region of interest bounds.</param> /// <param name="bounds">The region of interest bounds.</param>
/// <returns>The <see cref="ReadOnlyMemory{TPixel}"/> palette.</returns> void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds);
ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds);
/// <summary>
/// Quantizes an image frame and return the resulting output pixels.
/// </summary>
/// <param name="source">The source image frame to quantize.</param>
/// <param name="bounds">The bounds within the frame to quantize.</param>
/// <returns>
/// A <see cref="IndexedImageFrame{TPixel}"/> representing a quantized version of the source frame pixels.
/// </returns>
IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds);
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="color">The color to match.</param> /// <param name="color">The color to match.</param>
/// <param name="palette">The output color palette.</param>
/// <param name="match">The matched color.</param> /// <param name="match">The matched color.</param>
/// <returns>The <see cref="byte"/> index.</returns> /// <returns>The <see cref="byte"/> index.</returns>
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match); byte GetQuantizedColor(TPixel color, out TPixel match);
// TODO: Enable bulk operations. // TODO: Enable bulk operations.
// void GetQuantizedColors(ReadOnlySpan<TPixel> colors, ReadOnlySpan<TPixel> palette, Span<byte> indices, Span<TPixel> matches); // void GetQuantizedColors(ReadOnlySpan<TPixel> colors, ReadOnlySpan<TPixel> palette, Span<byte> indices, Span<TPixel> matches);

30
src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs

@ -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
{
/// <summary>
/// Allows the mapping of input colors to colors within a given palette.
/// TODO: Expose this somehow.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal interface IPixelMap<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Gets the color palette containing colors to match.
/// </summary>
ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Returns the closest color in the palette and the index of that pixel.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="int"/> index.</returns>
int GetClosestColor(TPixel color, out TPixel match);
}
}

116
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
{
/// <summary>
/// A pixel-specific image frame where each pixel buffer value represents an index in a color palette.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public sealed class IndexedImageFrame<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private IMemoryOwner<byte> pixelsOwner;
private IMemoryOwner<TPixel> paletteOwner;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="IndexedImageFrame{TPixel}"/> class.
/// </summary>
/// <param name="configuration">
/// The configuration which allows altering default behaviour or extending the library.
/// </param>
/// <param name="width">The frame width.</param>
/// <param name="height">The frame height.</param>
/// <param name="palette">The color palette.</param>
internal IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory<TPixel> 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<TPixel>(palette.Length);
palette.Span.CopyTo(this.paletteOwner.GetSpan());
this.Palette = this.paletteOwner.Memory.Slice(0, palette.Length);
}
/// <summary>
/// Gets the configuration which allows altering default behaviour or extending the library.
/// </summary>
public Configuration Configuration { get; }
/// <summary>
/// Gets the width of this <see cref="IndexedImageFrame{TPixel}"/>.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of this <see cref="IndexedImageFrame{TPixel}"/>.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the color palette of this <see cref="IndexedImageFrame{TPixel}"/>.
/// </summary>
public ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Gets the pixels of this <see cref="IndexedImageFrame{TPixel}"/>.
/// </summary>
/// <returns>The <see cref="ReadOnlySpan{T}"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlySpan<byte> GetPixelBufferSpan() => this.pixelsOwner.GetSpan(); // TODO: Buffer2D<byte>
/// <summary>
/// Gets the representation of the pixels as a <see cref="ReadOnlySpan{T}"/> of contiguous memory
/// at row <paramref name="rowIndex"/> beginning from the the first pixel on that row.
/// </summary>
/// <param name="rowIndex">The row index in the pixel buffer.</param>
/// <returns>The pixel row as a <see cref="ReadOnlySpan{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlySpan<byte> GetPixelRowSpan(int rowIndex)
=> this.GetWritablePixelRowSpanUnsafe(rowIndex);
/// <summary>
/// <para>
/// Gets the representation of the pixels as a <see cref="Span{T}"/> of contiguous memory
/// at row <paramref name="rowIndex"/> beginning from the the first pixel on that row.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
/// <param name="rowIndex">The row index in the pixel buffer.</param>
/// <returns>The pixel row as a <see cref="Span{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public Span<byte> GetWritablePixelRowSpanUnsafe(int rowIndex)
=> this.pixelsOwner.GetSpan().Slice(rowIndex * this.Width, this.Width);
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.isDisposed = true;
this.pixelsOwner.Dispose();
this.paletteOwner.Dispose();
this.pixelsOwner = null;
this.paletteOwner = null;
}
}
}
}

124
src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs

@ -3,9 +3,9 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -20,10 +20,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public struct OctreeFrameQuantizer<TPixel> : IFrameQuantizer<TPixel> public struct OctreeFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly int colors; private readonly int maxColors;
private readonly Octree octree; private readonly Octree octree;
private IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private EuclideanPixelMap<TPixel> pixelMap; private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering; private readonly bool isDithering;
private bool isDisposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> struct. /// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> struct.
@ -39,10 +42,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration; this.Configuration = configuration;
this.Options = options; this.Options = options;
this.colors = this.Options.MaxColors; this.maxColors = this.Options.MaxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.maxColors).Clamp(1, 8));
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.palette = default;
this.pixelMap = default; this.pixelMap = default;
this.isDithering = !(this.Options.Dither is null); this.isDithering = !(this.Options.Dither is null);
this.isDisposed = false;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -52,13 +58,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public QuantizerOptions Options { get; } public QuantizerOptions Options { get; }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] public ReadOnlyMemory<TPixel> Palette
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds) {
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); get
{
FrameQuantizerUtilities.CheckPaletteState(in this.palette);
return this.palette;
}
}
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds) public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{ {
using IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width); using IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width);
Span<Rgba32> bufferSpan = buffer.GetSpan(); Span<Rgba32> bufferSpan = buffer.GetSpan();
@ -78,32 +89,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
} }
} }
TPixel[] palette = this.octree.Palletize(this.colors); Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
this.pixelMap = new EuclideanPixelMap<TPixel>(palette); int paletteIndex = 0;
this.octree.Palletize(paletteSpan, this.maxColors, ref paletteIndex);
// Length of reduced palette + transparency.
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, Math.Min(paletteIndex + 2, QuantizerConstants.MaxColors));
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
return palette; this.palette = result;
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match) public readonly IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
{ {
// Octree only maps the RGB component of a color // Octree only maps the RGB component of a color
// so cannot tell the difference between a fully transparent // so cannot tell the difference between a fully transparent
// pixel and a black one. // pixel and a black one.
if (!this.isDithering && !color.Equals(default)) if (this.isDithering || color.Equals(default))
{ {
var index = (byte)this.octree.GetPaletteIndex(color); return (byte)this.pixelMap.GetClosestColor(color, out match);
match = palette[index];
return index;
} }
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;
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
if (!this.isDisposed)
{
this.isDisposed = true;
this.paletteOwner.Dispose();
this.paletteOwner = null;
}
} }
/// <summary> /// <summary>
@ -111,21 +139,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
private sealed class Octree private sealed class Octree
{ {
/// <summary>
/// Mask used when getting the appropriate pixels for a given node.
/// </summary>
private static readonly byte[] Mask = new byte[]
{
0b10000000,
0b1000000,
0b100000,
0b10000,
0b1000,
0b100,
0b10,
0b1
};
/// <summary> /// <summary>
/// The root of the Octree /// The root of the Octree
/// </summary> /// </summary>
@ -162,6 +175,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.previousNode = null; this.previousNode = null;
} }
/// <summary>
/// Gets the mask used when getting the appropriate pixels for a given node.
/// </summary>
private static ReadOnlySpan<byte> Mask => new byte[]
{
0b10000000,
0b1000000,
0b100000,
0b10000,
0b1000,
0b100,
0b10,
0b1
};
/// <summary> /// <summary>
/// Gets or sets the number of leaves in the tree /// Gets or sets the number of leaves in the tree
/// </summary> /// </summary>
@ -216,26 +244,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary> /// <summary>
/// Convert the nodes in the Octree to a palette with a maximum of colorCount colors /// Convert the nodes in the Octree to a palette with a maximum of colorCount colors
/// </summary> /// </summary>
/// <param name="palette">The palette to fill.</param>
/// <param name="colorCount">The maximum number of colors</param> /// <param name="colorCount">The maximum number of colors</param>
/// <returns> /// <param name="paletteIndex">The palette index, used to calculate the final size of the palette.</param>
/// An <see cref="List{TPixel}"/> with the palletized colors
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public TPixel[] Palletize(int colorCount) public void Palletize(Span<TPixel> palette, int colorCount, ref int paletteIndex)
{ {
while (this.Leaves > colorCount - 1) while (this.Leaves > colorCount - 1)
{ {
this.Reduce(); this.Reduce();
} }
// Now palletize the nodes
var palette = new TPixel[colorCount];
int paletteIndex = 0;
this.root.ConstructPalette(palette, ref paletteIndex); this.root.ConstructPalette(palette, ref paletteIndex);
// And return the palette
return palette;
} }
/// <summary> /// <summary>
@ -437,12 +457,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="palette">The palette</param> /// <param name="palette">The palette</param>
/// <param name="index">The current palette index</param> /// <param name="index">The current palette index</param>
[MethodImpl(InliningOptions.ColdPath)] [MethodImpl(InliningOptions.ColdPath)]
public void ConstructPalette(TPixel[] palette, ref int index) public void ConstructPalette(Span<TPixel> palette, ref int index)
{ {
if (this.leaf) if (this.leaf)
{ {
// Set the color of the palette entry // 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; TPixel pixel = default;
pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue)); pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue));
palette[index] = pixel; palette[index] = pixel;
@ -513,11 +537,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private static int GetColorIndex(ref Rgba32 color, int level) private static int GetColorIndex(ref Rgba32 color, int level)
{ {
DebugGuard.MustBeLessThan(level, Mask.Length, nameof(level));
int shift = 7 - 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) return ((color.R & mask) >> shift)
| ((color.G & mask) >> (shift - 1)) | ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2)); | ((color.B & mask) >> (shift - 2));
} }
/// <summary> /// <summary>

4
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs

@ -11,12 +11,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
public class OctreeQuantizer : IQuantizer public class OctreeQuantizer : IQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class /// Initializes a new instance of the <see cref="OctreeQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>. /// using the default <see cref="QuantizerOptions"/>.
/// </summary> /// </summary>
public OctreeQuantizer() public OctreeQuantizer()
: this(new QuantizerOptions()) : this(DefaultOptions)
{ {
} }

26
src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs

@ -15,7 +15,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
internal struct PaletteFrameQuantizer<TPixel> : IFrameQuantizer<TPixel> internal struct PaletteFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly ReadOnlyMemory<TPixel> palette;
private readonly EuclideanPixelMap<TPixel> pixelMap; private readonly EuclideanPixelMap<TPixel> pixelMap;
/// <summary> /// <summary>
@ -23,18 +22,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param> /// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="options">The quantizer options defining quantization rules.</param> /// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="colors">A <see cref="ReadOnlyMemory{TPixel}"/> containing all colors in the palette.</param> /// <param name="pixelMap">The pixel map for looking up color matches from a predefined palette.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory<TPixel> colors) public PaletteFrameQuantizer(
Configuration configuration,
QuantizerOptions options,
EuclideanPixelMap<TPixel> pixelMap)
{ {
Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
this.Configuration = configuration; this.Configuration = configuration;
this.Options = options; this.Options = options;
this.pixelMap = pixelMap;
this.palette = colors;
this.pixelMap = new EuclideanPixelMap<TPixel>(colors);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -43,19 +43,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/> /// <inheritdoc/>
public QuantizerOptions Options { get; } public QuantizerOptions Options { get; }
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette => this.pixelMap.Palette;
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds) public readonly IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds) public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
=> this.palette; {
}
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match) public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
=> (byte)this.pixelMap.GetClosestColor(color, out match); => (byte)this.pixelMap.GetClosestColor(color, out match);
/// <inheritdoc/> /// <inheritdoc/>

23
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs

@ -11,12 +11,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
public class PaletteQuantizer : IQuantizer public class PaletteQuantizer : IQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
private readonly ReadOnlyMemory<Color> colorPalette;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class. /// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary> /// </summary>
/// <param name="palette">The color palette.</param> /// <param name="palette">The color palette.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette) public PaletteQuantizer(ReadOnlyMemory<Color> 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.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
this.Palette = palette; this.colorPalette = palette;
this.Options = options; this.Options = options;
} }
/// <summary>
/// Gets the color palette.
/// </summary>
public ReadOnlyMemory<Color> Palette { get; }
/// <inheritdoc /> /// <inheritdoc />
public QuantizerOptions Options { get; } public QuantizerOptions Options { get; }
@ -53,11 +51,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
Guard.NotNull(options, nameof(options)); 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]; var palette = new TPixel[length];
Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, options, palette);
var pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
return new PaletteFrameQuantizer<TPixel>(configuration, options, pixelMap);
} }
} }
} }

10
src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs

@ -39,10 +39,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
Configuration configuration = this.Configuration; Configuration configuration = this.Configuration;
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(configuration); using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(configuration);
using QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(source, interest); using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(source, interest);
var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized); var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
configuration, configuration,
interest, interest,
in operation); in operation);
@ -52,13 +52,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly QuantizedFrame<TPixel> quantized; private readonly IndexedImageFrame<TPixel> quantized;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowIntervalOperation(
Rectangle bounds, Rectangle bounds,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
QuantizedFrame<TPixel> quantized) IndexedImageFrame<TPixel> quantized)
{ {
this.bounds = bounds; this.bounds = bounds;
this.source = source; this.source = source;
@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(in RowInterval rows)
{ {
ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelSpan(); ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelBufferSpan();
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span; ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span;
int offsetY = this.bounds.Top; int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left; int offsetX = this.bounds.Left;

91
src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs

@ -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
{
/// <summary>
/// Represents a quantized image frame where the pixels indexed by a color palette.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public sealed class QuantizedFrame<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private IMemoryOwner<byte> pixels;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="QuantizedFrame{TPixel}"/> class.
/// </summary>
/// <param name="memoryAllocator">Used to allocated memory for image processing operations.</param>
/// <param name="width">The image width.</param>
/// <param name="height">The image height.</param>
/// <param name="palette">The color palette.</param>
internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, ReadOnlyMemory<TPixel> 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);
}
/// <summary>
/// Gets the width of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the color palette of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
public ReadOnlyMemory<TPixel> Palette { get; private set; }
/// <summary>
/// Gets the pixels of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlySpan<byte> GetPixelSpan() => this.pixels.GetSpan();
/// <summary>
/// Gets the representation of the pixels as a <see cref="Span{T}"/> of contiguous memory
/// at row <paramref name="rowIndex"/> beginning from the the first pixel on that row.
/// </summary>
/// <param name="rowIndex">The row.</param>
/// <returns>The pixel row as a <see cref="ReadOnlySpan{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlySpan<byte> GetRowSpan(int rowIndex)
=> this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width);
/// <inheritdoc/>
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.isDisposed = true;
this.pixels?.Dispose();
this.pixels = null;
this.Palette = null;
}
/// <summary>
/// Get the non-readonly memory of pixel data so <see cref="IFrameQuantizer{TPixel}"/> can fill it.
/// </summary>
internal Memory<byte> GetWritablePixelMemory() => this.pixels.Memory;
}
}

4
src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs

@ -10,11 +10,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
public class WebSafePaletteQuantizer : PaletteQuantizer public class WebSafePaletteQuantizer : PaletteQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class. /// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.
/// </summary> /// </summary>
public WebSafePaletteQuantizer() public WebSafePaletteQuantizer()
: this(new QuantizerOptions()) : this(DefaultOptions)
{ {
} }

4
src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs

@ -9,11 +9,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
public class WernerPaletteQuantizer : PaletteQuantizer public class WernerPaletteQuantizer : PaletteQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class. /// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary> /// </summary>
public WernerPaletteQuantizer() public WernerPaletteQuantizer()
: this(new QuantizerOptions()) : this(DefaultOptions)
{ {
} }

144
src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs

@ -5,6 +5,7 @@ using System;
using System.Buffers; using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -65,30 +66,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
/// <summary> private IMemoryOwner<Moment> momentsOwner;
/// Color moments. private IMemoryOwner<byte> tagsOwner;
/// </summary> private IMemoryOwner<TPixel> paletteOwner;
private IMemoryOwner<Moment> moments; private ReadOnlyMemory<TPixel> palette;
private int maxColors;
/// <summary>
/// Color space tag.
/// </summary>
private IMemoryOwner<byte> tag;
/// <summary>
/// Maximum allowed color depth
/// </summary>
private int colors;
/// <summary>
/// The color cube representing the image palette
/// </summary>
private readonly Box[] colorCube; private readonly Box[] colorCube;
private EuclideanPixelMap<TPixel> pixelMap; private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering; private readonly bool isDithering;
private bool isDisposed; private bool isDisposed;
/// <summary> /// <summary>
@ -104,11 +89,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration; this.Configuration = configuration;
this.Options = options; this.Options = options;
this.maxColors = this.Options.MaxColors;
this.memoryAllocator = this.Configuration.MemoryAllocator; this.memoryAllocator = this.Configuration.MemoryAllocator;
this.moments = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean); this.momentsOwner = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tag = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean); this.tagsOwner = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.colors = this.Options.MaxColors; this.paletteOwner = this.memoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.colorCube = new Box[this.colors]; this.palette = default;
this.colorCube = new Box[this.maxColors];
this.isDisposed = false; this.isDisposed = false;
this.pixelMap = default; this.pixelMap = default;
this.isDithering = this.isDithering = !(this.Options.Dither is null); this.isDithering = this.isDithering = !(this.Options.Dither is null);
@ -121,21 +108,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public QuantizerOptions Options { get; } public QuantizerOptions Options { get; }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] public ReadOnlyMemory<TPixel> Palette
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds) {
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); get
{
FrameQuantizerUtilities.CheckPaletteState(in this.palette);
return this.palette;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds) public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{ {
this.Build3DHistogram(source, bounds); this.Build3DHistogram(source, bounds);
this.Get3DMoments(this.memoryAllocator); this.Get3DMoments(this.memoryAllocator);
this.BuildCube(); this.BuildCube();
var palette = new TPixel[this.colors]; ReadOnlySpan<Moment> momentsSpan = this.momentsOwner.GetSpan();
ReadOnlySpan<Moment> momentsSpan = this.moments.GetSpan(); Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
for (int k = 0; k < this.maxColors; k++)
for (int k = 0; k < this.colors; k++)
{ {
this.Mark(ref this.colorCube[k], (byte)k); this.Mark(ref this.colorCube[k], (byte)k);
@ -143,50 +134,57 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (moment.Weight > 0) if (moment.Weight > 0)
{ {
ref TPixel color = ref palette[k]; ref TPixel color = ref paletteSpan[k];
color.FromScaledVector4(moment.Normalize()); color.FromScaledVector4(moment.Normalize());
} }
} }
this.pixelMap = new EuclideanPixelMap<TPixel>(palette); ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, this.maxColors);
return palette; this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
this.palette = result;
} }
/// <inheritdoc/> /// <inheritdoc/>
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match) [MethodImpl(InliningOptions.ShortMethod)]
public readonly IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/>
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
{ {
if (!this.isDithering) if (this.isDithering)
{ {
Rgba32 rgba = default; return (byte)this.pixelMap.GetClosestColor(color, out match);
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<byte> 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); 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<byte> 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;
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() 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;
} }
/// <summary> /// <summary>
@ -364,7 +362,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="bounds">The bounds within the source image to quantize.</param> /// <param name="bounds">The bounds within the source image to quantize.</param>
private void Build3DHistogram(ImageFrame<TPixel> source, Rectangle bounds) private void Build3DHistogram(ImageFrame<TPixel> source, Rectangle bounds)
{ {
Span<Moment> momentSpan = this.moments.GetSpan(); Span<Moment> momentSpan = this.momentsOwner.GetSpan();
// Build up the 3-D color histogram // Build up the 3-D color histogram
using IMemoryOwner<Rgba32> buffer = this.memoryAllocator.Allocate<Rgba32>(bounds.Width); using IMemoryOwner<Rgba32> buffer = this.memoryAllocator.Allocate<Rgba32>(bounds.Width);
@ -392,13 +390,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary> /// <summary>
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
/// </summary> /// </summary>
/// <param name="memoryAllocator">The memory allocator used for allocating buffers.</param> /// <param name="allocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator memoryAllocator) private void Get3DMoments(MemoryAllocator allocator)
{ {
using IMemoryOwner<Moment> volume = memoryAllocator.Allocate<Moment>(IndexCount * IndexAlphaCount); using IMemoryOwner<Moment> volume = allocator.Allocate<Moment>(IndexCount * IndexAlphaCount);
using IMemoryOwner<Moment> area = memoryAllocator.Allocate<Moment>(IndexAlphaCount); using IMemoryOwner<Moment> area = allocator.Allocate<Moment>(IndexAlphaCount);
Span<Moment> momentSpan = this.moments.GetSpan(); Span<Moment> momentSpan = this.momentsOwner.GetSpan();
Span<Moment> volumeSpan = volume.GetSpan(); Span<Moment> volumeSpan = volume.GetSpan();
Span<Moment> areaSpan = area.GetSpan(); Span<Moment> areaSpan = area.GetSpan();
int baseIndex = GetPaletteIndex(1, 0, 0, 0); int baseIndex = GetPaletteIndex(1, 0, 0, 0);
@ -440,7 +438,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns> /// <returns>The <see cref="float"/>.</returns>
private double Variance(ref Box cube) private double Variance(ref Box cube)
{ {
ReadOnlySpan<Moment> momentSpan = this.moments.GetSpan(); ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
Moment volume = Volume(ref cube, momentSpan); Moment volume = Volume(ref cube, momentSpan);
Moment variance = Moment variance =
@ -481,7 +479,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns> /// <returns>The <see cref="float"/>.</returns>
private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole) private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole)
{ {
ReadOnlySpan<Moment> momentSpan = this.moments.GetSpan(); ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
Moment bottom = Bottom(ref cube, direction, momentSpan); Moment bottom = Bottom(ref cube, direction, momentSpan);
float max = 0F; float max = 0F;
@ -527,7 +525,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>Returns a value indicating whether the box has been split.</returns> /// <returns>Returns a value indicating whether the box has been split.</returns>
private bool Cut(ref Box set1, ref Box set2) private bool Cut(ref Box set1, ref Box set2)
{ {
ReadOnlySpan<Moment> momentSpan = this.moments.GetSpan(); ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
Moment whole = Volume(ref set1, momentSpan); Moment whole = Volume(ref set1, momentSpan);
float maxR = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutR, whole); 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
/// <param name="label">A label.</param> /// <param name="label">A label.</param>
private void Mark(ref Box cube, byte label) private void Mark(ref Box cube, byte label)
{ {
Span<byte> tagSpan = this.tag.GetSpan(); Span<byte> tagSpan = this.tagsOwner.GetSpan();
for (int r = cube.RMin + 1; r <= cube.RMax; r++) for (int r = cube.RMin + 1; r <= cube.RMax; r++)
{ {
@ -634,7 +632,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
private void BuildCube() private void BuildCube()
{ {
Span<double> vv = stackalloc double[this.colors]; // Store the volume variance.
using IMemoryOwner<double> vvOwner = this.Configuration.MemoryAllocator.Allocate<double>(this.maxColors);
Span<double> vv = vvOwner.GetSpan();
ref Box cube = ref this.colorCube[0]; ref Box cube = ref this.colorCube[0];
cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0; cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0;
@ -643,7 +643,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
int next = 0; 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 nextCube = ref this.colorCube[next];
ref Box currentCube = ref this.colorCube[i]; ref Box currentCube = ref this.colorCube[i];
@ -672,7 +672,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (temp <= 0D) if (temp <= 0D)
{ {
this.colors = i + 1; this.maxColors = i + 1;
break; break;
} }
} }

4
src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs

@ -10,12 +10,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
public class WuQuantizer : IQuantizer public class WuQuantizer : IQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class /// Initializes a new instance of the <see cref="WuQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>. /// using the default <see cref="QuantizerOptions"/>.
/// </summary> /// </summary>
public WuQuantizer() public WuQuantizer()
: this(new QuantizerOptions()) : this(DefaultOptions)
{ {
} }

19
src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs

@ -51,7 +51,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings parallelSettings =
ParallelExecutionSettings.FromConfiguration(this.Configuration).MultiplyMinimumPixelsPerTask(4); ParallelExecutionSettings.FromConfiguration(this.Configuration).MultiplyMinimumPixelsPerTask(4);
var operation = new RowIntervalOperation(bounds, source, destination); var operation = new RowOperation(bounds, source, destination);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRows(
bounds, bounds,
@ -62,20 +62,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the processor logic for <see cref="CropProcessor{T}"/>. /// A <see langword="struct"/> implementing the processor logic for <see cref="CropProcessor{T}"/>.
/// </summary> /// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation private readonly struct RowOperation : IRowOperation
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination; private readonly ImageFrame<TPixel> destination;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RowIntervalOperation"/> struct. /// Initializes a new instance of the <see cref="RowOperation"/> struct.
/// </summary> /// </summary>
/// <param name="bounds">The target processing bounds for the current instance.</param> /// <param name="bounds">The target processing bounds for the current instance.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current instance.</param> /// <param name="source">The source <see cref="Image{TPixel}"/> for the current instance.</param>
/// <param name="destination">The destination <see cref="Image{TPixel}"/> for the current instance.</param> /// <param name="destination">The destination <see cref="Image{TPixel}"/> for the current instance.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(Rectangle bounds, ImageFrame<TPixel> source, ImageFrame<TPixel> destination) public RowOperation(Rectangle bounds, ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{ {
this.bounds = bounds; this.bounds = bounds;
this.source = source; this.source = source;
@ -84,14 +84,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.Left);
{ Span<TPixel> targetRow = this.destination.GetPixelRowSpan(y - this.bounds.Top);
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.Left); sourceRow.Slice(0, this.bounds.Width).CopyTo(targetRow);
Span<TPixel> targetRow = this.destination.GetPixelRowSpan(y - this.bounds.Top);
sourceRow.Slice(0, this.bounds.Width).CopyTo(targetRow);
}
} }
} }
} }

79
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs

@ -111,7 +111,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
in operation); in operation);
} }
private readonly struct NNAffineOperation : IRowIntervalOperation private readonly struct NNAffineOperation : IRowOperation
{ {
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination; private readonly ImageFrame<TPixel> destination;
@ -133,28 +133,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> destRow = this.destination.GetPixelRowSpan(y);
for (int x = 0; x < this.maxX; x++)
{ {
Span<TPixel> 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); destRow[x] = this.source[px, py];
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];
}
} }
} }
} }
} }
private readonly struct AffineOperation<TResampler> : IRowIntervalOperation<Vector4> private readonly struct AffineOperation<TResampler> : IRowOperation<Vector4>
where TResampler : struct, IResampler where TResampler : struct, IResampler
{ {
private readonly Configuration configuration; private readonly Configuration configuration;
@ -193,41 +190,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span) public void Invoke(int y, Span<Vector4> span)
{ {
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer; Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
for (int y = rows.Min; y < rows.Max; y++)
{
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
this.destination.GetPixelRowSpan(y),
span);
ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y)); PixelOperations<TPixel>.Instance.ToVector4(
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y)); this.configuration,
this.destination.GetPixelRowSpan(y),
span);
for (int x = 0; x < this.maxX; x++) ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y));
{ ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), this.matrix);
LinearTransformUtilities.Convolve(
in this.sampler,
point,
sourceBuffer,
span,
x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive( for (int x = 0; x < this.maxX; x++)
this.configuration, {
// 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, span,
this.destination.GetPixelRowSpan(y)); x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
this.destination.GetPixelRowSpan(y));
} }
} }
} }

15
src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs

@ -5,7 +5,6 @@ using System;
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -76,28 +75,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private void FlipY(ImageFrame<TPixel> source, Configuration configuration) private void FlipY(ImageFrame<TPixel> source, Configuration configuration)
{ {
var operation = new RowIntervalOperation(source); var operation = new RowOperation(source);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRows(
configuration, configuration,
source.Bounds(), source.Bounds(),
in operation); in operation);
} }
private readonly struct RowIntervalOperation : IRowIntervalOperation private readonly struct RowOperation : IRowOperation
{ {
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(ImageFrame<TPixel> source) => this.source = source; public RowOperation(ImageFrame<TPixel> source) => this.source = source;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y) => this.source.GetPixelRowSpan(y).Reverse();
{
for (int y = rows.Min; y < rows.Max; y++)
{
this.source.GetPixelRowSpan(y).Reverse();
}
}
} }
} }
} }

79
src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs

@ -111,7 +111,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
in operation); in operation);
} }
private readonly struct NNProjectiveOperation : IRowIntervalOperation private readonly struct NNProjectiveOperation : IRowOperation
{ {
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination; private readonly ImageFrame<TPixel> destination;
@ -133,28 +133,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> destRow = this.destination.GetPixelRowSpan(y);
for (int x = 0; x < this.maxX; x++)
{ {
Span<TPixel> 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); destRow[x] = this.source[px, py];
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];
}
} }
} }
} }
} }
private readonly struct ProjectiveOperation<TResampler> : IRowIntervalOperation<Vector4> private readonly struct ProjectiveOperation<TResampler> : IRowOperation<Vector4>
where TResampler : struct, IResampler where TResampler : struct, IResampler
{ {
private readonly Configuration configuration; private readonly Configuration configuration;
@ -193,41 +190,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span) public void Invoke(int y, Span<Vector4> span)
{ {
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer; Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
for (int y = rows.Min; y < rows.Max; y++)
{
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
this.destination.GetPixelRowSpan(y),
span);
ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y)); PixelOperations<TPixel>.Instance.ToVector4(
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y)); this.configuration,
this.destination.GetPixelRowSpan(y),
span);
for (int x = 0; x < this.maxX; x++) ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y));
{ ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
// 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);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive( for (int x = 0; x < this.maxX; x++)
this.configuration, {
// 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, span,
this.destination.GetPixelRowSpan(y)); x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
this.destination.GetPixelRowSpan(y));
} }
} }
} }

44
src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs

@ -131,7 +131,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private void Rotate180(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration) private void Rotate180(ImageFrame<TPixel> source, ImageFrame<TPixel> 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( ParallelRowIterator.IterateRows(
configuration, configuration,
source.Bounds(), source.Bounds(),
@ -147,7 +147,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private void Rotate270(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration) private void Rotate270(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{ {
var operation = new Rotate270RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source, destination); var operation = new Rotate270RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source, destination);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
configuration, configuration,
source.Bounds(), source.Bounds(),
in operation); in operation);
@ -161,14 +161,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private void Rotate90(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration) private void Rotate90(ImageFrame<TPixel> source, ImageFrame<TPixel> 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( ParallelRowIterator.IterateRows(
configuration, configuration,
source.Bounds(), source.Bounds(),
in operation); in operation);
} }
private readonly struct Rotate180RowIntervalOperation : IRowIntervalOperation private readonly struct Rotate180RowOperation : IRowOperation
{ {
private readonly int width; private readonly int width;
private readonly int height; private readonly int height;
@ -176,7 +176,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ImageFrame<TPixel> destination; private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public Rotate180RowIntervalOperation( public Rotate180RowOperation(
int width, int width,
int height, int height,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
@ -189,17 +189,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
{ Span<TPixel> targetRow = this.destination.GetPixelRowSpan(this.height - y - 1);
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
Span<TPixel> targetRow = this.destination.GetPixelRowSpan(this.height - y - 1);
for (int x = 0; x < this.width; x++) for (int x = 0; x < this.width; x++)
{ {
targetRow[this.width - x - 1] = sourceRow[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 Rectangle bounds;
private readonly int width; private readonly int width;
@ -257,7 +254,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ImageFrame<TPixel> destination; private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public Rotate90RowIntervalOperation( public Rotate90RowOperation(
Rectangle bounds, Rectangle bounds,
int width, int width,
int height, int height,
@ -272,18 +269,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
int newX = this.height - y - 1;
for (int x = 0; x < this.width; x++)
{ {
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y); if (this.bounds.Contains(newX, x))
int newX = this.height - y - 1;
for (int x = 0; x < this.width; x++)
{ {
if (this.bounds.Contains(newX, x)) this.destination[newX, x] = sourceRow[x];
{
this.destination[newX, x] = sourceRow[x];
}
} }
} }
} }

25
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 widthFactor = sourceRectangle.Width / (float)destinationRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)destinationRectangle.Height; float heightFactor = sourceRectangle.Height / (float)destinationRectangle.Height;
var operation = new NNRowIntervalOperation( var operation = new NNRowOperation(
sourceRectangle, sourceRectangle,
destinationRectangle, destinationRectangle,
widthFactor, 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 sourceBounds;
private readonly Rectangle destinationBounds; private readonly Rectangle destinationBounds;
@ -203,7 +203,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ImageFrame<TPixel> destination; private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public NNRowIntervalOperation( public NNRowOperation(
Rectangle sourceBounds, Rectangle sourceBounds,
Rectangle destinationBounds, Rectangle destinationBounds,
float widthFactor, float widthFactor,
@ -220,7 +220,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(int y)
{ {
int sourceX = this.sourceBounds.X; int sourceX = this.sourceBounds.X;
int sourceY = this.sourceBounds.Y; int sourceY = this.sourceBounds.Y;
@ -229,17 +229,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int destLeft = this.destinationBounds.Left; int destLeft = this.destinationBounds.Left;
int destRight = this.destinationBounds.Right; int destRight = this.destinationBounds.Right;
for (int y = rows.Min; y < rows.Max; y++) // Y coordinates of source points
Span<TPixel> sourceRow = this.source.GetPixelRowSpan((int)(((y - destY) * this.heightFactor) + sourceY));
Span<TPixel> targetRow = this.destination.GetPixelRowSpan(y);
for (int x = destLeft; x < destRight; x++)
{ {
// Y coordinates of source points // X coordinates of source points
Span<TPixel> sourceRow = this.source.GetPixelRowSpan((int)(((y - destY) * this.heightFactor) + sourceY)); targetRow[x] = sourceRow[(int)(((x - destX) * this.widthFactor) + sourceX)];
Span<TPixel> 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)];
}
} }
} }
} }

19
tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs

@ -21,12 +21,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
private SDImage bmpDrawing; private SDImage bmpDrawing;
private Image<Rgba32> bmpCore; private Image<Rgba32> 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] [GlobalSetup]
public void ReadImages() public void ReadImages()
{ {
if (this.bmpStream == null) 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<Rgba32>(this.bmpStream); this.bmpCore = Image.Load<Rgba32>(this.bmpStream);
this.bmpStream.Position = 0; this.bmpStream.Position = 0;
this.bmpDrawing = SDImage.FromStream(this.bmpStream); this.bmpDrawing = SDImage.FromStream(this.bmpStream);
@ -53,15 +62,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
[Benchmark(Description = "ImageSharp Gif")] [Benchmark(Description = "ImageSharp Gif")]
public void GifCore() 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()) using (var memoryStream = new MemoryStream())
{ {
this.bmpCore.SaveAsGif(memoryStream, options); this.bmpCore.SaveAsGif(memoryStream, this.encoder);
} }
} }
} }

5
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -27,11 +27,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private const float ProgressiveTolerance = 0.2F / 100; private const float ProgressiveTolerance = 0.2F / 100;
static JpegDecoderTests()
{
TestEnvironment.PrepareRemoteExecutor();
}
private static ImageComparer GetImageComparer<TPixel>(TestImageProvider<TPixel> provider) private static ImageComparer GetImageComparer<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {

12
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. // Licensed under the Apache License, Version 2.0.
using System.Text; using System.Text;
@ -19,25 +19,25 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Fact] [Fact]
public void ProfileResolverHasCorrectJFifMarker() public void ProfileResolverHasCorrectJFifMarker()
{ {
Assert.Equal(JFifMarker, ProfileResolver.JFifMarker); Assert.Equal(JFifMarker, ProfileResolver.JFifMarker.ToArray());
} }
[Fact] [Fact]
public void ProfileResolverHasCorrectExifMarker() public void ProfileResolverHasCorrectExifMarker()
{ {
Assert.Equal(ExifMarker, ProfileResolver.ExifMarker); Assert.Equal(ExifMarker, ProfileResolver.ExifMarker.ToArray());
} }
[Fact] [Fact]
public void ProfileResolverHasCorrectIccMarker() public void ProfileResolverHasCorrectIccMarker()
{ {
Assert.Equal(IccMarker, ProfileResolver.IccMarker); Assert.Equal(IccMarker, ProfileResolver.IccMarker.ToArray());
} }
[Fact] [Fact]
public void ProfileResolverHasCorrectAdobeMarker() public void ProfileResolverHasCorrectAdobeMarker()
{ {
Assert.Equal(AdobeMarker, ProfileResolver.AdobeMarker); Assert.Equal(AdobeMarker, ProfileResolver.AdobeMarker.ToArray());
} }
[Fact] [Fact]
@ -76,4 +76,4 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.False(ProfileResolver.IsProfile(AdobeMarker, ProfileResolver.IccMarker)); Assert.False(ProfileResolver.IsProfile(AdobeMarker, ProfileResolver.IccMarker));
} }
} }
} }

18
tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var operation = new TestRowIntervalOperation(RowAction); var operation = new TestRowIntervalOperation(RowAction);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
rectangle, rectangle,
in parallelSettings, in parallelSettings,
in operation); in operation);
@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var operation = new TestRowIntervalOperation(RowAction); var operation = new TestRowIntervalOperation(RowAction);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
rectangle, rectangle,
in parallelSettings, in parallelSettings,
in operation); in operation);
@ -157,7 +157,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var operation = new TestRowIntervalOperation<Vector4>(RowAction); var operation = new TestRowIntervalOperation<Vector4>(RowAction);
ParallelRowIterator.IterateRows<TestRowIntervalOperation<Vector4>, Vector4>( ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Vector4>, Vector4>(
rectangle, rectangle,
in parallelSettings, in parallelSettings,
in operation); in operation);
@ -195,7 +195,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var operation = new TestRowIntervalOperation<Vector4>(RowAction); var operation = new TestRowIntervalOperation<Vector4>(RowAction);
ParallelRowIterator.IterateRows<TestRowIntervalOperation<Vector4>, Vector4>( ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Vector4>, Vector4>(
rectangle, rectangle,
in parallelSettings, in parallelSettings,
in operation); in operation);
@ -249,7 +249,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var operation = new TestRowIntervalOperation(RowAction); var operation = new TestRowIntervalOperation(RowAction);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
rectangle, rectangle,
in parallelSettings, in parallelSettings,
in operation); in operation);
@ -291,7 +291,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var operation = new TestRowIntervalOperation<Vector4>(RowAction); var operation = new TestRowIntervalOperation<Vector4>(RowAction);
ParallelRowIterator.IterateRows<TestRowIntervalOperation<Vector4>, Vector4>( ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Vector4>, Vector4>(
rectangle, rectangle,
in parallelSettings, in parallelSettings,
in operation); in operation);
@ -355,7 +355,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var operation = new TestRowIntervalOperation(RowAction); var operation = new TestRowIntervalOperation(RowAction);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRowIntervals(
rect, rect,
settings, settings,
in operation); in operation);
@ -383,7 +383,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var operation = new TestRowIntervalOperation(RowAction); var operation = new TestRowIntervalOperation(RowAction);
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>( ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(
() => ParallelRowIterator.IterateRows(rect, in parallelSettings, in operation)); () => ParallelRowIterator.IterateRowIntervals(rect, in parallelSettings, in operation));
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
} }
@ -406,7 +406,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
var operation = new TestRowIntervalOperation<Rgba32>(RowAction); var operation = new TestRowIntervalOperation<Rgba32>(RowAction);
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>( ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(
() => ParallelRowIterator.IterateRows<TestRowIntervalOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in operation)); () => ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in operation));
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
} }

8
tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs

@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp.Tests
this.Collection.AddFrame((ImageFrame<Rgba32>)null); this.Collection.AddFrame((ImageFrame<Rgba32>)null);
}); });
Assert.StartsWith("Value cannot be null.", ex.Message); Assert.StartsWith("Parameter \"frame\" must be not null.", ex.Message);
} }
[Fact] [Fact]
@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Tests
this.Collection.AddFrame(data); this.Collection.AddFrame(data);
}); });
Assert.StartsWith("Value cannot be null.", ex.Message); Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message);
} }
[Fact] [Fact]
@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Tests
this.Collection.AddFrame(new Rgba32[0]); 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] [Fact]
@ -93,7 +93,7 @@ namespace SixLabors.ImageSharp.Tests
this.Collection.InsertFrame(1, null); this.Collection.InsertFrame(1, null);
}); });
Assert.StartsWith("Value cannot be null.", ex.Message); Assert.StartsWith("Parameter \"frame\" must be not null.", ex.Message);
} }
[Fact] [Fact]

4
tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs

@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Tests
this.Collection.AddFrame(null); this.Collection.AddFrame(null);
}); });
Assert.StartsWith("Value cannot be null.", ex.Message); Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message);
} }
[Fact] [Fact]
@ -114,7 +114,7 @@ namespace SixLabors.ImageSharp.Tests
this.Collection.InsertFrame(1, null); this.Collection.InsertFrame(1, null);
}); });
Assert.StartsWith("Value cannot be null.", ex.Message); Assert.StartsWith("Parameter \"source\" must be not null.", ex.Message);
} }
[Fact] [Fact]

5
tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs

@ -28,11 +28,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
/// </summary> /// </summary>
private static MemoryAllocatorFixture StaticFixture { get; } = new MemoryAllocatorFixture(); private static MemoryAllocatorFixture StaticFixture { get; } = new MemoryAllocatorFixture();
static ArrayPoolMemoryAllocatorTests()
{
TestEnvironment.PrepareRemoteExecutor();
}
public class BufferTests : BufferTestSuite public class BufferTests : BufferTestSuite
{ {
public BufferTests() public BufferTests()

2
tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs

@ -394,7 +394,7 @@ namespace SixLabors.ImageSharp.Tests
public void ProfileToByteArray() public void ProfileToByteArray()
{ {
// Arrange // Arrange
byte[] exifBytesWithoutExifCode = ExifConstants.LittleEndianByteOrderMarker; byte[] exifBytesWithoutExifCode = ExifConstants.LittleEndianByteOrderMarker.ToArray();
ExifProfile expectedProfile = CreateExifProfile(); ExifProfile expectedProfile = CreateExifProfile();
var expectedProfileTags = expectedProfile.Values.Select(x => x.Tag).ToList(); var expectedProfileTags = expectedProfile.Values.Select(x => x.Tag).ToList();

5
tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs

@ -19,11 +19,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution
{ {
public class BokehBlurTest public class BokehBlurTest
{ {
static BokehBlurTest()
{
TestEnvironment.PrepareRemoteExecutor();
}
private static readonly string Components10x2 = @" private static readonly string Components10x2 = @"
[[ 0.00451261+0.0165137j 0.02161237-0.00299122j 0.00387479-0.02682816j [[ 0.00451261+0.0165137j 0.02161237-0.00299122j 0.00387479-0.02682816j
-0.02752798-0.01788438j -0.03553877+0.0154543j -0.01428268+0.04224722j -0.02752798-0.01788438j -0.03553877+0.0154543j -0.01428268+0.04224722j

20
tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs

@ -71,10 +71,10 @@ namespace SixLabors.ImageSharp.Tests
foreach (ImageFrame<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration)) using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration))
using (QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) using (IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{ {
int index = this.GetTransparentIndex(quantized); 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<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration)) using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration))
using (QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) using (IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{ {
int index = this.GetTransparentIndex(quantized); int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.GetPixelSpan()[0]); Assert.Equal(index, quantized.GetPixelBufferSpan()[0]);
} }
} }
} }
} }
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized) private int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// 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 index = -1;
Rgba32 trans = default;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span; ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
for (int i = paletteSpan.Length - 1; i >= 0; i--) Span<Rgba32> colorSpan = stackalloc Rgba32[QuantizerConstants.MaxColors].Slice(0, paletteSpan.Length);
{
paletteSpan[i].ToRgba32(ref trans);
if (trans.Equals(default)) PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, paletteSpan, colorSpan);
for (int i = colorSpan.Length - 1; i >= 0; i--)
{
if (colorSpan[i].Equals(default))
{ {
index = i; index = i;
} }

30
tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs

@ -21,13 +21,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame; ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config); using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config);
using QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); using IndexedImageFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(1, result.Palette.Length); 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(Color.Black, (Color)result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]); Assert.Equal(0, result.GetPixelBufferSpan()[0]);
} }
[Fact] [Fact]
@ -40,13 +40,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame; ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config); using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config);
using QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); using IndexedImageFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(1, result.Palette.Length); 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(default, result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]); Assert.Equal(0, result.GetPixelBufferSpan()[0]);
} }
[Fact] [Fact]
@ -85,19 +85,19 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame; ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config); using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config);
using QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); using IndexedImageFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(256, result.Palette.Length); Assert.Equal(256, result.Palette.Length);
Assert.Equal(256, result.GetPixelSpan().Length); Assert.Equal(256, result.GetPixelBufferSpan().Length);
var actualImage = new Image<Rgba32>(1, 256); var actualImage = new Image<Rgba32>(1, 256);
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span; ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1; int paletteCount = paletteSpan.Length - 1;
for (int y = 0; y < actualImage.Height; y++) for (int y = 0; y < actualImage.Height; y++)
{ {
Span<Rgba32> row = actualImage.GetPixelRowSpan(y); Span<Rgba32> row = actualImage.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelSpan(); ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelBufferSpan();
int yy = y * actualImage.Width; int yy = y * actualImage.Width;
for (int x = 0; x < actualImage.Width; x++) for (int x = 0; x < actualImage.Width; x++)
@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<TPixel> frame = image.Frames.RootFrame; ImageFrame<TPixel> frame = image.Frames.RootFrame;
using IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(config); using IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(config);
using QuantizedFrame<TPixel> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); using IndexedImageFrame<TPixel> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(48, result.Palette.Length); Assert.Equal(48, result.Palette.Length);
} }
@ -152,17 +152,17 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame; ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using (IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config)) using (IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config))
using (QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) using (IndexedImageFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{ {
Assert.Equal(4 * 8, result.Palette.Length); Assert.Equal(4 * 8, result.Palette.Length);
Assert.Equal(256, result.GetPixelSpan().Length); Assert.Equal(256, result.GetPixelBufferSpan().Length);
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span; ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1; int paletteCount = paletteSpan.Length - 1;
for (int y = 0; y < actualImage.Height; y++) for (int y = 0; y < actualImage.Height; y++)
{ {
Span<Rgba32> row = actualImage.GetPixelRowSpan(y); Span<Rgba32> row = actualImage.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelSpan(); ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelBufferSpan();
int yy = y * actualImage.Width; int yy = y * actualImage.Width;
for (int x = 0; x < actualImage.Width; x++) for (int x = 0; x < actualImage.Width; x++)

7
tests/ImageSharp.Tests/TestUtilities/Attributes/ImageDataAttributeBase.cs

@ -18,6 +18,13 @@ namespace SixLabors.ImageSharp.Tests
protected readonly PixelTypes PixelTypes; 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();
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageDataAttributeBase"/> class. /// Initializes a new instance of the <see cref="ImageDataAttributeBase"/> class.
/// </summary> /// </summary>

32
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs

@ -34,6 +34,11 @@ namespace SixLabors.ImageSharp.Tests
private static readonly Lazy<string> NetCoreVersionLazy = new Lazy<string>(GetNetCoreVersion); private static readonly Lazy<string> NetCoreVersionLazy = new Lazy<string>(GetNetCoreVersion);
static TestEnvironment()
{
PrepareRemoteExecutor();
}
/// <summary> /// <summary>
/// Gets the .NET Core version, if running on .NET Core, otherwise returns an empty string. /// Gets the .NET Core version, if running on .NET Core, otherwise returns an empty string.
/// </summary> /// </summary>
@ -111,6 +116,13 @@ namespace SixLabors.ImageSharp.Tests
internal static bool IsFramework => string.IsNullOrEmpty(NetCoreVersion); internal static bool IsFramework => string.IsNullOrEmpty(NetCoreVersion);
/// <summary>
/// A dummy operation to enforce the execution of the static constructor.
/// </summary>
internal static void EnsureSharedInitializersDone()
{
}
/// <summary> /// <summary>
/// Creates the image output directory. /// Creates the image output directory.
/// </summary> /// </summary>
@ -141,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests
/// When running in 32 bits, enforces 32 bit execution of Microsoft.DotNet.RemoteExecutor.exe /// When running in 32 bits, enforces 32 bit execution of Microsoft.DotNet.RemoteExecutor.exe
/// with the help of CorFlags.exe found in Windows SDK. /// with the help of CorFlags.exe found in Windows SDK.
/// </summary> /// </summary>
internal static void PrepareRemoteExecutor() private static void PrepareRemoteExecutor()
{ {
if (!IsFramework) if (!IsFramework)
{ {
@ -153,12 +165,11 @@ namespace SixLabors.ImageSharp.Tests
if (File.Exists(remoteExecutorConfigPath)) if (File.Exists(remoteExecutorConfigPath))
{ {
// already prepared // Already initialized
return; return;
} }
string testProjectConfigPath = TestAssemblyFile.FullName + ".config"; string testProjectConfigPath = TestAssemblyFile.FullName + ".config";
File.Copy(testProjectConfigPath, remoteExecutorConfigPath); File.Copy(testProjectConfigPath, remoteExecutorConfigPath);
if (Is64BitProcess) if (Is64BitProcess)
@ -184,7 +195,17 @@ namespace SixLabors.ImageSharp.Tests
string remoteExecutorPath = Path.Combine(TestAssemblyFile.DirectoryName, "Microsoft.DotNet.RemoteExecutor.exe"); 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() 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}"); $@"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) static FileInfo Find(DirectoryInfo root, string name)
{ {
FileInfo fi = root.EnumerateFiles(name).FirstOrDefault(); FileInfo fi = root.EnumerateFiles(name).FirstOrDefault();

2
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -713,7 +713,7 @@ namespace SixLabors.ImageSharp.Tests
var operation = new RowOperation(configuration, sourceRectangle, source); var operation = new RowOperation(configuration, sourceRectangle, source);
ParallelRowIterator.IterateRows<RowOperation, Vector4>( ParallelRowIterator.IterateRowIntervals<RowOperation, Vector4>(
configuration, configuration,
sourceRectangle, sourceRectangle,
in operation); in operation);

2
tests/Images/External

@ -1 +1 @@
Subproject commit f8a76fd3a900b90c98df67ac896574383a4d09f3 Subproject commit 1fea1ceab89e87cc5f11376fa46164d3d27566c0
Loading…
Cancel
Save