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_assignment_preference = discard_variable:suggestion
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_for_built_in_types = never
csharp_style_var_when_type_is_apparent = true:warning
csharp_style_var_elsewhere = false:warning
csharp_prefer_simple_using_statement = false:silent

1
ImageSharp.sln

@ -334,6 +334,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Tests.ProfilingS
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2aa31a1f-142c-43f4-8687-09abca4b3a26}*SharedItemsImports = 5
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution

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)" />
</ItemGroup>
</Project>

20
src/Directory.Build.targets

@ -52,4 +52,24 @@
<!-- https://github.com/Microsoft/vstest/issues/411 -->
<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>

2
src/ImageSharp/Advanced/IRowIntervalOperation.cs

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

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

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

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

154
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -17,6 +17,135 @@ namespace SixLabors.ImageSharp.Advanced
/// </summary>
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>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary>
@ -25,11 +154,11 @@ namespace SixLabors.ImageSharp.Advanced
/// <param name="rectangle">The <see cref="Rectangle"/>.</param>
/// <param name="operation">The operation defining the iteration logic on a single <see cref="RowInterval"/>.</param>
[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
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows(rectangle, in parallelSettings, in operation);
IterateRowIntervals(rectangle, in parallelSettings, in operation);
}
/// <summary>
@ -39,7 +168,7 @@ namespace SixLabors.ImageSharp.Advanced
/// <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 <see cref="RowInterval"/>.</param>
public static void IterateRows<T>(
public static void IterateRowIntervals<T>(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
in T operation)
@ -65,8 +194,7 @@ namespace SixLabors.ImageSharp.Advanced
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var info = new IterationParameters(top, bottom, verticalStep);
var wrappingOperation = new RowIntervalOperationWrapper<T>(in info, in operation);
var wrappingOperation = new RowIntervalOperationWrapper<T>(top, bottom, verticalStep, in operation);
Parallel.For(
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="rectangle">The <see cref="Rectangle"/>.</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 TBuffer : unmanaged
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows<T, TBuffer>(rectangle, in parallelSettings, in operation);
IterateRowIntervals<T, TBuffer>(rectangle, in parallelSettings, in operation);
}
/// <summary>
@ -101,7 +229,7 @@ namespace SixLabors.ImageSharp.Advanced
/// <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 <see cref="RowInterval"/>.</param>
public static void IterateRows<T, TBuffer>(
public static void IterateRowIntervals<T, TBuffer>(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
in T operation)
@ -123,18 +251,16 @@ namespace SixLabors.ImageSharp.Advanced
if (numOfSteps == 1)
{
var rows = new RowInterval(top, bottom);
using (IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(width))
{
Unsafe.AsRef(operation).Invoke(in rows, buffer.Memory.Span);
}
using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(width);
Unsafe.AsRef(operation).Invoke(in rows, buffer.Memory.Span);
return;
}
int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var info = new IterationParameters(top, bottom, verticalStep, width);
var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(in info, allocator, in operation);
var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(top, bottom, verticalStep, width, allocator, in operation);
Parallel.For(
0,

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

@ -248,7 +248,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion
}
/// <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>
/// <param name="source">The span to the source 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);
}
}
}
}

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

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

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

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

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)
where TPixel : unmanaged, IPixel<TPixel>
{
using IFrameQuantizer<TPixel> quantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using QuantizedFrame<TPixel> quantized = quantizer.QuantizeFrame(image, image.Bounds());
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image, image.Bounds());
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
var color = default(Rgba32);
@ -360,7 +360,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
for (int y = image.Height - 1; y >= 0; y--)
{
ReadOnlySpan<byte> pixelSpan = quantized.GetRowSpan(y);
ReadOnlySpan<byte> pixelSpan = quantized.GetPixelRowSpan(y);
stream.Write(pixelSpan);
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.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Text;
@ -21,11 +22,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
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>
/// The extension block introducer <value>!</value>.
/// </summary>
@ -51,11 +47,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
public const string NetscapeApplicationIdentification = "NETSCAPE2.0";
/// <summary>
/// The ASCII encoded application identification bytes.
/// </summary>
internal static readonly byte[] NetscapeApplicationIdentificationBytes = Encoding.ASCII.GetBytes(NetscapeApplicationIdentification);
/// <summary>
/// The Netscape looping application sub block size.
/// </summary>
@ -110,5 +101,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// The collection of file extensions that equate to a Gif.
/// </summary>
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 SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif
@ -17,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Gets or sets the quantizer for reducing the color count.
/// Defaults to the <see cref="OctreeQuantizer"/>
/// </summary>
public IQuantizer Quantizer { get; set; } = new OctreeQuantizer();
public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree;
/// <summary>
/// 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;
// Quantize the image returning a palette.
QuantizedFrame<TPixel> quantized;
IndexedImageFrame<TPixel> quantized;
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
}
// Get the number of bits.
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length);
// Write the header.
this.WriteHeader(stream);
@ -119,15 +119,20 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
// Clean up.
quantized?.Dispose();
quantized.Dispose();
// TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer);
}
private void EncodeGlobal<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>
{
// 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++)
{
ImageFrame<TPixel> frame = image.Frames[i];
@ -142,22 +147,27 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
using (var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette))
using (QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()))
if (!pixelMapSet)
{
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>
{
ImageFrame<TPixel> previousFrame = 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;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
if (quantized is null)
@ -173,27 +183,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
MaxColors = frameMetadata.ColorTableLength
};
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options))
{
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options);
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
else
{
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
}
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteGraphicalControlExtension(frameMetadata, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream);
quantized?.Dispose();
quantized.Dispose();
quantized = null; // So next frame can regenerate it
previousFrame = frame;
previousMeta = frameMetadata;
@ -208,25 +214,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <returns>
/// The <see cref="int"/>.
/// </returns>
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized)
private int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized)
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 length = quantized.Palette.Length;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
using (IMemoryOwner<Rgba32> rgbaBuffer = this.memoryAllocator.Allocate<Rgba32>(length))
{
Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan();
ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan);
using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteSpan.Length);
Span<Rgba32> rgbaSpan = rgbaOwner.GetSpan();
PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
for (int i = quantized.Palette.Length - 1; i >= 0; i--)
for (int i = rgbaSpan.Length - 1; i >= 0; i--)
{
if (Unsafe.Add(ref rgbaSpanRef, i).Equals(default))
{
if (Unsafe.Add(ref paletteRef, i).Equals(default))
{
index = i;
}
index = i;
}
}
@ -238,7 +242,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="stream">The stream to write to.</param>
[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>
/// Writes the logical screen descriptor to the stream.
@ -326,8 +330,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
return;
}
foreach (string comment in metadata.Comments)
for (var i = 0; i < metadata.Comments.Count; i++)
{
string comment = metadata.Comments[i];
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.CommentLabel;
stream.Write(this.buffer, 0, 2);
@ -335,7 +340,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan();
int idx = 0;
for (; idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; idx += GifConstants.MaxCommentSubBlockLength)
for (;
idx <= comment.Length - GifConstants.MaxCommentSubBlockLength;
idx += GifConstants.MaxCommentSubBlockLength)
{
WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength);
}
@ -391,7 +398,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="extension">The extension to write to the stream.</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[1] = extension.Label;
@ -437,37 +445,33 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</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>
{
// The maximum number of colors for the bit depth
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3;
int pixelCount = image.Palette.Length;
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf<Rgb24>();
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
{
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
image.Palette.Span,
colorTable.GetSpan(),
pixelCount);
stream.Write(colorTable.Array, 0, colorTableLength);
}
using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength, AllocationOptions.Clean);
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
image.Palette.Span,
colorTable.GetSpan(),
image.Palette.Length);
stream.Write(colorTable.Array, 0, colorTableLength);
}
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <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>
private void WriteImageData<TPixel>(QuantizedFrame<TPixel> image, Stream stream)
private void WriteImageData<TPixel>(IndexedImageFrame<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth))
{
encoder.Encode(image.GetPixelSpan(), stream);
}
using var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth);
encoder.Encode(image.GetPixelBufferSpan(), stream);
}
}
}

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

@ -274,7 +274,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
ent = this.NextPixel(indexedPixels);
// TODO: PERF: It looks likt hshift could be calculated once statically.
// TODO: PERF: It looks like hshift could be calculated once statically.
hshift = 0;
for (fcode = this.hsize; fcode < 65536; fcode *= 2)
{

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

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

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.
using System;
using System.Text;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
@ -12,24 +11,38 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
internal static class ProfileResolver
{
/// <summary>
/// Describes the JFIF specific markers.
/// Gets the JFIF specific markers.
/// </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>
/// Describes the ICC specific markers.
/// Gets the ICC specific markers.
/// </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>
/// Describes the EXIF specific markers.
/// Gets the EXIF specific markers.
/// </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>
/// 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>
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>
/// 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);
}
}
}
}

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

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

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Text;
@ -36,21 +37,6 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
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>
/// The header bytes as a big-endian coded ulong.
/// </summary>
@ -77,5 +63,20 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The minimum length of a keyword in a text chunk is 1 byte.
/// </summary>
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;
PngMetadata pngMetadata = metadata.GetPngMetadata();
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);
stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length);
stream.Write(PngConstants.HeaderBytes);
this.WriteHeaderChunk(stream);
this.WritePaletteChunk(stream, quantized);
@ -371,7 +371,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</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>
{
switch (this.options.ColorType)
@ -380,12 +380,11 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.bitDepth < 8)
{
PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetPixelRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
int stride = this.currentScanline.Length();
quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan());
quantized.GetPixelRowSpan(row).CopyTo(this.currentScanline.GetSpan());
}
break;
@ -440,7 +439,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param>
/// <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>
{
this.CollectPixelBytes(rowSpan, quantized, row);
@ -546,59 +545,54 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</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>
{
if (quantized == null)
if (quantized is null)
{
return;
}
// Grab the palette and write it to the stream.
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
int paletteLength = Math.Min(palette.Length, 256);
int colorTableLength = paletteLength * 3;
bool anyAlpha = false;
int paletteLength = palette.Length;
int colorTableLength = paletteLength * Unsafe.SizeOf<Rgb24>();
bool hasAlpha = false;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength))
{
ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan());
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
ReadOnlySpan<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);
using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
using IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength);
byte alpha = rgba.A;
ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, Rgb24>(colorTable.GetSpan()));
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
Unsafe.Add(ref colorTableRef, offset) = rgba.R;
Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G;
Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B;
// Bulk convert our palette to RGBA to allow assignment to tables.
using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteLength);
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)
{
alpha = byte.MaxValue;
}
// Loop, assign, and extract alpha values from the palette.
for (int i = 0; i < paletteLength; i++)
{
Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, i);
byte alpha = rgba.A;
anyAlpha = anyAlpha || alpha < byte.MaxValue;
Unsafe.Add(ref alphaTableRef, i) = alpha;
}
Unsafe.Add(ref colorTableRef, i) = rgba.Rgb;
if (alpha > this.options.Threshold)
{
alpha = byte.MaxValue;
}
this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength);
hasAlpha = hasAlpha || alpha < byte.MaxValue;
Unsafe.Add(ref alphaTableRef, i) = alpha;
}
// Write the transparency data
if (anyAlpha)
{
this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength);
}
this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength);
// Write the transparency data
if (hasAlpha)
{
this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength);
}
}
@ -783,7 +777,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The image.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</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>
{
byte[] buffer;
@ -881,7 +875,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</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>
{
int bytesPerScanline = this.CalculateScanlineLength(this.width);
@ -960,7 +954,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="quantized">The quantized.</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>
{
int width = quantized.Width;
@ -987,7 +981,7 @@ namespace SixLabors.ImageSharp.Formats.Png
row += Adam7.RowIncrement[pass])
{
// collect data
ReadOnlySpan<byte> srcRow = quantized.GetRowSpan(row);
ReadOnlySpan<byte> srcRow = quantized.GetPixelRowSpan(row);
for (int col = startCol, i = 0;
col < width;
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>
/// <param name="options">The options.</param>
/// <param name="image">The image.</param>
public static QuantizedFrame<TPixel> CreateQuantizedFrame<TPixel>(
public static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
PngEncoderOptions options,
Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Png
public static byte CalculateBitDepth<TPixel>(
PngEncoderOptions options,
Image<TPixel> image,
QuantizedFrame<TPixel> quantizedFrame)
IndexedImageFrame<TPixel> quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
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;
// The lengths of the bit length codes are sent in order of decreasing
// probability, to avoid transmitting the lengths for unused bit length codes.
private static readonly int[] BitLengthOrder = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };
private static readonly byte[] Bit4Reverse = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 };
private static readonly short[] StaticLCodes;
private static readonly byte[] StaticLLength;
private static readonly short[] StaticDCodes;
@ -128,6 +122,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer;
}
/// <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>
/// Gets the pending buffer to use.
/// </summary>
@ -158,6 +159,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5);
this.Pending.WriteBits(this.distTree.NumCodes - 1, 5);
this.Pending.WriteBits(blTreeCodes - 4, 4);
for (int rank = 0; rank < blTreeCodes; rank++)
{
this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3);
@ -250,6 +252,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.blTree.BuildTree();
int blTreeCodes = 4;
for (int i = 18; i > blTreeCodes; i--)
{
if (this.blTree.Length[BitLengthOrder[i]] > 0)
@ -363,10 +366,30 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
[MethodImpl(InliningOptions.ShortMethod)]
public static short BitReverse(int toReverse)
{
return (short)(Bit4Reverse[toReverse & 0xF] << 12
| Bit4Reverse[(toReverse >> 4) & 0xF] << 8
| Bit4Reverse[(toReverse >> 8) & 0xF] << 4
| Bit4Reverse[toReverse >> 12]);
/* Use unsafe offsetting and manually validate the input index to reduce the
* total number of conditional branches. There are two main cases to test here:
* 1. In the first 3, the input value (or some combination of it) is combined
* with & 0xF, which results in a maximum value of 0xF no matter what the
* input value was. That is 15, which is always in range for the target span.
* As a result, no input validation is needed at all in this case.
* 2. There are two cases where the input value might cause an invalid access:
* when it is either negative, or greater than 15 << 12. We can test both
* conditions in a single pass by casting the input value to uint and right
* shifting it by 12, which also preserves the sign. If it is a negative
* value (2-complement), the test will fail as the uint cast will result
* in a much larger value. If the value was simply too high, the test will
* fail as expected. We can't simply check whether the value is lower than
* 15 << 12, because higher values are acceptable in the first 3 accesses.
* Doing this reduces the total number of index checks from 4 down to just 1. */
int toReverseRightShiftBy12 = toReverse >> 12;
Guard.MustBeLessThanOrEqualTo<uint>((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/>

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 operation = new RowIntervalOperation<TPixel2>(this, target, configuration);
ParallelRowIterator.IterateRows(
ParallelRowIterator.IterateRowIntervals(
configuration,
this.Bounds(),
in operation);

3
src/ImageSharp/ImageSharp.csproj

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

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

@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Memory
/// <inheritdoc />
public override Span<T> GetSpan()
{
if (this.Data == null)
if (this.Data is null)
{
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.
/// The last buffer is allowed to be smaller.
/// </summary>
public int BufferLength { get; }
int BufferLength { get; }
/// <summary>
/// Gets the aggregate number of elements in the group.
/// </summary>
public long TotalLength { get; }
long TotalLength { get; }
/// <summary>
/// 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.
using System;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{
internal static class ExifConstants
{
public static readonly byte[] LittleEndianByteOrderMarker =
public static ReadOnlySpan<byte> LittleEndianByteOrderMarker => new byte[]
{
(byte)'I',
(byte)'I',
@ -13,7 +15,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
0x00,
};
public static readonly byte[] BigEndianByteOrderMarker =
public static ReadOnlySpan<byte> BigEndianByteOrderMarker => new byte[]
{
(byte)'M',
(byte)'M',
@ -21,4 +23,4 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
0x2A
};
}
}
}

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

@ -77,7 +77,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
int i = 0;
// The byte order marker for little-endian, followed by the number 42 and a 0
ExifConstants.LittleEndianByteOrderMarker.AsSpan().CopyTo(result.AsSpan(start: i));
ExifConstants.LittleEndianByteOrderMarker.CopyTo(result.AsSpan(start: i));
i += ExifConstants.LittleEndianByteOrderMarker.Length;
uint ifdOffset = ((uint)i - startIndex) + 4U;

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

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

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

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

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

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

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)
{
// Preliminary gamma highlight pass
var gammaOperation = new ApplyGammaExposureRowIntervalOperation(this.SourceRectangle, source.PixelBuffer, this.Configuration, this.gamma);
ParallelRowIterator.IterateRows<ApplyGammaExposureRowIntervalOperation, Vector4>(
var gammaOperation = new ApplyGammaExposureRowOperation(this.SourceRectangle, source.PixelBuffer, this.Configuration, this.gamma);
ParallelRowIterator.IterateRows<ApplyGammaExposureRowOperation, Vector4>(
this.Configuration,
this.SourceRectangle,
in gammaOperation);
@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
float inverseGamma = 1 / this.gamma;
// Apply the inverse gamma exposure pass, and write the final pixel data
var operation = new ApplyInverseGammaExposureRowIntervalOperation(this.SourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, inverseGamma);
var operation = new ApplyInverseGammaExposureRowOperation(this.SourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, inverseGamma);
ParallelRowIterator.IterateRows(
this.Configuration,
this.SourceRectangle,
@ -120,14 +120,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
Vector4 parameters = Unsafe.Add(ref paramsRef, i);
// Compute the vertical 1D convolution
var verticalOperation = new ApplyVerticalConvolutionRowIntervalOperation(sourceRectangle, firstPassBuffer, source.PixelBuffer, kernel);
var verticalOperation = new ApplyVerticalConvolutionRowOperation(sourceRectangle, firstPassBuffer, source.PixelBuffer, kernel);
ParallelRowIterator.IterateRows(
configuration,
sourceRectangle,
in verticalOperation);
// Compute the horizontal 1D convolutions and accumulate the partial results on the target buffer
var horizontalOperation = new ApplyHorizontalConvolutionRowIntervalOperation(sourceRectangle, processingBuffer, firstPassBuffer, kernel, parameters.Z, parameters.W);
var horizontalOperation = new ApplyHorizontalConvolutionRowOperation(sourceRectangle, processingBuffer, firstPassBuffer, kernel, parameters.Z, parameters.W);
ParallelRowIterator.IterateRows(
configuration,
sourceRectangle,
@ -138,7 +138,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary>
/// A <see langword="struct"/> implementing the vertical convolution logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary>
private readonly struct ApplyVerticalConvolutionRowIntervalOperation : IRowIntervalOperation
private readonly struct ApplyVerticalConvolutionRowOperation : IRowOperation
{
private readonly Rectangle bounds;
private readonly Buffer2D<ComplexVector4> targetValues;
@ -148,7 +148,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)]
public ApplyVerticalConvolutionRowIntervalOperation(
public ApplyVerticalConvolutionRowOperation(
Rectangle bounds,
Buffer2D<ComplexVector4> targetValues,
Buffer2D<TPixel> sourcePixels,
@ -164,16 +164,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
[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++)
{
Buffer2DUtils.Convolve4(this.kernel, this.sourcePixels, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX);
}
for (int x = 0; x < this.bounds.Width; x++)
{
Buffer2DUtils.Convolve4(this.kernel, this.sourcePixels, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX);
}
}
}
@ -181,7 +178,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary>
/// A <see langword="struct"/> implementing the horizontal convolution logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary>
private readonly struct ApplyHorizontalConvolutionRowIntervalOperation : IRowIntervalOperation
private readonly struct ApplyHorizontalConvolutionRowOperation : IRowOperation
{
private readonly Rectangle bounds;
private readonly Buffer2D<Vector4> targetValues;
@ -193,7 +190,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)]
public ApplyHorizontalConvolutionRowIntervalOperation(
public ApplyHorizontalConvolutionRowOperation(
Rectangle bounds,
Buffer2D<Vector4> targetValues,
Buffer2D<ComplexVector4> sourceValues,
@ -213,16 +210,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
[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++)
{
Buffer2DUtils.Convolve4AndAccumulatePartials(this.kernel, this.sourceValues, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX, this.z, this.w);
}
for (int x = 0; x < this.bounds.Width; x++)
{
Buffer2DUtils.Convolve4AndAccumulatePartials(this.kernel, this.sourceValues, targetRowSpan, y, x, this.bounds.Y, this.maxY, this.bounds.X, this.maxX, this.z, this.w);
}
}
}
@ -230,7 +224,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary>
/// A <see langword="struct"/> implementing the gamma exposure logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary>
private readonly struct ApplyGammaExposureRowIntervalOperation : IRowIntervalOperation<Vector4>
private readonly struct ApplyGammaExposureRowOperation : IRowOperation<Vector4>
{
private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> targetPixels;
@ -238,7 +232,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly float gamma;
[MethodImpl(InliningOptions.ShortMethod)]
public ApplyGammaExposureRowIntervalOperation(
public ApplyGammaExposureRowOperation(
Rectangle bounds,
Buffer2D<TPixel> targetPixels,
Configuration configuration,
@ -252,31 +246,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
[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);
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++)
{
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);
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);
}
}
/// <summary>
/// A <see langword="struct"/> implementing the inverse gamma exposure logic for <see cref="BokehBlurProcessor{T}"/>.
/// </summary>
private readonly struct ApplyInverseGammaExposureRowIntervalOperation : IRowIntervalOperation
private readonly struct ApplyInverseGammaExposureRowOperation : IRowOperation
{
private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> targetPixels;
@ -285,7 +276,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly float inverseGamma;
[MethodImpl(InliningOptions.ShortMethod)]
public ApplyInverseGammaExposureRowIntervalOperation(
public ApplyInverseGammaExposureRowOperation(
Rectangle bounds,
Buffer2D<TPixel> targetPixels,
Buffer2D<Vector4> sourceValues,
@ -301,28 +292,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
Vector4 low = Vector4.Zero;
var high = new Vector4(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
for (int y = rows.Min; y < rows.Max; y++)
Span<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);
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++)
{
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);
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);
}
}
}

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

@ -65,9 +65,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
source.CopyTo(targetPixels);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
var operation = new RowIntervalOperation(interest, targetPixels, source.PixelBuffer, this.KernelY, this.KernelX, this.Configuration, this.PreserveAlpha);
var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, this.KernelY, this.KernelX, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration,
interest,
in operation);
@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="Convolution2DProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
private readonly struct RowOperation : IRowOperation<Vector4>
{
private readonly Rectangle bounds;
private readonly int maxY;
@ -91,7 +91,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly bool preserveAlpha;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public RowOperation(
Rectangle bounds,
Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> sourcePixels,
@ -113,52 +113,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
[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);
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);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span);
if (this.preserveAlpha)
for (int x = 0; x < this.bounds.Width; x++)
{
for (int x = 0; x < this.bounds.Width; x++)
{
DenseMatrixUtils.Convolve2D3(
in this.kernelY,
in this.kernelX,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
DenseMatrixUtils.Convolve2D3(
in this.kernelY,
in this.kernelX,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
else
}
else
{
for (int x = 0; x < this.bounds.Width; x++)
{
for (int x = 0; x < this.bounds.Width; x++)
{
DenseMatrixUtils.Convolve2D4(
in this.kernelY,
in this.kernelX,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
DenseMatrixUtils.Convolve2D4(
in this.kernelY,
in this.kernelX,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
PixelOperations<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());
// Horizontal convolution
var horizontalOperation = new RowIntervalOperation(interest, firstPassPixels, source.PixelBuffer, this.KernelX, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
var horizontalOperation = new RowOperation(interest, firstPassPixels, source.PixelBuffer, this.KernelX, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration,
interest,
in horizontalOperation);
// Vertical convolution
var verticalOperation = new RowIntervalOperation(interest, source.PixelBuffer, firstPassPixels, this.KernelY, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
var verticalOperation = new RowOperation(interest, source.PixelBuffer, firstPassPixels, this.KernelY, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration,
interest,
in verticalOperation);
@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="Convolution2PassProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
private readonly struct RowOperation : IRowOperation<Vector4>
{
private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> targetPixels;
@ -91,7 +91,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly bool preserveAlpha;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public RowOperation(
Rectangle bounds,
Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> sourcePixels,
@ -109,53 +109,50 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
[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);
int maxY = this.bounds.Bottom - 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,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
maxY,
this.bounds.X,
maxX);
}
DenseMatrixUtils.Convolve3(
in this.kernel,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
maxY,
this.bounds.X,
maxX);
}
else
}
else
{
for (int x = 0; x < this.bounds.Width; x++)
{
for (int x = 0; x < this.bounds.Width; x++)
{
DenseMatrixUtils.Convolve4(
in this.kernel,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
maxY,
this.bounds.X,
maxX);
}
DenseMatrixUtils.Convolve4(
in this.kernel,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
maxY,
this.bounds.X,
maxX);
}
PixelOperations<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);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
var operation = new RowIntervalOperation(interest, targetPixels, source.PixelBuffer, this.KernelXY, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, this.KernelXY, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration,
interest,
in operation);
@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="ConvolutionProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
private readonly struct RowOperation : IRowOperation<Vector4>
{
private readonly Rectangle bounds;
private readonly int maxY;
@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly bool preserveAlpha;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public RowOperation(
Rectangle bounds,
Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> sourcePixels,
@ -100,50 +100,47 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
[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);
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,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
DenseMatrixUtils.Convolve3(
in this.kernel,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
else
}
else
{
for (int x = 0; x < this.bounds.Width; x++)
{
for (int x = 0; x < this.bounds.Width; x++)
{
DenseMatrixUtils.Convolve4(
in this.kernel,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
DenseMatrixUtils.Convolve4(
in this.kernel,
this.sourcePixels,
ref spanRef,
y,
x,
this.bounds.Y,
this.maxY,
this.bounds.X,
this.maxX);
}
PixelOperations<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);
}
var operation = new RowIntervalOperation(source.PixelBuffer, pass.PixelBuffer, interest);
var operation = new RowOperation(source.PixelBuffer, pass.PixelBuffer, interest);
ParallelRowIterator.IterateRows(
this.Configuration,
interest,
@ -89,7 +89,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="EdgeDetectorCompassProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation
private readonly struct RowOperation : IRowOperation
{
private readonly Buffer2D<TPixel> targetPixels;
private readonly Buffer2D<TPixel> passPixels;
@ -97,7 +97,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public RowOperation(
Buffer2D<TPixel> targetPixels,
Buffer2D<TPixel> passPixels,
Rectangle bounds)
@ -110,23 +110,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
for (int y = rows.Min; y < rows.Max; y++)
{
ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(this.passPixels.GetRowSpan(y));
ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(this.targetPixels.GetRowSpan(y));
ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(this.passPixels.GetRowSpan(y));
ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(this.targetPixels.GetRowSpan(y));
for (int x = this.minX; x < this.maxX; x++)
{
// Grab the max components of the two pixels
ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, x);
ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, x);
for (int x = this.minX; x < this.maxX; x++)
{
// Grab the max components of the two pixels
ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, x);
ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, x);
var pixelValue = Vector4.Max(currentPassPixel.ToVector4(), currentTargetPixel.ToVector4());
var pixelValue = Vector4.Max(currentPassPixel.ToVector4(), currentTargetPixel.ToVector4());
currentTargetPixel.FromVector4(pixelValue);
}
currentTargetPixel.FromVector4(pixelValue);
}
}
}

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

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

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

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -19,15 +18,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The frame quantizer.</param>
/// <param name="palette">The quantized palette.</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>
void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Memory<byte> output,
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>;
@ -36,18 +33,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Transforms the image frame applying a dither matrix.
/// This method should be treated as destructive, altering the input pixels.
/// </summary>
/// <typeparam name="TPaletteDitherImageProcessor">The type of palette dithering processor.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The quantized palette.</param>
/// <param name="processor">The palette dithering processor.</param>
/// <param name="source">The source image.</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<TPixel>(
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
in TPaletteDitherImageProcessor processor,
ImageFrame<TPixel> source,
Rectangle bounds,
float scale)
Rectangle bounds)
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<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>
/// An ordered dithering matrix with equal sides of arbitrary length
/// </content>
public readonly partial struct OrderedDither : IDither
public readonly partial struct OrderedDither
{
/// <summary>
/// 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.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -105,21 +105,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Memory<byte> output,
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
var ditherOperation = new QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel>(
var ditherOperation = new QuantizeDitherRowOperation<TFrameQuantizer, TPixel>(
ref quantizer,
in Unsafe.AsRef(this),
source,
output,
bounds,
palette,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
destination,
bounds);
ParallelRowIterator.IterateRows(
quantizer.Configuration,
@ -129,24 +126,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPixel>(
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
in TPaletteDitherImageProcessor processor,
ImageFrame<TPixel> source,
Rectangle bounds,
float scale)
Rectangle bounds)
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
var ditherOperation = new PaletteDitherRowIntervalOperation<TPixel>(
var ditherOperation = new PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel>(
in processor,
in Unsafe.AsRef(this),
source,
bounds,
palette,
scale,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
bounds);
ParallelRowIterator.IterateRows(
configuration,
processor.Configuration,
bounds,
in ditherOperation);
}
@ -200,102 +194,87 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public override int GetHashCode()
=> HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY);
private readonly struct QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation
private readonly struct QuantizeDitherRowOperation<TFrameQuantizer, TPixel> : IRowOperation
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TFrameQuantizer quantizer;
private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output;
private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizeDitherRowIntervalOperation(
public QuantizeDitherRowOperation(
ref TFrameQuantizer quantizer,
in OrderedDither dither,
ImageFrame<TPixel> source,
Memory<byte> output,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette,
int bitDepth)
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
{
this.quantizer = quantizer;
this.dither = dither;
this.source = source;
this.output = output;
this.destination = destination;
this.bounds = bounds;
this.palette = palette;
this.bitDepth = bitDepth;
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
Span<byte> outputSpan = this.output.Span;
int width = this.bounds.Width;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
float scale = this.quantizer.Options.DitherScale;
for (int y = rows.Min; y < rows.Max; y++)
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
int rowStart = (y - offsetY) * width;
// TODO: This can be a bulk operation.
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, scale);
outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _);
}
TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale);
Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _);
}
}
}
private readonly struct PaletteDitherRowIntervalOperation<TPixel> : IRowIntervalOperation
private readonly struct PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel> : IRowOperation
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TPaletteDitherImageProcessor processor;
private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source;
private readonly Rectangle bounds;
private readonly EuclideanPixelMap<TPixel> pixelMap;
private readonly float scale;
private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteDitherRowIntervalOperation(
public PaletteDitherRowOperation(
in TPaletteDitherImageProcessor processor,
in OrderedDither dither,
ImageFrame<TPixel> source,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette,
float scale,
int bitDepth)
Rectangle bounds)
{
this.processor = processor;
this.dither = dither;
this.source = source;
this.bounds = bounds;
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
this.scale = scale;
this.bitDepth = bitDepth;
this.scale = processor.DitherScale;
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
for (int y = rows.Min; y < rows.Max; y++)
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, this.scale);
this.pixelMap.GetClosestColor(dithered, out TPixel transformed);
row[x] = transformed;
}
ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale);
sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered);
}
}
}

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

@ -3,7 +3,9 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
@ -14,11 +16,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
internal sealed class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly int paletteLength;
private readonly DitherProcessor ditherProcessor;
private readonly IDither dither;
private readonly float ditherScale;
private readonly ReadOnlyMemory<Color> sourcePalette;
private IMemoryOwner<TPixel> palette;
private IMemoryOwner<TPixel> paletteOwner;
private bool isDisposed;
/// <summary>
@ -31,37 +31,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.paletteLength = definition.Palette.Span.Length;
this.dither = definition.Dither;
this.ditherScale = definition.DitherScale;
this.sourcePalette = definition.Palette;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
ReadOnlySpan<Color> sourcePalette = definition.Palette.Span;
this.paletteOwner = this.Configuration.MemoryAllocator.Allocate<TPixel>(sourcePalette.Length);
Color.ToPixel(this.Configuration, sourcePalette, this.paletteOwner.Memory.Span);
this.dither.ApplyPaletteDither(
this.ditherProcessor = new DitherProcessor(
this.Configuration,
this.palette.Memory,
source,
interest,
this.ditherScale);
this.paletteOwner.Memory,
definition.DitherScale);
}
/// <inheritdoc/>
protected override void BeforeFrameApply(ImageFrame<TPixel> source)
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
// Lazy init palettes:
if (this.palette is null)
{
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);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest);
}
/// <inheritdoc/>
@ -72,15 +58,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
return;
}
this.isDisposed = true;
if (disposing)
{
this.palette?.Dispose();
this.paletteOwner.Dispose();
}
this.palette = null;
this.isDisposed = true;
this.paletteOwner = null;
base.Dispose(disposing);
}
/// <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.");
}
var operation = new RowIntervalOperation(source, targetImage, blender, configuration, minX, width, locationY, targetX, this.Opacity);
var operation = new RowOperation(source, targetImage, blender, configuration, minX, width, locationY, targetX, this.Opacity);
ParallelRowIterator.IterateRows(
configuration,
workingRect,
@ -109,7 +109,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
/// <summary>
/// A <see langword="struct"/> implementing the draw logic for <see cref="DrawImageProcessor{TPixelBg,TPixelFg}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation
private readonly struct RowOperation : IRowOperation
{
private readonly ImageFrame<TPixelBg> sourceFrame;
private readonly Image<TPixelFg> targetImage;
@ -122,7 +122,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
private readonly float opacity;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public RowOperation(
ImageFrame<TPixelBg> sourceFrame,
Image<TPixelFg> targetImage,
PixelBlender<TPixelBg> blender,
@ -146,14 +146,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
/// <inheritdoc/>
[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);
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity);
}
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);
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);
var operation = new RowIntervalOperation(this.SourceRectangle, targetPixels, source, this.Configuration, brushSize >> 1, this.definition.Levels);
ParallelRowIterator.IterateRows(
ParallelRowIterator.IterateRowIntervals(
this.Configuration,
this.SourceRectangle,
in operation);

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)
{
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,
interest,
in operation);
@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
/// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="PixelRowDelegateProcessor{TPixel,TDelegate}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
private readonly struct RowOperation : IRowOperation<Vector4>
{
private readonly int startX;
private readonly ImageFrame<TPixel> source;
@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
private readonly TDelegate rowProcessor;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public RowOperation(
int startX,
ImageFrame<TPixel> source,
Configuration configuration,
@ -86,18 +86,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
/// <inheritdoc/>
[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
Unsafe.AsRef(this.rowProcessor).Invoke(span, new Point(this.startX, y));
// Run the user defined pixel shader to the current row of pixels
Unsafe.AsRef(this.rowProcessor).Invoke(span, new Point(this.startX, y));
PixelOperations<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)
{
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,
interest,
in operation);
@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
/// <summary>
/// A <see langword="struct"/> implementing the convolution logic for <see cref="FilterProcessor{TPixel}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
private readonly struct RowOperation : IRowOperation<Vector4>
{
private readonly int startX;
private readonly ImageFrame<TPixel> source;
@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
private readonly Configuration configuration;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public RowOperation(
int startX,
ImageFrame<TPixel> source,
ColorMatrix matrix,
@ -69,17 +69,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
/// <inheritdoc/>
[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);
ParallelRowIterator.IterateRows(
ParallelRowIterator.IterateRowIntervals(
this.Configuration,
new Rectangle(0, 0, sourceWidth, tileYStartPositions.Count),
in operation);
@ -522,7 +522,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
this.luminanceLevels,
source);
ParallelRowIterator.IterateRows(
ParallelRowIterator.IterateRowIntervals(
this.configuration,
new Rectangle(0, 0, this.sourceWidth, this.tileYStartPositions.Count),
in operation);

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

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

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);
rowColors.GetSpan().Fill(glowColor);
var operation = new RowIntervalOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source);
ParallelRowIterator.IterateRows<RowIntervalOperation, float>(
var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source);
ParallelRowIterator.IterateRows<RowOperation, float>(
configuration,
interest,
in operation);
}
private readonly struct RowIntervalOperation : IRowIntervalOperation<float>
private readonly struct RowOperation : IRowOperation<float>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
@ -74,7 +74,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public RowOperation(
Configuration configuration,
Rectangle bounds,
IMemoryOwner<TPixel> colors,
@ -95,27 +95,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
}
[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();
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.configuration,
destination,
destination,
colorSpan,
span);
}
this.blender.Blend(
this.configuration,
destination,
destination,
colorSpan,
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);
rowColors.GetSpan().Fill(vignetteColor);
var operation = new RowIntervalOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source);
ParallelRowIterator.IterateRows<RowIntervalOperation, float>(
var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source);
ParallelRowIterator.IterateRows<RowOperation, float>(
configuration,
interest,
in operation);
}
private readonly struct RowIntervalOperation : IRowIntervalOperation<float>
private readonly struct RowOperation : IRowOperation<float>
{
private readonly Configuration configuration;
private readonly Rectangle bounds;
@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public RowOperation(
Configuration configuration,
Rectangle bounds,
IMemoryOwner<TPixel> colors,
@ -103,27 +103,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
}
[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();
for (int y = rows.Min; y < rows.Max; y++)
for (int i = 0; i < this.bounds.Width; i++)
{
for (int i = 0; i < this.bounds.Width; i++)
{
float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y));
span[i] = (this.blendPercent * (.9F * (distance / this.maxDistance))).Clamp(0, 1);
}
Span<TPixel> destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
this.blender.Blend(
this.configuration,
destination,
destination,
colorSpan,
span);
float distance = Vector2.Distance(this.center, new Vector2(i + this.bounds.X, y));
span[i] = (this.blendPercent * (.9F * (distance / this.maxDistance))).Clamp(0, 1);
}
Span<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.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Gets the closest color to the supplied color based upon the Eucladean distance.
/// TODO: Expose this somehow.
/// Gets the closest color to the supplied color based upon the Euclidean distance.
/// </summary>
/// <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>
{
private readonly ConcurrentDictionary<int, Vector4> vectorCache;
private readonly Vector4[] vectorCache;
private readonly ConcurrentDictionary<TPixel, int> distanceCache;
/// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct.
/// </summary>
/// <param name="configuration">The configuration.</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;
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span;
this.vectorCache = new ConcurrentDictionary<int, Vector4>();
this.distanceCache = new ConcurrentDictionary<TPixel, int>();
this.vectorCache = new Vector4[palette.Length];
for (int i = 0; i < paletteSpan.Length; i++)
{
this.vectorCache[i] = paletteSpan[i].ToScaledVector4();
}
// Use the same rules across all target frameworks.
this.distanceCache = new ConcurrentDictionary<TPixel, int>(Environment.ProcessorCount, 31);
PixelOperations<TPixel>.Instance.ToVector4(configuration, this.Palette.Span, this.vectorCache);
}
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette { get; }
/// <inheritdoc/>
public override bool Equals(object obj)
=> obj is EuclideanPixelMap<TPixel> map && this.Equals(map);
/// <inheritdoc/>
public bool Equals(EuclideanPixelMap<TPixel> other)
=> this.Palette.Equals(other.Palette);
/// <summary>
/// Gets the color palette of this <see cref="EuclideanPixelMap{TPixel}"/>.
/// The palette memory is owned by the palette source that created it.
/// </summary>
public ReadOnlyMemory<TPixel> Palette
{
[MethodImpl(InliningOptions.ShortMethod)]
get;
}
/// <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)]
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
if (this.distanceCache.TryGetValue(color, out int index))
if (!this.distanceCache.TryGetValue(color, out int index))
{
match = paletteSpan[index];
return index;
return this.GetClosestColorSlow(color, ref paletteRef, out match);
}
return this.GetClosestColorSlow(color, paletteSpan, out match);
match = Unsafe.Add(ref paletteRef, index);
return index;
}
/// <inheritdoc/>
public override int GetHashCode()
=> this.vectorCache.GetHashCode();
[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.
int index = 0;
float leastDistance = float.MaxValue;
Vector4 vector = color.ToScaledVector4();
for (int i = 0; i < palette.Length; i++)
var vector = color.ToVector4();
ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference<Vector4>(this.vectorCache);
for (int i = 0; i < this.Palette.Length; i++)
{
Vector4 candidate = this.vectorCache[i];
Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i);
float distance = Vector4.DistanceSquared(vector, candidate);
// Less than... assign.
// If it's an exact match, exit the loop
if (distance == 0)
{
index = i;
break;
}
if (distance < leastDistance)
{
// Less than... assign.
index = i;
leastDistance = distance;
// And if it's an exact match, exit the loop
if (distance == 0)
{
break;
}
}
}
// Now I have the index, pop it into the cache for next time
this.distanceCache[color] = index;
match = palette[index];
match = Unsafe.Add(ref paletteRef, index);
return index;
}
}

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
{
/// <summary>
/// Contains extension methods for frame quantizers.
/// Contains utility methods for <see cref="IFrameQuantizer{TPixel}"/> instances.
/// </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>
/// Quantizes an image frame and return the resulting output pixels.
/// </summary>
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</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="bounds">The bounds within the frame to quantize.</param>
/// <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>
public static QuantizedFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>(
public static IndexedImageFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
Rectangle bounds)
@ -37,35 +55,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
var interest = Rectangle.Intersect(source.Bounds(), bounds);
// Collect the palette. Required before the second pass runs.
ReadOnlyMemory<TPixel> palette = quantizer.BuildPalette(source, interest);
MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator;
quantizer.BuildPalette(source, interest);
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette);
Memory<byte> output = quantizedFrame.GetWritablePixelMemory();
var destination = new IndexedImageFrame<TPixel>(
quantizer.Configuration,
interest.Width,
interest.Height,
quantizer.Palette);
if (quantizer.Options.Dither is null)
{
SecondPass(ref quantizer, source, interest, output, palette);
SecondPass(ref quantizer, source, destination, interest);
}
else
{
// We clone the image as we don't want to alter the original via error diffusion based dithering.
using (ImageFrame<TPixel> clone = source.Clone())
{
SecondPass(ref quantizer, clone, interest, output, palette);
}
using ImageFrame<TPixel> clone = source.Clone();
SecondPass(ref quantizer, clone, destination, interest);
}
return quantizedFrame;
return destination;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static void SecondPass<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
Rectangle bounds,
Memory<byte> output,
ReadOnlyMemory<TPixel> palette)
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
@ -73,8 +90,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (dither is null)
{
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(quantizer, source, output, bounds, palette);
ParallelRowIterator.IterateRows(
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(
ref quantizer,
source,
destination,
bounds);
ParallelRowIterator.IterateRowIntervals(
quantizer.Configuration,
bounds,
in operation);
@ -82,7 +104,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return;
}
dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds);
dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds);
}
private readonly struct RowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation
@ -91,43 +113,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
private readonly TFrameQuantizer quantizer;
private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output;
private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
in TFrameQuantizer quantizer,
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
Memory<byte> output,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette)
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
{
this.quantizer = quantizer;
this.source = source;
this.output = output;
this.destination = destination;
this.bounds = bounds;
this.palette = palette;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
Span<byte> outputSpan = this.output.Span;
int width = this.bounds.Width;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
int rowStart = (y - offsetY) * width;
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
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++)
{
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; }
/// <summary>
/// Quantizes an image frame and return the resulting output pixels.
/// Gets the quantized color palette.
/// </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="QuantizedFrame{TPixel}"/> representing a quantized version of the source frame pixels.
/// </returns>
QuantizedFrame<TPixel> QuantizeFrame(
ImageFrame<TPixel> source,
Rectangle bounds);
/// <exception cref="InvalidOperationException">
/// The palette has not been built via <see cref="BuildPalette(ImageFrame{TPixel}, Rectangle)"/>.
/// </exception>
ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Builds the quantized palette from the given image frame and bounds.
/// </summary>
/// <param name="source">The source image frame.</param>
/// <param name="bounds">The region of interest bounds.</param>
/// <returns>The <see cref="ReadOnlyMemory{TPixel}"/> palette.</returns>
ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds);
void 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>
/// 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>
/// <param name="color">The color to match.</param>
/// <param name="palette">The output color palette.</param>
/// <param name="match">The matched color.</param>
/// <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.
// 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.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -20,10 +20,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public struct OctreeFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly int colors;
private readonly int maxColors;
private readonly Octree octree;
private IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering;
private bool isDisposed;
/// <summary>
/// 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.Options = options;
this.colors = this.Options.MaxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8));
this.maxColors = this.Options.MaxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.maxColors).Clamp(1, 8));
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.palette = default;
this.pixelMap = default;
this.isDithering = !(this.Options.Dither is null);
this.isDisposed = false;
}
/// <inheritdoc/>
@ -52,13 +58,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public QuantizerOptions Options { get; }
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
public ReadOnlyMemory<TPixel> Palette
{
get
{
FrameQuantizerUtilities.CheckPaletteState(in this.palette);
return this.palette;
}
}
/// <inheritdoc/>
[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);
Span<Rgba32> bufferSpan = buffer.GetSpan();
@ -78,32 +89,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
}
TPixel[] palette = this.octree.Palletize(this.colors);
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
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/>
[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
// so cannot tell the difference between a fully transparent
// pixel and a black one.
if (!this.isDithering && !color.Equals(default))
if (this.isDithering || color.Equals(default))
{
var index = (byte)this.octree.GetPaletteIndex(color);
match = palette[index];
return index;
return (byte)this.pixelMap.GetClosestColor(color, out match);
}
return (byte)this.pixelMap.GetClosestColor(color, out match);
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.Palette.Span);
var index = (byte)this.octree.GetPaletteIndex(color);
match = Unsafe.Add(ref paletteRef, index);
return index;
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.isDisposed = true;
this.paletteOwner.Dispose();
this.paletteOwner = null;
}
}
/// <summary>
@ -111,21 +139,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
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>
/// The root of the Octree
/// </summary>
@ -162,6 +175,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
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>
/// Gets or sets the number of leaves in the tree
/// </summary>
@ -216,26 +244,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Convert the nodes in the Octree to a palette with a maximum of colorCount colors
/// </summary>
/// <param name="palette">The palette to fill.</param>
/// <param name="colorCount">The maximum number of colors</param>
/// <returns>
/// An <see cref="List{TPixel}"/> with the palletized colors
/// </returns>
/// <param name="paletteIndex">The palette index, used to calculate the final size of the palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public TPixel[] Palletize(int colorCount)
public void Palletize(Span<TPixel> palette, int colorCount, ref int paletteIndex)
{
while (this.Leaves > colorCount - 1)
{
this.Reduce();
}
// Now palletize the nodes
var palette = new TPixel[colorCount];
int paletteIndex = 0;
this.root.ConstructPalette(palette, ref paletteIndex);
// And return the palette
return palette;
}
/// <summary>
@ -437,12 +457,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="palette">The palette</param>
/// <param name="index">The current palette index</param>
[MethodImpl(InliningOptions.ColdPath)]
public void ConstructPalette(TPixel[] palette, ref int index)
public void ConstructPalette(Span<TPixel> palette, ref int index)
{
if (this.leaf)
{
// Set the color of the palette entry
var vector = Vector3.Clamp(new Vector3(this.red, this.green, this.blue) / this.pixelCount, Vector3.Zero, new Vector3(255));
var vector = Vector3.Clamp(
new Vector3(this.red, this.green, this.blue) / this.pixelCount,
Vector3.Zero,
new Vector3(255));
TPixel pixel = default;
pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue));
palette[index] = pixel;
@ -513,11 +537,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetColorIndex(ref Rgba32 color, int level)
{
DebugGuard.MustBeLessThan(level, Mask.Length, nameof(level));
int shift = 7 - level;
byte mask = Mask[level];
ref byte maskRef = ref MemoryMarshal.GetReference(Mask);
byte mask = Unsafe.Add(ref maskRef, level);
return ((color.R & mask) >> shift)
| ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2));
| ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2));
}
/// <summary>

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

@ -11,12 +11,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class OctreeQuantizer : IQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>.
/// </summary>
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>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ReadOnlyMemory<TPixel> palette;
private readonly EuclideanPixelMap<TPixel> pixelMap;
/// <summary>
@ -23,18 +22,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <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="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)]
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(options, nameof(options));
this.Configuration = configuration;
this.Options = options;
this.palette = colors;
this.pixelMap = new EuclideanPixelMap<TPixel>(colors);
this.pixelMap = pixelMap;
}
/// <inheritdoc/>
@ -43,19 +43,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
public QuantizerOptions Options { get; }
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette => this.pixelMap.Palette;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
public readonly IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
=> this.palette;
public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{
}
/// <inheritdoc/>
[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);
/// <inheritdoc/>

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

@ -11,12 +11,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class PaletteQuantizer : IQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
private readonly ReadOnlyMemory<Color> colorPalette;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The color palette.</param>
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.NotNull(options, nameof(options));
this.Palette = palette;
this.colorPalette = palette;
this.Options = options;
}
/// <summary>
/// Gets the color palette.
/// </summary>
public ReadOnlyMemory<Color> Palette { get; }
/// <inheritdoc />
public QuantizerOptions Options { get; }
@ -53,11 +51,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
Guard.NotNull(options, nameof(options));
int length = Math.Min(this.Palette.Span.Length, options.MaxColors);
// The palette quantizer can reuse the same pixel map across multiple frames
// since the palette is unchanging. This allows a reduction of memory usage across
// multi frame gifs using a global palette.
int length = Math.Min(this.colorPalette.Length, options.MaxColors);
var palette = new TPixel[length];
Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, options, palette);
Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan());
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;
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);
ParallelRowIterator.IterateRows(
ParallelRowIterator.IterateRowIntervals(
configuration,
interest,
in operation);
@ -52,13 +52,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source;
private readonly QuantizedFrame<TPixel> quantized;
private readonly IndexedImageFrame<TPixel> quantized;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Rectangle bounds,
ImageFrame<TPixel> source,
QuantizedFrame<TPixel> quantized)
IndexedImageFrame<TPixel> quantized)
{
this.bounds = bounds;
this.source = source;
@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelSpan();
ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelBufferSpan();
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span;
int offsetY = this.bounds.Top;
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>
public class WebSafePaletteQuantizer : PaletteQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary>
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.
/// </summary>
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>
public class WernerPaletteQuantizer : PaletteQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary>
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary>
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.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -65,30 +66,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
/// <summary>
/// Color moments.
/// </summary>
private IMemoryOwner<Moment> moments;
/// <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 IMemoryOwner<Moment> momentsOwner;
private IMemoryOwner<byte> tagsOwner;
private IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private int maxColors;
private readonly Box[] colorCube;
private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering;
private bool isDisposed;
/// <summary>
@ -104,11 +89,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration;
this.Options = options;
this.maxColors = this.Options.MaxColors;
this.memoryAllocator = this.Configuration.MemoryAllocator;
this.moments = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tag = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.colors = this.Options.MaxColors;
this.colorCube = new Box[this.colors];
this.momentsOwner = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tagsOwner = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.paletteOwner = this.memoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.palette = default;
this.colorCube = new Box[this.maxColors];
this.isDisposed = false;
this.pixelMap = default;
this.isDithering = this.isDithering = !(this.Options.Dither is null);
@ -121,21 +108,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public QuantizerOptions Options { get; }
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
public ReadOnlyMemory<TPixel> Palette
{
get
{
FrameQuantizerUtilities.CheckPaletteState(in this.palette);
return this.palette;
}
}
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{
this.Build3DHistogram(source, bounds);
this.Get3DMoments(this.memoryAllocator);
this.BuildCube();
var palette = new TPixel[this.colors];
ReadOnlySpan<Moment> momentsSpan = this.moments.GetSpan();
for (int k = 0; k < this.colors; k++)
ReadOnlySpan<Moment> momentsSpan = this.momentsOwner.GetSpan();
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
for (int k = 0; k < this.maxColors; k++)
{
this.Mark(ref this.colorCube[k], (byte)k);
@ -143,50 +134,57 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (moment.Weight > 0)
{
ref TPixel color = ref palette[k];
ref TPixel color = ref paletteSpan[k];
color.FromScaledVector4(moment.Normalize());
}
}
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
return palette;
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, this.maxColors);
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
this.palette = result;
}
/// <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;
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);
}
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/>
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>
@ -364,7 +362,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="bounds">The bounds within the source image to quantize.</param>
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
using IMemoryOwner<Rgba32> buffer = this.memoryAllocator.Allocate<Rgba32>(bounds.Width);
@ -392,13 +390,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
/// </summary>
/// <param name="memoryAllocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator memoryAllocator)
/// <param name="allocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator allocator)
{
using IMemoryOwner<Moment> volume = memoryAllocator.Allocate<Moment>(IndexCount * IndexAlphaCount);
using IMemoryOwner<Moment> area = memoryAllocator.Allocate<Moment>(IndexAlphaCount);
using IMemoryOwner<Moment> volume = allocator.Allocate<Moment>(IndexCount * 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> areaSpan = area.GetSpan();
int baseIndex = GetPaletteIndex(1, 0, 0, 0);
@ -440,7 +438,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns>
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 variance =
@ -481,7 +479,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns>
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);
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>
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);
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>
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++)
{
@ -634,7 +632,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
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];
cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0;
@ -643,7 +643,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
int next = 0;
for (int i = 1; i < this.colors; i++)
for (int i = 1; i < this.maxColors; i++)
{
ref Box nextCube = ref this.colorCube[next];
ref Box currentCube = ref this.colorCube[i];
@ -672,7 +672,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (temp <= 0D)
{
this.colors = i + 1;
this.maxColors = i + 1;
break;
}
}

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

@ -10,12 +10,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class WuQuantizer : IQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>.
/// </summary>
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.FromConfiguration(this.Configuration).MultiplyMinimumPixelsPerTask(4);
var operation = new RowIntervalOperation(bounds, source, destination);
var operation = new RowOperation(bounds, source, destination);
ParallelRowIterator.IterateRows(
bounds,
@ -62,20 +62,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary>
/// A <see langword="struct"/> implementing the processor logic for <see cref="CropProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation
private readonly struct RowOperation : IRowOperation
{
private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
/// <summary>
/// Initializes a new instance of the <see cref="RowIntervalOperation"/> struct.
/// Initializes a new instance of the <see cref="RowOperation"/> struct.
/// </summary>
/// <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="destination">The destination <see cref="Image{TPixel}"/> for the current instance.</param>
[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.source = source;
@ -84,14 +84,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <inheritdoc/>
[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);
sourceRow.Slice(0, this.bounds.Width).CopyTo(targetRow);
}
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.Left);
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);
}
private readonly struct NNAffineOperation : IRowIntervalOperation
private readonly struct NNAffineOperation : IRowOperation
{
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
@ -133,28 +133,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
for (int y = rows.Min; y < rows.Max; y++)
Span<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);
int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y);
if (this.bounds.Contains(px, py))
{
destRow[x] = this.source[px, py];
}
destRow[x] = this.source[px, py];
}
}
}
}
private readonly struct AffineOperation<TResampler> : IRowIntervalOperation<Vector4>
private readonly struct AffineOperation<TResampler> : IRowOperation<Vector4>
where TResampler : struct, IResampler
{
private readonly Configuration configuration;
@ -193,41 +190,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
public void Invoke(int y, Span<Vector4> span)
{
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));
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
this.destination.GetPixelRowSpan(y),
span);
for (int x = 0; x < this.maxX; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), this.matrix);
LinearTransformUtilities.Convolve(
in this.sampler,
point,
sourceBuffer,
span,
x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
}
ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y));
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
for (int x = 0; x < this.maxX; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), this.matrix);
LinearTransformUtilities.Convolve(
in this.sampler,
point,
sourceBuffer,
span,
this.destination.GetPixelRowSpan(y));
x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
}
PixelOperations<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.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -76,28 +75,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param>
private void FlipY(ImageFrame<TPixel> source, Configuration configuration)
{
var operation = new RowIntervalOperation(source);
var operation = new RowOperation(source);
ParallelRowIterator.IterateRows(
configuration,
source.Bounds(),
in operation);
}
private readonly struct RowIntervalOperation : IRowIntervalOperation
private readonly struct RowOperation : IRowOperation
{
private readonly ImageFrame<TPixel> source;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(ImageFrame<TPixel> source) => this.source = source;
public RowOperation(ImageFrame<TPixel> source) => this.source = source;
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
this.source.GetPixelRowSpan(y).Reverse();
}
}
public void Invoke(int y) => this.source.GetPixelRowSpan(y).Reverse();
}
}
}

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

@ -111,7 +111,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
in operation);
}
private readonly struct NNProjectiveOperation : IRowIntervalOperation
private readonly struct NNProjectiveOperation : IRowOperation
{
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
@ -133,28 +133,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
for (int y = rows.Min; y < rows.Max; y++)
Span<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);
int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y);
if (this.bounds.Contains(px, py))
{
destRow[x] = this.source[px, py];
}
destRow[x] = this.source[px, py];
}
}
}
}
private readonly struct ProjectiveOperation<TResampler> : IRowIntervalOperation<Vector4>
private readonly struct ProjectiveOperation<TResampler> : IRowOperation<Vector4>
where TResampler : struct, IResampler
{
private readonly Configuration configuration;
@ -193,41 +190,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
public void Invoke(int y, Span<Vector4> span)
{
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));
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
this.destination.GetPixelRowSpan(y),
span);
for (int x = 0; x < this.maxX; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix);
LinearTransformUtilities.Convolve(
in this.sampler,
point,
sourceBuffer,
span,
x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
}
ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y));
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
for (int x = 0; x < this.maxX; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix);
LinearTransformUtilities.Convolve(
in this.sampler,
point,
sourceBuffer,
span,
this.destination.GetPixelRowSpan(y));
x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
}
PixelOperations<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>
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(
configuration,
source.Bounds(),
@ -147,7 +147,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private void Rotate270(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{
var operation = new Rotate270RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source, destination);
ParallelRowIterator.IterateRows(
ParallelRowIterator.IterateRowIntervals(
configuration,
source.Bounds(),
in operation);
@ -161,14 +161,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param>
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(
configuration,
source.Bounds(),
in operation);
}
private readonly struct Rotate180RowIntervalOperation : IRowIntervalOperation
private readonly struct Rotate180RowOperation : IRowOperation
{
private readonly int width;
private readonly int height;
@ -176,7 +176,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public Rotate180RowIntervalOperation(
public Rotate180RowOperation(
int width,
int height,
ImageFrame<TPixel> source,
@ -189,17 +189,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<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++)
{
targetRow[this.width - x - 1] = sourceRow[x];
}
for (int x = 0; x < this.width; x++)
{
targetRow[this.width - x - 1] = sourceRow[x];
}
}
}
@ -248,7 +245,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
}
private readonly struct Rotate90RowIntervalOperation : IRowIntervalOperation
private readonly struct Rotate90RowOperation : IRowOperation
{
private readonly Rectangle bounds;
private readonly int width;
@ -257,7 +254,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public Rotate90RowIntervalOperation(
public Rotate90RowOperation(
Rectangle bounds,
int width,
int height,
@ -272,18 +269,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
for (int y = rows.Min; y < rows.Max; y++)
Span<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);
int newX = this.height - y - 1;
for (int x = 0; x < this.width; x++)
if (this.bounds.Contains(newX, x))
{
if (this.bounds.Contains(newX, x))
{
this.destination[newX, x] = sourceRow[x];
}
this.destination[newX, x] = sourceRow[x];
}
}
}

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

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

@ -21,12 +21,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
private SDImage bmpDrawing;
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]
public void ReadImages()
{
if (this.bmpStream == null)
{
this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Bmp.Car));
this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage));
this.bmpCore = Image.Load<Rgba32>(this.bmpStream);
this.bmpStream.Position = 0;
this.bmpDrawing = SDImage.FromStream(this.bmpStream);
@ -53,15 +62,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
[Benchmark(Description = "ImageSharp Gif")]
public void GifCore()
{
// Try to get as close to System.Drawing's output as possible
var options = new GifEncoder
{
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 })
};
using (var memoryStream = new MemoryStream())
{
this.bmpCore.SaveAsGif(memoryStream, options);
this.bmpCore.SaveAsGif(memoryStream, this.encoder);
}
}
}

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;
static JpegDecoderTests()
{
TestEnvironment.PrepareRemoteExecutor();
}
private static ImageComparer GetImageComparer<TPixel>(TestImageProvider<TPixel> provider)
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.
using System.Text;
@ -19,25 +19,25 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Fact]
public void ProfileResolverHasCorrectJFifMarker()
{
Assert.Equal(JFifMarker, ProfileResolver.JFifMarker);
Assert.Equal(JFifMarker, ProfileResolver.JFifMarker.ToArray());
}
[Fact]
public void ProfileResolverHasCorrectExifMarker()
{
Assert.Equal(ExifMarker, ProfileResolver.ExifMarker);
Assert.Equal(ExifMarker, ProfileResolver.ExifMarker.ToArray());
}
[Fact]
public void ProfileResolverHasCorrectIccMarker()
{
Assert.Equal(IccMarker, ProfileResolver.IccMarker);
Assert.Equal(IccMarker, ProfileResolver.IccMarker.ToArray());
}
[Fact]
public void ProfileResolverHasCorrectAdobeMarker()
{
Assert.Equal(AdobeMarker, ProfileResolver.AdobeMarker);
Assert.Equal(AdobeMarker, ProfileResolver.AdobeMarker.ToArray());
}
[Fact]
@ -76,4 +76,4 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.False(ProfileResolver.IsProfile(AdobeMarker, ProfileResolver.IccMarker));
}
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -71,10 +71,10 @@ namespace SixLabors.ImageSharp.Tests
foreach (ImageFrame<TPixel> frame in image.Frames)
{
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);
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)
{
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);
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>
{
// Transparent pixels are much more likely to be found at the end of a palette
int index = -1;
Rgba32 trans = default;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
for (int i = paletteSpan.Length - 1; i >= 0; i--)
{
paletteSpan[i].ToRgba32(ref trans);
Span<Rgba32> colorSpan = stackalloc Rgba32[QuantizerConstants.MaxColors].Slice(0, paletteSpan.Length);
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;
}

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

@ -21,13 +21,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
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.GetPixelSpan().Length);
Assert.Equal(1, result.GetPixelBufferSpan().Length);
Assert.Equal(Color.Black, (Color)result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
Assert.Equal(0, result.GetPixelBufferSpan()[0]);
}
[Fact]
@ -40,13 +40,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
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.GetPixelSpan().Length);
Assert.Equal(1, result.GetPixelBufferSpan().Length);
Assert.Equal(default, result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
Assert.Equal(0, result.GetPixelBufferSpan()[0]);
}
[Fact]
@ -85,19 +85,19 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
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.GetPixelSpan().Length);
Assert.Equal(256, result.GetPixelBufferSpan().Length);
var actualImage = new Image<Rgba32>(1, 256);
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++)
{
Span<Rgba32> row = actualImage.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelSpan();
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelBufferSpan();
int yy = y * actualImage.Width;
for (int x = 0; x < actualImage.Width; x++)
@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<TPixel> frame = image.Frames.RootFrame;
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);
}
@ -152,17 +152,17 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
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(256, result.GetPixelSpan().Length);
Assert.Equal(256, result.GetPixelBufferSpan().Length);
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++)
{
Span<Rgba32> row = actualImage.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelSpan();
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelBufferSpan();
int yy = y * actualImage.Width;
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;
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>
/// Initializes a new instance of the <see cref="ImageDataAttributeBase"/> class.
/// </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);
static TestEnvironment()
{
PrepareRemoteExecutor();
}
/// <summary>
/// Gets the .NET Core version, if running on .NET Core, otherwise returns an empty string.
/// </summary>
@ -111,6 +116,13 @@ namespace SixLabors.ImageSharp.Tests
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>
/// Creates the image output directory.
/// </summary>
@ -141,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests
/// When running in 32 bits, enforces 32 bit execution of Microsoft.DotNet.RemoteExecutor.exe
/// with the help of CorFlags.exe found in Windows SDK.
/// </summary>
internal static void PrepareRemoteExecutor()
private static void PrepareRemoteExecutor()
{
if (!IsFramework)
{
@ -153,12 +165,11 @@ namespace SixLabors.ImageSharp.Tests
if (File.Exists(remoteExecutorConfigPath))
{
// already prepared
// Already initialized
return;
}
string testProjectConfigPath = TestAssemblyFile.FullName + ".config";
File.Copy(testProjectConfigPath, remoteExecutorConfigPath);
if (Is64BitProcess)
@ -184,7 +195,17 @@ namespace SixLabors.ImageSharp.Tests
string remoteExecutorPath = Path.Combine(TestAssemblyFile.DirectoryName, "Microsoft.DotNet.RemoteExecutor.exe");
string args = $"{remoteExecutorPath} /32Bit+ /Force";
string remoteExecutorTmpPath = $"{remoteExecutorPath}._tmp";
if (File.Exists(remoteExecutorTmpPath))
{
// Already initialized
return;
}
File.Copy(remoteExecutorPath, remoteExecutorTmpPath);
string args = $"{remoteExecutorTmpPath} /32Bit+ /Force";
var si = new ProcessStartInfo()
{
@ -206,6 +227,9 @@ namespace SixLabors.ImageSharp.Tests
$@"Failed to run {si.FileName} {si.Arguments}:\n STDOUT: {standardOutput}\n STDERR: {standardError}");
}
File.Delete(remoteExecutorPath);
File.Copy(remoteExecutorTmpPath, remoteExecutorPath);
static FileInfo Find(DirectoryInfo root, string name)
{
FileInfo fi = root.EnumerateFiles(name).FirstOrDefault();

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

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

2
tests/Images/External

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