Browse Source

Merge branch 'master' into colorspace-transforms

af/merge-core
James Jackson-South 8 years ago
parent
commit
8c0e2e9d46
  1. 9
      README.md
  2. 178
      src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs
  3. 223
      src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs
  4. 34
      src/ImageSharp/Common/Extensions/EncoderExtensions.cs
  5. 61
      src/ImageSharp/Common/Helpers/ParallelFor.cs
  6. 71
      src/ImageSharp/Common/ParallelUtils/ParallelExecutionSettings.cs
  7. 146
      src/ImageSharp/Common/ParallelUtils/ParallelHelper.cs
  8. 3
      src/ImageSharp/Configuration.cs
  9. 20
      src/ImageSharp/Formats/Bmp/BmpMetaData.cs
  10. 23
      src/ImageSharp/Formats/Gif/GifFrameMetaData.cs
  11. 23
      src/ImageSharp/Formats/Gif/GifMetaData.cs
  12. 18
      src/ImageSharp/Formats/Jpeg/JpegMetaData.cs
  13. 56
      src/ImageSharp/Formats/Png/Adam7.cs
  14. 107
      src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs
  15. 14
      src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
  16. 6
      src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
  17. 6
      src/ImageSharp/Formats/Png/Filters/SubFilter.cs
  18. 2
      src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
  19. 2
      src/ImageSharp/Formats/Png/PngBitDepth.cs
  20. 12
      src/ImageSharp/Formats/Png/PngConstants.cs
  21. 916
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  22. 2
      src/ImageSharp/Formats/Png/PngEncoder.cs
  23. 250
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  24. 68
      src/ImageSharp/Formats/Png/PngHeader.cs
  25. 23
      src/ImageSharp/Formats/Png/PngMetaData.cs
  26. 600
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  27. 48
      src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs
  28. 31
      src/ImageSharp/IDeepCloneable.cs
  29. 2
      src/ImageSharp/Image.Decode.cs
  30. 18
      src/ImageSharp/ImageFrameCollection.cs
  31. 104
      src/ImageSharp/ImageFrame{TPixel}.cs
  32. 3
      src/ImageSharp/ImageSharp.csproj.DotSettings
  33. 88
      src/ImageSharp/Image{TPixel}.cs
  34. 20
      src/ImageSharp/Memory/Buffer2DExtensions.cs
  35. 45
      src/ImageSharp/Memory/RowInterval.cs
  36. 20
      src/ImageSharp/MetaData/ImageFrameMetaData.cs
  37. 38
      src/ImageSharp/MetaData/ImageMetaData.cs
  38. 27
      src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs
  39. 13
      src/ImageSharp/MetaData/Profiles/Exif/ExifReader.cs
  40. 69
      src/ImageSharp/MetaData/Profiles/Exif/ExifValue.cs
  41. 36
      src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs
  42. 43
      src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs
  43. 119
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs
  44. 61
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs
  45. 94
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs
  46. 42
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs
  47. 110
      src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs
  48. 28
      src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs
  49. 38
      src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs
  50. 59
      src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs
  51. 61
      src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs
  52. 12
      src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs
  53. 190
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs
  54. 36
      src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs
  55. 64
      src/ImageSharp/Processing/Processors/Transforms/FlipProcessor.cs
  56. 220
      src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs
  57. 152
      src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs
  58. 86
      src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs
  59. 115
      tests/ImageSharp.Benchmarks/Codecs/CopyPixels.cs
  60. 38
      tests/ImageSharp.Benchmarks/Samplers/Glow.cs
  61. 2
      tests/ImageSharp.Tests/ConfigurationTests.cs
  62. 255
      tests/ImageSharp.Tests/Drawing/FillPatternTests.cs
  63. 44
      tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs
  64. 22
      tests/ImageSharp.Tests/Formats/Bmp/BmpMetaDataTests.cs
  65. 32
      tests/ImageSharp.Tests/Formats/Gif/GifFrameMetaDataTests.cs
  66. 32
      tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs
  67. 33
      tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs
  68. 22
      tests/ImageSharp.Tests/Formats/Jpg/JpegMetaDataTests.cs
  69. 160
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  70. 31
      tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs
  71. 338
      tests/ImageSharp.Tests/Helpers/ParallelHelperTests.cs
  72. 38
      tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs
  73. 2
      tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs
  74. 4
      tests/ImageSharp.Tests/Image/ImageTests.cs
  75. 8
      tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs
  76. 34
      tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs
  77. 28
      tests/ImageSharp.Tests/MetaData/Profiles/Exif/ExifProfileTests.cs
  78. 2
      tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs
  79. 24
      tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs
  80. 19
      tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs
  81. 23
      tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs
  82. 2
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs
  83. 2
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs
  84. 6
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
  85. 19
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  86. 8
      tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs
  87. 2
      tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs
  88. 2
      tests/Images/External

9
README.md

@ -70,6 +70,9 @@ Our API is designed to be simple to consume. Here's an example of the code requi
On platforms supporting netstandard 1.3+
```csharp
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
// Image.Load(string path) is a shortcut for our default type.
// Other pixel formats use Image.Load<TPixel>(string path))
using (Image<Rgba32> image = Image.Load("foo.jpg"))
@ -83,6 +86,9 @@ using (Image<Rgba32> image = Image.Load("foo.jpg"))
On netstandard 1.1 - 1.2
```csharp
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
// Image.Load(Stream stream) is a shortcut for our default type.
// Other pixel formats use Image.Load<TPixel>(Stream stream))
using (FileStream stream = File.OpenRead("foo.jpg"))
@ -99,6 +105,9 @@ using (Image<Rgba32> image = Image.Load(stream))
Setting individual pixel values can be performed as follows:
```csharp
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
// Individual pixels
using (Image<Rgba32> image = new Image<Rgba32>(400, 400))
{

178
src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawImageProcessor.cs

@ -1,95 +1,101 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
/// <summary>
/// Combines two images together by blending the pixels.
/// </summary>
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
/// <summary>
/// Combines two images together by blending the pixels.
/// </summary>
/// <typeparam name="TPixelDst">The pixel format of destination image.</typeparam>
/// <typeparam name="TPixelSrc">The pixel format os source image.</typeparam>
internal class DrawImageProcessor<TPixelDst, TPixelSrc> : ImageProcessor<TPixelDst>
/// <typeparam name="TPixelSrc">The pixel format of source image.</typeparam>
internal class DrawImageProcessor<TPixelDst, TPixelSrc> : ImageProcessor<TPixelDst>
where TPixelDst : struct, IPixel<TPixelDst>
where TPixelSrc : struct, IPixel<TPixelSrc>
where TPixelSrc : struct, IPixel<TPixelSrc>
{
/// <summary>
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelDst, TPixelSrc}"/> class.
/// </summary>
/// <param name="image">The image to blend with the currently processing image.</param>
/// <summary>
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelDst, TPixelSrc}"/> class.
/// </summary>
/// <param name="image">The image to blend with the currently processing image.</param>
/// <param name="location">The location to draw the blended image.</param>
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
public DrawImageProcessor(Image<TPixelSrc> image, Point location, PixelColorBlendingMode colorBlendingMode, PixelAlphaCompositionMode alphaCompositionMode, float opacity)
{
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity));
this.Image = image;
this.Opacity = opacity;
this.Blender = PixelOperations<TPixelDst>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
this.Location = location;
}
/// <summary>
/// Gets the image to blend
/// </summary>
public Image<TPixelSrc> Image { get; }
/// <summary>
/// Gets the opacity of the image to blend
/// </summary>
public float Opacity { get; }
/// <summary>
/// Gets the pixel blender
/// </summary>
public PixelBlender<TPixelDst> Blender { get; }
/// <summary>
/// Gets the location to draw the blended image
/// </summary>
public Point Location { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixelDst> source, Rectangle sourceRectangle, Configuration configuration)
{
Image<TPixelSrc> targetImage = this.Image;
PixelBlender<TPixelDst> blender = this.Blender;
int locationY = this.Location.Y;
// Align start/end positions.
Rectangle bounds = targetImage.Bounds();
int minX = Math.Max(this.Location.X, sourceRectangle.X);
int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width);
int targetX = minX - this.Location.X;
int minY = Math.Max(this.Location.Y, sourceRectangle.Y);
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom);
int width = maxX - minX;
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
public DrawImageProcessor(Image<TPixelSrc> image, Point location, PixelColorBlendingMode colorBlendingMode, PixelAlphaCompositionMode alphaCompositionMode, float opacity)
{
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity));
this.Image = image;
this.Opacity = opacity;
this.Blender = PixelOperations<TPixelDst>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
this.Location = location;
}
/// <summary>
/// Gets the image to blend
/// </summary>
public Image<TPixelSrc> Image { get; }
/// <summary>
/// Gets the opacity of the image to blend
/// </summary>
public float Opacity { get; }
/// <summary>
/// Gets the pixel blender
/// </summary>
public PixelBlender<TPixelDst> Blender { get; }
/// <summary>
/// Gets the location to draw the blended image
/// </summary>
public Point Location { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixelDst> source, Rectangle sourceRectangle, Configuration configuration)
{
Image<TPixelSrc> targetImage = this.Image;
PixelBlender<TPixelDst> blender = this.Blender;
int locationY = this.Location.Y;
// Align start/end positions.
Rectangle bounds = targetImage.Bounds();
int minX = Math.Max(this.Location.X, sourceRectangle.X);
int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Width);
int targetX = minX - this.Location.X;
int minY = Math.Max(this.Location.Y, sourceRectangle.Y);
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom);
int width = maxX - minX;
MemoryAllocator memoryAllocator = this.Image.GetConfiguration().MemoryAllocator;
ParallelFor.WithConfiguration(
minY,
maxY,
configuration,
y =>
{
Span<TPixelDst> background = source.GetPixelRowSpan(y).Slice(minX, width);
Span<TPixelSrc> foreground = targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width);
blender.Blend<TPixelSrc>(memoryAllocator, background, background, foreground, this.Opacity);
});
}
}
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
ParallelHelper.IterateRows(
workingRect,
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixelDst> background = source.GetPixelRowSpan(y).Slice(minX, width);
Span<TPixelSrc> foreground =
targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width);
blender.Blend<TPixelSrc>(memoryAllocator, background, background, foreground, this.Opacity);
}
});
}
}
}

223
src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs

@ -1,107 +1,116 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
/// <summary>
/// Using the brush as a source of pixels colors blends the brush color with source.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class FillProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The brush.
/// </summary>
private readonly IBrush<TPixel> brush;
private readonly GraphicsOptions options;
/// <summary>
/// Initializes a new instance of the <see cref="FillProcessor{TPixel}"/> class.
/// </summary>
/// <param name="brush">The brush to source pixel colors from.</param>
/// <param name="options">The options</param>
public FillProcessor(IBrush<TPixel> brush, GraphicsOptions options)
{
this.brush = brush;
this.options = options;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
int width = maxX - minX;
// If there's no reason for blending, then avoid it.
if (this.IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush))
{
ParallelFor.WithConfiguration(
minY,
maxY,
configuration,
y =>
{
source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color);
});
}
else
{
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width))
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator(
source,
sourceRectangle,
this.options))
{
amount.GetSpan().Fill(1f);
ParallelFor.WithConfiguration(
minY,
maxY,
configuration,
y =>
{
int offsetY = y - startY;
int offsetX = minX - startX;
applicator.Apply(amount.GetSpan(), offsetX, offsetY);
});
}
}
}
private bool IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush)
{
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
/// <summary>
/// Using the brush as a source of pixels colors blends the brush color with source.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class FillProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The brush.
/// </summary>
private readonly IBrush<TPixel> brush;
private readonly GraphicsOptions options;
/// <summary>
/// Initializes a new instance of the <see cref="FillProcessor{TPixel}"/> class.
/// </summary>
/// <param name="brush">The brush to source pixel colors from.</param>
/// <param name="options">The options</param>
public FillProcessor(IBrush<TPixel> brush, GraphicsOptions options)
{
this.brush = brush;
this.options = options;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
int startX = sourceRectangle.X;
int endX = sourceRectangle.Right;
int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom;
// Align start/end positions.
int minX = Math.Max(0, startX);
int maxX = Math.Min(source.Width, endX);
int minY = Math.Max(0, startY);
int maxY = Math.Min(source.Height, endY);
int width = maxX - minX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
// If there's no reason for blending, then avoid it.
if (this.IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush))
{
ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4);
ParallelHelper.IterateRows(
workingRect,
parallelSettings,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color);
}
});
}
else
{
// Reset offset if necessary.
if (minX > 0)
{
startX = 0;
}
if (minY > 0)
{
startY = 0;
}
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width))
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator(
source,
sourceRectangle,
this.options))
{
amount.GetSpan().Fill(1f);
ParallelHelper.IterateRows(
workingRect,
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
int offsetY = y - startY;
int offsetX = minX - startX;
applicator.Apply(amount.GetSpan(), offsetX, offsetY);
}
});
}
}
}
private bool IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush)
{
solidBrush = this.brush as SolidBrush<TPixel>;
if (solidBrush == null)
@ -109,7 +118,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
return false;
}
return this.options.IsOpaqueColorWithoutBlending(solidBrush.Color);
}
}
return this.options.IsOpaqueColorWithoutBlending(solidBrush.Color);
}
}
}

34
src/ImageSharp/Common/Extensions/EncoderExtensions.cs

@ -0,0 +1,34 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
#if !NETCOREAPP2_1
using System;
using System.Text;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Extension methods for the <see cref="Encoder"/> type.
/// </summary>
internal static unsafe class EncoderExtensions
{
/// <summary>
/// Gets a string from the provided buffer data.
/// </summary>
/// <param name="encoding">The encoding.</param>
/// <param name="buffer">The buffer.</param>
/// <returns>The string.</returns>
public static string GetString(this Encoding encoding, ReadOnlySpan<byte> buffer)
{
#if NETSTANDARD1_1
return encoding.GetString(buffer.ToArray());
#else
fixed (byte* bytes = buffer)
{
return encoding.GetString(bytes, buffer.Length);
}
#endif
}
}
}
#endif

61
src/ImageSharp/Common/Helpers/ParallelFor.cs

@ -1,61 +0,0 @@
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.Memory;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Utility methods for Parallel.For() execution. Use this instead of raw <see cref="Parallel"/> calls!
/// </summary>
internal static class ParallelFor
{
/// <summary>
/// Helper method to execute Parallel.For using the settings in <paramref name="configuration"/>
/// </summary>
public static void WithConfiguration(int fromInclusive, int toExclusive, Configuration configuration, Action<int> body)
{
Parallel.For(fromInclusive, toExclusive, configuration.GetParallelOptions(), body);
}
/// <summary>
/// Helper method to execute Parallel.For with temporary worker buffer shared between executing tasks.
/// The buffer is not guaranteed to be clean!
/// </summary>
/// <typeparam name="T">The value type of the buffer</typeparam>
/// <param name="fromInclusive">The start index, inclusive.</param>
/// <param name="toExclusive">The end index, exclusive.</param>
/// <param name="configuration">The <see cref="Configuration"/> used for getting the <see cref="MemoryAllocator"/> and <see cref="ParallelOptions"/></param>
/// <param name="bufferLength">The length of the requested parallel buffer</param>
/// <param name="body">The delegate that is invoked once per iteration.</param>
public static void WithTemporaryBuffer<T>(
int fromInclusive,
int toExclusive,
Configuration configuration,
int bufferLength,
Action<int, IMemoryOwner<T>> body)
where T : struct
{
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
ParallelOptions parallelOptions = configuration.GetParallelOptions();
IMemoryOwner<T> InitBuffer()
{
return memoryAllocator.Allocate<T>(bufferLength);
}
void CleanUpBuffer(IMemoryOwner<T> buffer)
{
buffer.Dispose();
}
IMemoryOwner<T> BodyFunc(int i, ParallelLoopState state, IMemoryOwner<T> buffer)
{
body(i, buffer);
return buffer;
}
Parallel.For(fromInclusive, toExclusive, parallelOptions, InitBuffer, BodyFunc, CleanUpBuffer);
}
}
}

71
src/ImageSharp/Common/ParallelUtils/ParallelExecutionSettings.cs

@ -0,0 +1,71 @@
// Copyright(c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Threading.Tasks;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.ParallelUtils
{
/// <summary>
/// Defines execution settings for methods in <see cref="ParallelHelper"/>.
/// </summary>
internal readonly struct ParallelExecutionSettings
{
/// <summary>
/// Default value for <see cref="MinimumPixelsProcessedPerTask"/>.
/// </summary>
public const int DefaultMinimumPixelsProcessedPerTask = 4096;
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
public ParallelExecutionSettings(
int maxDegreeOfParallelism,
int minimumPixelsProcessedPerTask,
MemoryAllocator memoryAllocator)
{
this.MaxDegreeOfParallelism = maxDegreeOfParallelism;
this.MinimumPixelsProcessedPerTask = minimumPixelsProcessedPerTask;
this.MemoryAllocator = memoryAllocator;
}
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator)
: this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator)
{
}
/// <summary>
/// Gets the MemoryAllocator
/// </summary>
public MemoryAllocator MemoryAllocator { get; }
/// <summary>
/// Gets the value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// </summary>
public int MaxDegreeOfParallelism { get; }
/// <summary>
/// Gets the minimum number of pixels being processed by a single task when parallelizing operations with TPL.
/// Launching tasks for pixel regions below this limit is not worth the overhead.
/// Initialized with <see cref="DefaultMinimumPixelsProcessedPerTask"/> by default,
/// the optimum value is operation specific. (The cheaper the operation, the larger the value is.)
/// </summary>
public int MinimumPixelsProcessedPerTask { get; }
/// <summary>
/// Creates a new instance of <see cref="ParallelExecutionSettings"/>
/// having <see cref="MinimumPixelsProcessedPerTask"/> multiplied by <paramref name="multiplier"/>
/// </summary>
public ParallelExecutionSettings MultiplyMinimumPixelsPerTask(int multiplier)
{
return new ParallelExecutionSettings(
this.MaxDegreeOfParallelism,
this.MinimumPixelsProcessedPerTask * multiplier,
this.MemoryAllocator);
}
}
}

146
src/ImageSharp/Common/ParallelUtils/ParallelHelper.cs

@ -0,0 +1,146 @@
// Copyright(c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.ParallelUtils
{
/// <summary>
/// Utility methods for batched processing of pixel row intervals.
/// Parallel execution is optimized for image processing.
/// Use this instead of direct <see cref="Parallel"/> calls!
/// </summary>
internal static class ParallelHelper
{
/// <summary>
/// Get the default <see cref="ParallelExecutionSettings"/> for a <see cref="Configuration"/>
/// </summary>
public static ParallelExecutionSettings GetParallelSettings(this Configuration configuration)
{
return new ParallelExecutionSettings(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator);
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary>
public static void IterateRows(Rectangle rectangle, Configuration configuration, Action<RowInterval> body)
{
ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings();
IterateRows(rectangle, parallelSettings, body);
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s.
/// </summary>
public static void IterateRows(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
Action<RowInterval> body)
{
DebugGuard.MustBeGreaterThan(rectangle.Width, 0, nameof(rectangle));
int maxSteps = DivideCeil(rectangle.Width * rectangle.Height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(rectangle.Top, rectangle.Bottom);
body(rows);
return;
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = numOfSteps };
Parallel.For(
0,
numOfSteps,
parallelOptions,
i =>
{
int yMin = rectangle.Top + (i * verticalStep);
int yMax = Math.Min(yMin + verticalStep, rectangle.Bottom);
var rows = new RowInterval(yMin, yMax);
body(rows);
});
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s
/// instantiating a temporary buffer for each <paramref name="body"/> invocation.
/// </summary>
public static void IterateRowsWithTempBuffer<T>(
Rectangle rectangle,
in ParallelExecutionSettings parallelSettings,
Action<RowInterval, Memory<T>> body)
where T : struct
{
int maxSteps = DivideCeil(rectangle.Width * rectangle.Height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator memoryAllocator = parallelSettings.MemoryAllocator;
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(rectangle.Top, rectangle.Bottom);
using (IMemoryOwner<T> buffer = memoryAllocator.Allocate<T>(rectangle.Width))
{
body(rows, buffer.Memory);
}
return;
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = numOfSteps };
Parallel.For(
0,
numOfSteps,
parallelOptions,
i =>
{
int yMin = rectangle.Top + (i * verticalStep);
int yMax = Math.Min(yMin + verticalStep, rectangle.Bottom);
var rows = new RowInterval(yMin, yMax);
using (IMemoryOwner<T> buffer = memoryAllocator.Allocate<T>(rectangle.Width))
{
body(rows, buffer.Memory);
}
});
}
/// <summary>
/// Iterate through the rows of a rectangle in optimized batches defined by <see cref="RowInterval"/>-s
/// instantiating a temporary buffer for each <paramref name="body"/> invocation.
/// </summary>
public static void IterateRowsWithTempBuffer<T>(
Rectangle rectangle,
Configuration configuration,
Action<RowInterval, Memory<T>> body)
where T : struct
{
IterateRowsWithTempBuffer(rectangle, configuration.GetParallelSettings(), body);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
}
}

3
src/ImageSharp/Configuration.cs

@ -59,6 +59,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
/// configured with this <see cref="Configuration"/> instance.
/// Initialized with <see cref="Environment.ProcessorCount"/> by default.
/// </summary>
public int MaxDegreeOfParallelism
{
@ -125,7 +126,7 @@ namespace SixLabors.ImageSharp
/// Creates a shallow copy of the <see cref="Configuration"/>
/// </summary>
/// <returns>A new configuration instance</returns>
public Configuration ShallowCopy()
public Configuration Clone()
{
return new Configuration
{

20
src/ImageSharp/Formats/Bmp/BmpMetaData.cs

@ -6,13 +6,29 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary>
/// Provides Bmp specific metadata information for the image.
/// </summary>
public class BmpMetaData
public class BmpMetaData : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="BmpMetaData"/> class.
/// </summary>
public BmpMetaData()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BmpMetaData"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private BmpMetaData(BmpMetaData other) => this.BitsPerPixel = other.BitsPerPixel;
/// <summary>
/// Gets or sets the number of bits per pixel.
/// </summary>
public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24;
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new BmpMetaData(this);
// TODO: Colors used once we support encoding palette bmps.
}
}
}

23
src/ImageSharp/Formats/Gif/GifFrameMetaData.cs

@ -6,8 +6,26 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Provides Gif specific metadata information for the image frame.
/// </summary>
public class GifFrameMetaData
public class GifFrameMetaData : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="GifFrameMetaData"/> class.
/// </summary>
public GifFrameMetaData()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GifFrameMetaData"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private GifFrameMetaData(GifFrameMetaData other)
{
this.ColorTableLength = other.ColorTableLength;
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
}
/// <summary>
/// Gets or sets the length of the color table for paletted images.
/// If not 0, then this field indicates the maximum number of colors to use when quantizing the
@ -29,5 +47,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// be treated after being displayed.
/// </summary>
public GifDisposalMethod DisposalMethod { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifFrameMetaData(this);
}
}

23
src/ImageSharp/Formats/Gif/GifMetaData.cs

@ -6,8 +6,26 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Provides Gif specific metadata information for the image.
/// </summary>
public class GifMetaData
public class GifMetaData : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="GifMetaData"/> class.
/// </summary>
public GifMetaData()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GifMetaData"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private GifMetaData(GifMetaData other)
{
this.RepeatCount = other.RepeatCount;
this.ColorTableMode = other.ColorTableMode;
this.GlobalColorTableLength = other.GlobalColorTableLength;
}
/// <summary>
/// Gets or sets the number of times any animation is repeated.
/// <remarks>
@ -25,5 +43,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Gets or sets the length of the global color table if present.
/// </summary>
public int GlobalColorTableLength { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifMetaData(this);
}
}

18
src/ImageSharp/Formats/Jpeg/JpegMetaData.cs

@ -6,11 +6,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// Provides Jpeg specific metadata information for the image.
/// </summary>
public class JpegMetaData
public class JpegMetaData : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="JpegMetaData"/> class.
/// </summary>
public JpegMetaData()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="JpegMetaData"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private JpegMetaData(JpegMetaData other) => this.Quality = other.Quality;
/// <summary>
/// Gets or sets the encoded quality.
/// </summary>
public int Quality { get; set; } = 75;
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new JpegMetaData(this);
}
}

56
src/ImageSharp/Formats/Png/Adam7.cs

@ -0,0 +1,56 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Constants and helper methods for the Adam7 interlacing algorithm.
/// </summary>
internal static class Adam7
{
/// <summary>
/// The amount to increment when processing each column per scanline for each interlaced pass.
/// </summary>
public static readonly int[] ColumnIncrement = { 8, 8, 4, 4, 2, 2, 1 };
/// <summary>
/// The index to start at when processing each column per scanline for each interlaced pass.
/// </summary>
public static readonly int[] FirstColumn = { 0, 4, 0, 2, 0, 1, 0 };
/// <summary>
/// The index to start at when processing each row per scanline for each interlaced pass.
/// </summary>
public static readonly int[] FirstRow = { 0, 0, 4, 0, 2, 0, 1 };
/// <summary>
/// The amount to increment when processing each row per scanline for each interlaced pass.
/// </summary>
public static readonly int[] RowIncrement = { 8, 8, 8, 4, 4, 2, 2 };
/// <summary>
/// Returns the correct number of columns for each interlaced pass.
/// </summary>
/// <param name="width">The line width.</param>
/// <param name="passIndex">The current pass index.</param>
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ComputeColumns(int width, int passIndex)
{
switch (passIndex)
{
case 0: return (width + 7) / 8;
case 1: return (width + 3) / 8;
case 2: return (width + 3) / 4;
case 3: return (width + 1) / 4;
case 4: return (width + 1) / 2;
case 5: return width / 2;
case 6: return width;
default: throw new ArgumentException($"Not a valid pass index: {passIndex}");
}
}
}
}

107
src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs

@ -0,0 +1,107 @@
using System;
using System.Buffers.Binary;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.MetaData;
namespace SixLabors.ImageSharp.Formats.Png.Chunks
{
/// <summary>
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary>
internal readonly struct PhysicalChunkData
{
public const int Size = 9;
public PhysicalChunkData(uint x, uint y, byte unitSpecifier)
{
this.XAxisPixelsPerUnit = x;
this.YAxisPixelsPerUnit = y;
this.UnitSpecifier = unitSpecifier;
}
/// <summary>
/// Gets the number of pixels per unit on the X axis.
/// </summary>
public uint XAxisPixelsPerUnit { get; }
/// <summary>
/// Gets the number of pixels per unit on the Y axis.
/// </summary>
public uint YAxisPixelsPerUnit { get; }
/// <summary>
/// Gets the unit specifier.
/// 0: unit is unknown
/// 1: unit is the meter
/// When the unit specifier is 0, the pHYs chunk defines pixel aspect ratio only; the actual size of the pixels remains unspecified.
/// </summary>
public byte UnitSpecifier { get; }
/// <summary>
/// Parses the PhysicalChunkData from the given buffer.
/// </summary>
/// <param name="data">The data buffer.</param>
/// <returns>The parsed PhysicalChunkData.</returns>
public static PhysicalChunkData Parse(ReadOnlySpan<byte> data)
{
uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(0, 4));
uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4));
byte unit = data[8];
return new PhysicalChunkData(hResolution, vResolution, unit);
}
/// <summary>
/// Constructs the PngPhysicalChunkData from the provided metadata.
/// If the resolution units are not in meters, they are automatically convereted.
/// </summary>
/// <param name="meta">The metadata.</param>
/// <returns>The constructed PngPhysicalChunkData instance.</returns>
public static PhysicalChunkData FromMetadata(ImageMetaData meta)
{
byte unitSpecifier = 0;
uint x;
uint y;
switch (meta.ResolutionUnits)
{
case PixelResolutionUnit.AspectRatio:
unitSpecifier = 0; // Unspecified
x = (uint)Math.Round(meta.HorizontalResolution);
y = (uint)Math.Round(meta.VerticalResolution);
break;
case PixelResolutionUnit.PixelsPerInch:
unitSpecifier = 1; // Per meter
x = (uint)Math.Round(UnitConverter.InchToMeter(meta.HorizontalResolution));
y = (uint)Math.Round(UnitConverter.InchToMeter(meta.VerticalResolution));
break;
case PixelResolutionUnit.PixelsPerCentimeter:
unitSpecifier = 1; // Per meter
x = (uint)Math.Round(UnitConverter.CmToMeter(meta.HorizontalResolution));
y = (uint)Math.Round(UnitConverter.CmToMeter(meta.VerticalResolution));
break;
default:
unitSpecifier = 1; // Per meter
x = (uint)Math.Round(meta.HorizontalResolution);
y = (uint)Math.Round(meta.VerticalResolution);
break;
}
return new PhysicalChunkData(x, y, unitSpecifier);
}
/// <summary>
/// Writes the data to the given buffer.
/// </summary>
/// <param name="buffer">The buffer.</param>
public void WriteTo(Span<byte> buffer)
{
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(0, 4), this.XAxisPixelsPerUnit);
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), this.YAxisPixelsPerUnit);
buffer[8] = this.UnitSpecifier;
}
}
}

14
src/ImageSharp/Formats/Png/Filters/AverageFilter.cs

@ -30,7 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
// Average(x) + floor((Raw(x-bpp)+Prior(x))/2)
int x = 1;
for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) {
for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x)
{
ref byte scan = ref Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref prevBaseRef, x);
scan = (byte)(scan + (above >> 1));
@ -68,7 +69,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
resultBaseRef = 3;
int x = 0;
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) {
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref prevBaseRef, x);
++x;
@ -77,7 +79,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
sum += ImageMaths.FastAbs(unchecked((sbyte)res));
}
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) {
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte left = Unsafe.Add(ref scanBaseRef, xLeft);
byte above = Unsafe.Add(ref prevBaseRef, x);
@ -97,9 +100,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
/// <param name="above">The above byte</param>
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Average(byte left, byte above)
{
return (left + above) >> 1;
}
private static int Average(byte left, byte above) => (left + above) >> 1;
}
}

6
src/ImageSharp/Formats/Png/Filters/PaethFilter.cs

@ -72,7 +72,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
resultBaseRef = 4;
int x = 0;
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) {
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref prevBaseRef, x);
++x;
@ -81,7 +82,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
sum += ImageMaths.FastAbs(unchecked((sbyte)res));
}
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) {
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte left = Unsafe.Add(ref scanBaseRef, xLeft);
byte above = Unsafe.Add(ref prevBaseRef, x);

6
src/ImageSharp/Formats/Png/Filters/SubFilter.cs

@ -59,7 +59,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
resultBaseRef = 1;
int x = 0;
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) {
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
++x;
ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
@ -67,7 +68,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
sum += ImageMaths.FastAbs(unchecked((sbyte)res));
}
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) {
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte prev = Unsafe.Add(ref scanBaseRef, xLeft);
++x;

2
src/ImageSharp/Formats/Png/IPngEncoderOptions.cs

@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Gets the filter method.
/// </summary>
PngFilterMethod FilterMethod { get; }
PngFilterMethod? FilterMethod { get; }
/// <summary>
/// Gets the compression level 1-9.

2
src/ImageSharp/Formats/Png/PngBitDepth.cs

@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Provides enumeration for the available PNG bit depths.
/// </summary>
public enum PngBitDepth
public enum PngBitDepth : byte
{
/// <summary>
/// 1 bit per sample or per palette index (not per pixel).

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

@ -41,5 +41,17 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The header bytes as a big endian coded ulong.
/// </summary>
public const ulong HeaderValue = 0x89504E470D0A1A0AUL;
/// <summary>
/// The dictionary of available color types.
/// </summary>
public static readonly Dictionary<PngColorType, byte[]> ColorTypes = new Dictionary<PngColorType, byte[]>()
{
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 },
[PngColorType.Rgb] = new byte[] { 8, 16 },
[PngColorType.Palette] = new byte[] { 1, 2, 4, 8 },
[PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 },
[PngColorType.RgbWithAlpha] = new byte[] { 8, 16 }
};
}
}

916
src/ImageSharp/Formats/Png/PngDecoderCore.cs

File diff suppressed because it is too large

2
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Gets or sets the filter method.
/// </summary>
public PngFilterMethod FilterMethod { get; set; } = PngFilterMethod.Paeth;
public PngFilterMethod? FilterMethod { get; set; }
/// <summary>
/// Gets or sets the compression level 1-9.

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

@ -3,9 +3,13 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Png.Zlib;
using SixLabors.ImageSharp.Memory;
@ -21,6 +25,21 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
internal sealed class PngEncoderCore : IDisposable
{
/// <summary>
/// The dictionary of available color types.
/// </summary>
private static readonly Dictionary<PngColorType, byte[]> ColorTypes = new Dictionary<PngColorType, byte[]>()
{
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 },
[PngColorType.Rgb] = new byte[] { 8, 16 },
[PngColorType.Palette] = new byte[] { 1, 2, 4, 8 },
[PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 },
[PngColorType.RgbWithAlpha] = new byte[] { 8, 16 }
};
/// <summary>
/// Used the manage memory allocations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
@ -158,7 +177,11 @@ namespace SixLabors.ImageSharp.Formats.Png
this.memoryAllocator = memoryAllocator;
this.pngBitDepth = options.BitDepth;
this.pngColorType = options.ColorType;
this.pngFilterMethod = options.FilterMethod;
// Specification recommends default filter method None for paletted images and Paeth for others.
this.pngFilterMethod = options.FilterMethod ?? (options.ColorType.Equals(PngColorType.Palette)
? PngFilterMethod.None
: PngFilterMethod.Paeth);
this.compressionLevel = options.CompressionLevel;
this.gamma = options.Gamma;
this.quantizer = options.Quantizer;
@ -189,28 +212,37 @@ namespace SixLabors.ImageSharp.Formats.Png
this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth;
this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16);
// Ensure we are not allowing impossible combinations.
if (!ColorTypes.ContainsKey(this.pngColorType.Value))
{
throw new NotSupportedException("Color type is not supported or not valid.");
}
stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length);
QuantizedFrame<TPixel> quantized = null;
ReadOnlySpan<byte> quantizedPixelsSpan = default;
if (this.pngColorType == PngColorType.Palette)
{
byte bits;
byte bits = (byte)this.pngBitDepth;
if (!ColorTypes[this.pngColorType.Value].Contains(bits))
{
throw new NotSupportedException("Bit depth is not supported or not valid.");
}
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
if (this.quantizer == null)
{
bits = (byte)Math.Min(8u, (short)this.pngBitDepth);
int colorSize = ImageMaths.GetColorCountForBitDepth(bits);
this.quantizer = new WuQuantizer(colorSize);
this.quantizer = new WuQuantizer(ImageMaths.GetColorCountForBitDepth(bits));
}
// Create quantized frame returning the palette and set the bit depth.
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
quantizedPixelsSpan = quantized.GetPixelSpan();
bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
byte quantizedBits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
bits = Math.Max(bits, quantizedBits);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
// We check again for the bit depth as the bit depth of the color palette from a given quantizer might not
// be within the acceptable range.
if (bits == 3)
{
bits = 4;
@ -224,7 +256,11 @@ namespace SixLabors.ImageSharp.Formats.Png
}
else
{
this.bitDepth = (byte)(this.use16Bit ? 16 : 8);
this.bitDepth = (byte)this.pngBitDepth;
if (!ColorTypes[this.pngColorType.Value].Contains(this.bitDepth))
{
throw new NotSupportedException("Bit depth is not supported or not valid.");
}
}
this.bytesPerPixel = this.CalculateBytesPerPixel();
@ -249,7 +285,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WritePhysicalChunk(stream, metaData);
this.WriteGammaChunk(stream);
this.WriteExifChunk(stream, metaData);
this.WriteDataChunks(image.Frames.RootFrame, quantizedPixelsSpan, stream);
this.WriteDataChunks(image.Frames.RootFrame, quantized, stream);
this.WriteEndChunk(stream);
stream.Flush();
@ -280,32 +316,55 @@ namespace SixLabors.ImageSharp.Formats.Png
const float RX = .2126F;
const float GX = .7152F;
const float BX = .0722F;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
Span<byte> rawScanlineSpan = this.rawScanline.GetSpan();
ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan);
if (this.pngColorType.Equals(PngColorType.Grayscale))
{
// TODO: Realistically we should support 1, 2, 4, 8, and 16 bit grayscale images.
// we currently do the other types via palette. Maybe RC as I don't understand how the data is packed yet
// for 1, 2, and 4 bit grayscale images.
// TODO: Research and add support for grayscale plus tRNS
if (this.use16Bit)
{
// 16 bit grayscale
Rgb48 rgb = default;
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 2)
{
rowSpan[x].ToRgb48(ref rgb);
Unsafe.Add(ref rowSpanRef, x).ToRgb48(ref rgb);
ushort luminance = (ushort)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B));
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance);
}
}
else
{
// 8 bit grayscale
Rgb24 rgb = default;
for (int x = 0; x < rowSpan.Length; x++)
if (this.bitDepth == 8)
{
// 8 bit grayscale
Rgb24 rgb = default;
for (int x = 0; x < rowSpan.Length; x++)
{
Unsafe.Add(ref rowSpanRef, x).ToRgb24(ref rgb);
Unsafe.Add(ref rawScanlineSpanRef, x) = (byte)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B));
}
}
else
{
rowSpan[x].ToRgb24(ref rgb);
rawScanlineSpan[x] = (byte)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B));
// 1, 2, and 4 bit grayscale
using (IManagedByteBuffer temp = this.memoryAllocator.AllocateManagedByteBuffer(rowSpan.Length, AllocationOptions.Clean))
{
int scaleFactor = 255 / (ImageMaths.GetColorCountForBitDepth(this.bitDepth) - 1);
Span<byte> tempSpan = temp.GetSpan();
ref byte tempSpanRef = ref MemoryMarshal.GetReference(tempSpan);
Rgb24 rgb = default;
for (int x = 0; x < rowSpan.Length; x++)
{
Unsafe.Add(ref rowSpanRef, x).ToRgb24(ref rgb);
float luminance = ((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B)) / scaleFactor;
Unsafe.Add(ref tempSpanRef, x) = (byte)luminance;
this.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth);
}
}
}
}
}
@ -317,7 +376,7 @@ namespace SixLabors.ImageSharp.Formats.Png
Rgba64 rgba = default;
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 4)
{
rowSpan[x].ToRgba64(ref rgba);
Unsafe.Add(ref rowSpanRef, x).ToRgba64(ref rgba);
ushort luminance = (ushort)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B));
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.A);
@ -329,9 +388,9 @@ namespace SixLabors.ImageSharp.Formats.Png
Rgba32 rgba = default;
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 2)
{
rowSpan[x].ToRgba32(ref rgba);
rawScanlineSpan[o] = (byte)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B));
rawScanlineSpan[o + 1] = rgba.A;
Unsafe.Add(ref rowSpanRef, x).ToRgba32(ref rgba);
Unsafe.Add(ref rawScanlineSpanRef, o) = (byte)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B));
Unsafe.Add(ref rawScanlineSpanRef, o + 1) = rgba.A;
}
}
}
@ -367,9 +426,10 @@ namespace SixLabors.ImageSharp.Formats.Png
{
// 16 bit Rgba
Rgba64 rgba = default;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 8)
{
rowSpan[x].ToRgba64(ref rgba);
Unsafe.Add(ref rowSpanRef, x).ToRgba64(ref rgba);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgba.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgba.B);
@ -383,9 +443,10 @@ namespace SixLabors.ImageSharp.Formats.Png
{
// 16 bit Rgb
Rgb48 rgb = default;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 6)
{
rowSpan[x].ToRgb48(ref rgb);
Unsafe.Add(ref rowSpanRef, x).ToRgb48(ref rgb);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgb.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgb.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgb.B);
@ -402,18 +463,25 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="rowSpan">The row span.</param>
/// <param name="quantizedPixelsSpan">The span of quantized pixels. Can be null.</param>
/// <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, ReadOnlySpan<byte> quantizedPixelsSpan, int row)
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, QuantizedFrame<TPixel> quantized, int row)
where TPixel : struct, IPixel<TPixel>
{
switch (this.pngColorType)
{
case PngColorType.Palette:
int stride = this.rawScanline.Length();
quantizedPixelsSpan.Slice(row * stride, stride).CopyTo(this.rawScanline.GetSpan());
if (this.bitDepth < 8)
{
this.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.rawScanline.GetSpan(), this.bitDepth);
}
else
{
int stride = this.rawScanline.Length();
quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.rawScanline.GetSpan());
}
break;
case PngColorType.Grayscale:
@ -538,16 +606,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="header">The <see cref="PngHeader"/>.</param>
private void WriteHeaderChunk(Stream stream, in PngHeader header)
{
BinaryPrimitives.WriteInt32BigEndian(this.chunkDataBuffer.AsSpan(0, 4), header.Width);
BinaryPrimitives.WriteInt32BigEndian(this.chunkDataBuffer.AsSpan(4, 4), header.Height);
this.chunkDataBuffer[8] = header.BitDepth;
this.chunkDataBuffer[9] = (byte)header.ColorType;
this.chunkDataBuffer[10] = header.CompressionMethod;
this.chunkDataBuffer[11] = header.FilterMethod;
this.chunkDataBuffer[12] = (byte)header.InterlaceMethod;
header.WriteTo(this.chunkDataBuffer);
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, 13);
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, PngHeader.Size);
}
/// <summary>
@ -569,8 +630,8 @@ namespace SixLabors.ImageSharp.Formats.Png
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength))
{
Span<byte> colorTableSpan = colorTable.GetSpan();
Span<byte> alphaTableSpan = alphaTable.GetSpan();
ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan());
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
Span<byte> quantizedSpan = quantized.GetPixelSpan();
for (int i = 0; i < paletteLength; i++)
@ -582,9 +643,9 @@ namespace SixLabors.ImageSharp.Formats.Png
byte alpha = rgba.A;
colorTableSpan[offset] = rgba.R;
colorTableSpan[offset + 1] = rgba.G;
colorTableSpan[offset + 2] = rgba.B;
Unsafe.Add(ref colorTableRef, offset) = rgba.R;
Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G;
Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B;
if (alpha > this.threshold)
{
@ -592,7 +653,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
anyAlpha = anyAlpha || alpha < byte.MaxValue;
alphaTableSpan[i] = alpha;
Unsafe.Add(ref alphaTableRef, i) = alpha;
}
}
@ -613,51 +674,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="meta">The image meta data.</param>
private void WritePhysicalChunk(Stream stream, ImageMetaData meta)
{
// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. It contains:
// Pixels per unit, X axis: 4 bytes (unsigned integer)
// Pixels per unit, Y axis: 4 bytes (unsigned integer)
// Unit specifier: 1 byte
//
// The following values are legal for the unit specifier:
// 0: unit is unknown
// 1: unit is the meter
//
// When the unit specifier is 0, the pHYs chunk defines pixel aspect ratio only; the actual size of the pixels remains unspecified.
Span<byte> hResolution = this.chunkDataBuffer.AsSpan(0, 4);
Span<byte> vResolution = this.chunkDataBuffer.AsSpan(4, 4);
switch (meta.ResolutionUnits)
{
case PixelResolutionUnit.AspectRatio:
this.chunkDataBuffer[8] = 0;
BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(meta.HorizontalResolution));
BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(meta.VerticalResolution));
break;
case PixelResolutionUnit.PixelsPerInch:
this.chunkDataBuffer[8] = 1; // Per meter
BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(UnitConverter.InchToMeter(meta.HorizontalResolution)));
BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(UnitConverter.InchToMeter(meta.VerticalResolution)));
break;
PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer);
case PixelResolutionUnit.PixelsPerCentimeter:
this.chunkDataBuffer[8] = 1; // Per meter
BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(UnitConverter.CmToMeter(meta.HorizontalResolution)));
BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(UnitConverter.CmToMeter(meta.VerticalResolution)));
break;
default:
this.chunkDataBuffer[8] = 1; // Per meter
BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(meta.HorizontalResolution));
BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(meta.VerticalResolution));
break;
}
this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, 9);
this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, PhysicalChunkData.Size);
}
/// <summary>
@ -696,9 +715,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The image.</param>
/// <param name="quantizedPixelsSpan">The span of quantized pixel data. Can be null.</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, ReadOnlySpan<byte> quantizedPixelsSpan, Stream stream)
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, QuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
this.bytesPerScanline = this.CalculateScanlineLength(this.width);
@ -714,26 +733,22 @@ namespace SixLabors.ImageSharp.Formats.Png
break;
case PngFilterMethod.Sub:
this.sub = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
break;
case PngFilterMethod.Up:
this.up = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
break;
case PngFilterMethod.Average:
this.average = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
break;
case PngFilterMethod.Paeth:
this.paeth = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
break;
case PngFilterMethod.Adaptive:
case PngFilterMethod.Adaptive:
this.sub = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
this.up = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
this.average = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
@ -750,7 +765,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{
for (int y = 0; y < this.height; y++)
{
IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan<TPixel>)pixels.GetPixelRowSpan(y), quantizedPixelsSpan, y);
IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan<TPixel>)pixels.GetPixelRowSpan(y), quantized, y);
deflateStream.Write(r.Array, 0, resultLength);
IManagedByteBuffer temp = this.rawScanline;
@ -830,6 +845,47 @@ namespace SixLabors.ImageSharp.Formats.Png
stream.Write(this.buffer, 0, 4); // write the crc
}
/// <summary>
/// Packs the given 8 bit array into and array of <paramref name="bits"/> depths.
/// </summary>
/// <param name="source">The source span in 8 bits.</param>
/// <param name="result">The resultant span in <paramref name="bits"/>.</param>
/// <param name="bits">The bit depth.</param>
private void ScaleDownFrom8BitArray(ReadOnlySpan<byte> source, Span<byte> result, int bits)
{
ref byte sourceRef = ref MemoryMarshal.GetReference(source);
ref byte resultRef = ref MemoryMarshal.GetReference(result);
byte mask = (byte)(0xFF >> (8 - bits));
byte shift0 = (byte)(8 - bits);
int shift = 8 - bits;
int v = 0;
int resultOffset = 0;
for (int i = 0; i < source.Length; i++)
{
int value = Unsafe.Add(ref sourceRef, i) & mask;
v |= value << shift;
if (shift == 0)
{
shift = shift0;
Unsafe.Add(ref resultRef, resultOffset) = (byte)v;
resultOffset++;
v = 0;
}
else
{
shift -= bits;
}
}
if (shift != shift0)
{
Unsafe.Add(ref resultRef, resultOffset) = (byte)v;
}
}
/// <summary>
/// Calculates the scanline length.
/// </summary>

68
src/ImageSharp/Formats/Png/PngHeader.cs

@ -1,6 +1,9 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
@ -8,6 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
internal readonly struct PngHeader
{
public const int Size = 13;
public PngHeader(
int width,
int height,
@ -74,5 +79,68 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Two values are currently defined: 0 (no interlace) or 1 (Adam7 interlace).
/// </summary>
public PngInterlaceMode InterlaceMethod { get; }
/// <summary>
/// Validates the png header.
/// </summary>
/// <exception cref="NotSupportedException">
/// Thrown if the image does pass validation.
/// </exception>
public void Validate()
{
if (!PngConstants.ColorTypes.TryGetValue(this.ColorType, out byte[] supportedBitDepths))
{
throw new NotSupportedException($"Invalid or unsupported color type. Was '{this.ColorType}'.");
}
if (supportedBitDepths.AsSpan().IndexOf(this.BitDepth) == -1)
{
throw new NotSupportedException($"Invalid or unsupported bit depth. Was '{this.BitDepth}'.");
}
if (this.FilterMethod != 0)
{
throw new NotSupportedException($"Invalid filter method. Expected 0. Was '{this.FilterMethod}'.");
}
// The png specification only defines 'None' and 'Adam7' as interlaced methods.
if (this.InterlaceMethod != PngInterlaceMode.None && this.InterlaceMethod != PngInterlaceMode.Adam7)
{
throw new NotSupportedException($"Invalid interlace method. Expected 'None' or 'Adam7'. Was '{this.InterlaceMethod}'.");
}
}
/// <summary>
/// Writes the header to the given buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteTo(Span<byte> buffer)
{
BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(0, 4), this.Width);
BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(4, 4), this.Height);
buffer[8] = this.BitDepth;
buffer[9] = (byte)this.ColorType;
buffer[10] = this.CompressionMethod;
buffer[11] = this.FilterMethod;
buffer[12] = (byte)this.InterlaceMethod;
}
/// <summary>
/// Parses the PngHeader from the given data buffer.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed PngHeader.</returns>
public static PngHeader Parse(ReadOnlySpan<byte> data)
{
return new PngHeader(
width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)),
height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)),
bitDepth: data[8],
colorType: (PngColorType)data[9],
compressionMethod: data[10],
filterMethod: data[11],
interlaceMethod: (PngInterlaceMode)data[12]);
}
}
}

23
src/ImageSharp/Formats/Png/PngMetaData.cs

@ -6,8 +6,26 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Provides Png specific metadata information for the image.
/// </summary>
public class PngMetaData
public class PngMetaData : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="PngMetaData"/> class.
/// </summary>
public PngMetaData()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PngMetaData"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private PngMetaData(PngMetaData other)
{
this.BitDepth = other.BitDepth;
this.ColorType = other.ColorType;
this.Gamma = other.Gamma;
}
/// <summary>
/// Gets or sets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType"/> values.
@ -23,5 +41,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets or sets the gamma value for the image.
/// </summary>
public float Gamma { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetaData(this);
}
}

600
src/ImageSharp/Formats/Png/PngScanlineProcessor.cs

@ -0,0 +1,600 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Provides methods to allow the decoding of raw scanlines to image rows of different pixel formats.
/// </summary>
internal static class PngScanlineProcessor
{
public static void ProcessGrayscaleScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
bool hasTrans,
ushort luminance16Trans,
byte luminanceTrans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
int scaleFactor = 255 / (ImageMaths.GetColorCountForBitDepth(header.BitDepth) - 1);
if (!hasTrans)
{
if (header.BitDepth == 16)
{
Rgb48 rgb48 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.R = luminance;
rgb48.G = luminance;
rgb48.B = luminance;
pixel.PackFromRgb48(rgb48);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
// TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method.
var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = 0; x < header.Width; x++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return;
}
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = 0; x < header.Width; x++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = luminance.Equals(luminanceTrans) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedGrayscaleScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
bool hasTrans,
ushort luminance16Trans,
byte luminanceTrans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
int scaleFactor = 255 / (ImageMaths.GetColorCountForBitDepth(header.BitDepth) - 1);
if (!hasTrans)
{
if (header.BitDepth == 16)
{
Rgb48 rgb48 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.R = luminance;
rgb48.G = luminance;
rgb48.B = luminance;
pixel.PackFromRgb48(rgb48);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
// TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method.
var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return;
}
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = pixelOffset; x < header.Width; x += increment)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = luminance.Equals(luminanceTrans) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessGrayscaleWithAlphaScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += 4)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = alpha;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba32 = default;
int bps = bytesPerSample;
for (int x = 0; x < header.Width; x++)
{
int offset = x * bytesPerPixel;
byte luminance = Unsafe.Add(ref scanlineSpanRef, offset);
byte alpha = Unsafe.Add(ref scanlineSpanRef, offset + bps);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = alpha;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedGrayscaleWithAlphaScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
int bytesPerPixel,
int bytesPerSample)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += 4)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = alpha;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = pixelOffset; x < header.Width; x += increment)
{
int offset = x * bytesPerPixel;
byte luminance = Unsafe.Add(ref scanlineSpanRef, offset);
byte alpha = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = alpha;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessPaletteScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
ReadOnlySpan<byte> palette,
byte[] paletteAlpha)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(palette);
ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
if (paletteAlpha?.Length > 0)
{
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
Rgba32 rgba = default;
ref byte paletteAlphaRef = ref paletteAlpha[0];
for (int x = 0; x < header.Width; x++)
{
int index = Unsafe.Add(ref scanlineSpanRef, x);
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue;
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
// TODO: We should have PackFromRgb24.
var rgba = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = 0; x < header.Width; x++)
{
int index = Unsafe.Add(ref scanlineSpanRef, x);
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedPaletteScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
ReadOnlySpan<byte> palette,
byte[] paletteAlpha)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(palette);
ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
if (paletteAlpha?.Length > 0)
{
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
Rgba32 rgba = default;
ref byte paletteAlphaRef = ref paletteAlpha[0];
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o++)
{
int index = Unsafe.Add(ref scanlineSpanRef, o);
rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue;
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
var rgba = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o++)
{
int index = Unsafe.Add(ref scanlineSpanRef, o);
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessRgbScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample,
bool hasTrans,
Rgb48 rgb48Trans,
Rgb24 rgb24Trans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (!hasTrans)
{
if (header.BitDepth == 16)
{
Rgb48 rgb48 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
pixel.PackFromRgb48(rgb48);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
PixelOperations<TPixel>.Instance.PackFromRgb24Bytes(scanlineSpan, rowSpan, header.Width);
}
return;
}
if (header.BitDepth == 16)
{
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
ReadOnlySpan<Rgb24> rgb24Span = MemoryMarshal.Cast<byte, Rgb24>(scanlineSpan);
ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span);
for (int x = 0; x < header.Width; x++)
{
ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x);
Rgba32 rgba32 = default;
rgba32.Rgb = rgb24;
rgba32.A = rgb24.Equals(rgb24Trans) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedRgbScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
int bytesPerPixel,
int bytesPerSample,
bool hasTrans,
Rgb48 rgb48Trans,
Rgb24 rgb24Trans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
if (hasTrans)
{
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgb48 rgb48 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
pixel.PackFromRgb48(rgb48);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return;
}
if (hasTrans)
{
Rgba32 rgba = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgba.R = Unsafe.Add(ref scanlineSpanRef, o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, o + bytesPerSample);
rgba.B = Unsafe.Add(ref scanlineSpanRef, o + (2 * bytesPerSample));
rgba.A = rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
var rgba = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgba.R = Unsafe.Add(ref scanlineSpanRef, o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, o + bytesPerSample);
rgba.B = Unsafe.Add(ref scanlineSpanRef, o + (2 * bytesPerSample));
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessRgbaScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += bytesPerPixel)
{
rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample));
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
PixelOperations<TPixel>.Instance.PackFromRgba32Bytes(scanlineSpan, rowSpan, header.Width);
}
}
public static void ProcessInterlacedRgbaScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
int bytesPerPixel,
int bytesPerSample)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample));
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgba.R = Unsafe.Add(ref scanlineSpanRef, o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, o + bytesPerSample);
rgba.B = Unsafe.Add(ref scanlineSpanRef, o + (2 * bytesPerSample));
rgba.A = Unsafe.Add(ref scanlineSpanRef, o + (3 * bytesPerSample));
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
}
}

48
src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs

@ -43,22 +43,24 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
private bool isDisposed;
/// <summary>
/// Whether the crc value has been read.
/// The current data remaining to be read
/// </summary>
private bool crcRead;
private int currentDataRemaining;
/// <summary>
/// The current data remaining to be read
/// Delegate to get more data once we've exhausted the current data remaining
/// </summary>
private int currentDataRemaining;
private readonly Func<int> getData;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream</param>
public ZlibInflateStream(Stream innerStream)
/// <param name="getData">A delegate to get more data from the inner stream</param>
public ZlibInflateStream(Stream innerStream, Func<int> getData)
{
this.innerStream = innerStream;
this.getData = getData;
}
/// <inheritdoc/>
@ -112,12 +114,36 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
{
if (this.currentDataRemaining == 0)
{
return 0;
// last buffer was read in its entirety, let's make sure we don't actually have more
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining == 0)
{
return 0;
}
}
int bytesToRead = Math.Min(count, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
return this.innerStream.Read(buffer, offset, bytesToRead);
int bytesRead = this.innerStream.Read(buffer, offset, bytesToRead);
// keep reading data until we've reached the end of the stream or filled the buffer
while (this.currentDataRemaining == 0 && bytesRead < count)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining == 0)
{
return bytesRead;
}
offset += bytesRead;
bytesToRead = Math.Min(count - bytesRead, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
bytesRead += this.innerStream.Read(buffer, offset, bytesToRead);
}
return bytesRead;
}
/// <inheritdoc/>
@ -153,14 +179,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
{
this.compressedStream.Dispose();
this.compressedStream = null;
if (!this.crcRead)
{
// Consume the trailing 4 bytes
this.innerStream.Read(ChecksumBuffer, 0, 4);
this.currentDataRemaining -= 4;
this.crcRead = true;
}
}
}

31
src/ImageSharp/IDeepCloneable.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp
{
/// <summary>
/// A generic interface for a deeply cloneable type.
/// </summary>
/// <typeparam name="T">The type of object to clone.</typeparam>
public interface IDeepCloneable<out T>
where T : class
{
/// <summary>
/// Creates a new <typeparamref name="T"/> that is a deep copy of the current instance.
/// </summary>
/// <returns>The <typeparamref name="T"/>.</returns>
T DeepClone();
}
/// <summary>
/// An interface for objects that can be cloned. This creates a deep copy of the object.
/// </summary>
public interface IDeepCloneable
{
/// <summary>
/// Creates a new object that is a deep copy of the current instance.
/// </summary>
/// <returns>The <see cref="IDeepCloneable"/>.</returns>
IDeepCloneable DeepClone();
}
}

2
src/ImageSharp/Image.Decode.cs

@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp
return null;
}
using (IManagedByteBuffer buffer = config.MemoryAllocator.AllocateManagedByteBuffer(maxHeaderSize))
using (IManagedByteBuffer buffer = config.MemoryAllocator.AllocateManagedByteBuffer(maxHeaderSize, AllocationOptions.Clean))
{
long startPosition = stream.Position;
stream.Read(buffer.Array, 0, maxHeaderSize);

18
src/ImageSharp/ImageFrameCollection.cs

@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp
public ImageFrame<TPixel> InsertFrame(int index, ImageFrame<TPixel> source)
{
this.ValidateFrame(source);
ImageFrame<TPixel> clonedFrame = source.Clone();
ImageFrame<TPixel> clonedFrame = source.Clone(this.parent.GetConfiguration());
this.frames.Insert(index, clonedFrame);
return clonedFrame;
}
@ -107,7 +107,7 @@ namespace SixLabors.ImageSharp
public ImageFrame<TPixel> AddFrame(ImageFrame<TPixel> source)
{
this.ValidateFrame(source);
ImageFrame<TPixel> clonedFrame = source.Clone();
ImageFrame<TPixel> clonedFrame = source.Clone(this.parent.GetConfiguration());
this.frames.Add(clonedFrame);
return clonedFrame;
}
@ -155,10 +155,7 @@ namespace SixLabors.ImageSharp
/// <returns>
/// <c>true</c> if the <seealso cref="ImageFrameCollection{TPixel}"/> contains the specified frame; otherwise, <c>false</c>.
/// </returns>
public bool Contains(ImageFrame<TPixel> frame)
{
return this.frames.Contains(frame);
}
public bool Contains(ImageFrame<TPixel> frame) => this.frames.Contains(frame);
/// <summary>
/// Moves an <seealso cref="ImageFrame{TPixel}"/> from <paramref name="sourceIndex"/> to <paramref name="destinationIndex"/>.
@ -195,7 +192,7 @@ namespace SixLabors.ImageSharp
this.frames.Remove(frame);
return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { frame });
return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.MetaData.DeepClone(), new[] { frame });
}
/// <summary>
@ -208,7 +205,7 @@ namespace SixLabors.ImageSharp
{
ImageFrame<TPixel> frame = this[index];
ImageFrame<TPixel> clonedFrame = frame.Clone();
return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.MetaData.Clone(), new[] { clonedFrame });
return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.MetaData.DeepClone(), new[] { clonedFrame });
}
/// <summary>
@ -217,10 +214,7 @@ namespace SixLabors.ImageSharp
/// <returns>
/// The new <see cref="ImageFrame{TPixel}" />.
/// </returns>
public ImageFrame<TPixel> CreateFrame()
{
return this.CreateFrame(default);
}
public ImageFrame<TPixel> CreateFrame() => this.CreateFrame(default);
/// <summary>
/// Creates a new <seealso cref="ImageFrame{TPixel}" /> and appends it to the end of the collection.

104
src/ImageSharp/ImageFrame{TPixel}.cs

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -95,6 +96,10 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class wrapping an existing buffer.
/// </summary>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="memorySource">The memory source.</param>
internal ImageFrame(Configuration configuration, int width, int height, MemorySource<TPixel> memorySource)
: this(configuration, width, height, memorySource, new ImageFrameMetaData())
{
@ -103,12 +108,12 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class wrapping an existing buffer.
/// </summary>
internal ImageFrame(
Configuration configuration,
int width,
int height,
MemorySource<TPixel> memorySource,
ImageFrameMetaData metaData)
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="memorySource">The memory source.</param>
/// <param name="metaData">The meta data.</param>
internal ImageFrame(Configuration configuration, int width, int height, MemorySource<TPixel> memorySource, ImageFrameMetaData metaData)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.MustBeGreaterThan(width, 0, nameof(width));
@ -135,7 +140,7 @@ namespace SixLabors.ImageSharp
this.MemoryAllocator = configuration.MemoryAllocator;
this.PixelBuffer = this.MemoryAllocator.Allocate2D<TPixel>(source.PixelBuffer.Width, source.PixelBuffer.Height);
source.PixelBuffer.GetSpan().CopyTo(this.PixelBuffer.GetSpan());
this.MetaData = source.MetaData.Clone();
this.MetaData = source.MetaData.DeepClone();
}
/// <summary>
@ -247,35 +252,61 @@ namespace SixLabors.ImageSharp
/// <inheritdoc/>
public override string ToString() => $"ImageFrame<{typeof(TPixel).Name}>: {this.Width}x{this.Height}";
/// <summary>
/// Clones the current instance.
/// </summary>
/// <returns>The <see cref="ImageFrame{TPixel}"/></returns>
internal ImageFrame<TPixel> Clone() => this.Clone(this.configuration);
/// <summary>
/// Clones the current instance.
/// </summary>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <returns>The <see cref="ImageFrame{TPixel}"/></returns>
internal ImageFrame<TPixel> Clone(Configuration configuration) => new ImageFrame<TPixel>(configuration, this);
/// <summary>
/// Returns a copy of the image frame in the given pixel format.
/// </summary>
/// <typeparam name="TPixel2">The pixel format.</typeparam>
/// <returns>The <see cref="ImageFrame{TPixel2}"/></returns>
internal ImageFrame<TPixel2> CloneAs<TPixel2>()
where TPixel2 : struct, IPixel<TPixel2> => this.CloneAs<TPixel2>(this.configuration);
/// <summary>
/// Returns a copy of the image frame in the given pixel format.
/// </summary>
/// <typeparam name="TPixel2">The pixel format.</typeparam>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <returns>The <see cref="ImageFrame{TPixel2}"/></returns>
internal ImageFrame<TPixel2> CloneAs<TPixel2>(Configuration configuration)
where TPixel2 : struct, IPixel<TPixel2>
{
if (typeof(TPixel2) == typeof(TPixel))
{
return this.Clone() as ImageFrame<TPixel2>;
return this.Clone(configuration) as ImageFrame<TPixel2>;
}
var target = new ImageFrame<TPixel2>(this.configuration, this.Width, this.Height, this.MetaData.Clone());
ParallelFor.WithTemporaryBuffer(
0,
this.Height,
this.configuration,
this.Width,
(int y, IMemoryOwner<Vector4> tempRowBuffer) =>
{
Span<TPixel> sourceRow = this.GetPixelRowSpan(y);
Span<TPixel2> targetRow = target.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.GetSpan();
PixelOperations<TPixel>.Instance.ToScaledVector4(sourceRow, tempRowSpan, sourceRow.Length);
PixelOperations<TPixel2>.Instance.PackFromScaledVector4(tempRowSpan, targetRow, targetRow.Length);
});
var target = new ImageFrame<TPixel2>(configuration, this.Width, this.Height, this.MetaData.DeepClone());
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
this.Bounds(),
configuration,
(rows, tempRowBuffer) =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = this.GetPixelRowSpan(y);
Span<TPixel2> targetRow = target.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.Span;
PixelOperations<TPixel>.Instance.ToScaledVector4(sourceRow, tempRowSpan, sourceRow.Length);
PixelOperations<TPixel2>.Instance.PackFromScaledVector4(
tempRowSpan,
targetRow,
targetRow.Length);
}
});
return target;
}
@ -287,22 +318,17 @@ namespace SixLabors.ImageSharp
/// <param name="value">The value to initialize the bitmap with.</param>
internal void Clear(ParallelOptions parallelOptions, TPixel value)
{
Parallel.For(
0,
this.Height,
parallelOptions,
y =>
{
Span<TPixel> targetRow = this.GetPixelRowSpan(y);
targetRow.Fill(value);
});
}
Span<TPixel> span = this.GetPixelSpan();
/// <summary>
/// Clones the current instance.
/// </summary>
/// <returns>The <see cref="ImageFrame{TPixel}"/></returns>
internal ImageFrame<TPixel> Clone() => new ImageFrame<TPixel>(this.configuration, this);
if (value.Equals(default))
{
span.Clear();
}
else
{
span.Fill(value);
}
}
/// <inheritdoc/>
void IDisposable.Dispose() => this.Dispose();

3
src/ImageSharp/ImageSharp.csproj.DotSettings

@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=common/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=common_005Cexceptions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

88
src/ImageSharp/Image{TPixel}.cs

@ -11,7 +11,6 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
namespace SixLabors.ImageSharp
{
@ -23,15 +22,12 @@ namespace SixLabors.ImageSharp
where TPixel : struct, IPixel<TPixel>
{
private readonly Configuration configuration;
private readonly ImageFrameCollection<TPixel> frames;
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// with the height and the width of the image.
/// </summary>
/// <param name="configuration">
/// The configuration providing initialization code which allows extending the library.
/// </param>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
public Image(Configuration configuration, int width, int height)
@ -43,9 +39,7 @@ namespace SixLabors.ImageSharp
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// with the height and the width of the image.
/// </summary>
/// <param name="configuration">
/// The configuration providing initialization code which allows extending the library.
/// </param>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="backgroundColor">The color to initialize the pixels with.</param>
@ -69,9 +63,7 @@ namespace SixLabors.ImageSharp
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// with the height and the width of the image.
/// </summary>
/// <param name="configuration">
/// The configuration providing initialization code which allows extending the library.
/// </param>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="metadata">The images metadata.</param>
@ -80,37 +72,41 @@ namespace SixLabors.ImageSharp
this.configuration = configuration ?? Configuration.Default;
this.PixelType = new PixelTypeInfo(Unsafe.SizeOf<TPixel>() * 8);
this.MetaData = metadata ?? new ImageMetaData();
this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
this.Frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
}
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// wrapping an external <see cref="MemorySource{T}"/>
/// </summary>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="memorySource">The memory source.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="metadata">The images metadata.</param>
internal Image(Configuration configuration, MemorySource<TPixel> memorySource, int width, int height, ImageMetaData metadata)
{
this.configuration = configuration;
this.PixelType = new PixelTypeInfo(Unsafe.SizeOf<TPixel>() * 8);
this.MetaData = metadata;
this.frames = new ImageFrameCollection<TPixel>(this, width, height, memorySource);
this.Frames = new ImageFrameCollection<TPixel>(this, width, height, memorySource);
}
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// with the height and the width of the image.
/// </summary>
/// <param name="configuration">
/// The configuration providing initialization code which allows extending the library.
/// </param>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="backgroundColor">The color to initialize the pixels with.</param>
/// <param name="metadata">The images metadata.</param>
internal Image(Configuration configuration, int width, int height, TPixel backgroundColor, ImageMetaData metadata) {
internal Image(Configuration configuration, int width, int height, TPixel backgroundColor, ImageMetaData metadata)
{
this.configuration = configuration ?? Configuration.Default;
this.PixelType = new PixelTypeInfo(Unsafe.SizeOf<TPixel>() * 8);
this.MetaData = metadata ?? new ImageMetaData();
this.frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor);
this.Frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor);
}
/// <summary>
@ -126,7 +122,7 @@ namespace SixLabors.ImageSharp
this.PixelType = new PixelTypeInfo(Unsafe.SizeOf<TPixel>() * 8);
this.MetaData = metadata ?? new ImageMetaData();
this.frames = new ImageFrameCollection<TPixel>(this, frames);
this.Frames = new ImageFrameCollection<TPixel>(this, frames);
}
/// <summary>
@ -138,10 +134,10 @@ namespace SixLabors.ImageSharp
public PixelTypeInfo PixelType { get; }
/// <inheritdoc/>
public int Width => this.frames.RootFrame.Width;
public int Width => this.Frames.RootFrame.Width;
/// <inheritdoc/>
public int Height => this.frames.RootFrame.Height;
public int Height => this.Frames.RootFrame.Height;
/// <inheritdoc/>
public ImageMetaData MetaData { get; }
@ -149,12 +145,12 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Gets the frames.
/// </summary>
public ImageFrameCollection<TPixel> Frames => this.frames;
public ImageFrameCollection<TPixel> Frames { get; }
/// <summary>
/// Gets the root frame.
/// </summary>
private IPixelSource<TPixel> PixelSource => this.frames?.RootFrame ?? throw new ObjectDisposedException(nameof(Image<TPixel>));
private IPixelSource<TPixel> PixelSource => this.Frames?.RootFrame ?? throw new ObjectDisposedException(nameof(Image<TPixel>));
/// <summary>
/// Gets or sets the pixel at the specified position.
@ -187,16 +183,17 @@ namespace SixLabors.ImageSharp
/// Clones the current image
/// </summary>
/// <returns>Returns a new image with all the same metadata as the original.</returns>
public Image<TPixel> Clone()
{
IEnumerable<ImageFrame<TPixel>> clonedFrames = this.frames.Select(x => x.Clone());
return new Image<TPixel>(this.configuration, this.MetaData.Clone(), clonedFrames);
}
public Image<TPixel> Clone() => this.Clone(this.configuration);
/// <inheritdoc/>
public override string ToString()
/// <summary>
/// Clones the current image with the given configueation.
/// </summary>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <returns>Returns a new <see cref="Image{TPixel}"/> with all the same pixel data as the original.</returns>
public Image<TPixel> Clone(Configuration configuration)
{
return $"Image<{typeof(TPixel).Name}>: {this.Width}x{this.Height}";
IEnumerable<ImageFrame<TPixel>> clonedFrames = this.Frames.Select(x => x.Clone(configuration));
return new Image<TPixel>(configuration, this.MetaData.DeepClone(), clonedFrames);
}
/// <summary>
@ -205,22 +202,27 @@ namespace SixLabors.ImageSharp
/// <typeparam name="TPixel2">The pixel format.</typeparam>
/// <returns>The <see cref="Image{TPixel2}"/></returns>
public Image<TPixel2> CloneAs<TPixel2>()
where TPixel2 : struct, IPixel<TPixel2>
{
IEnumerable<ImageFrame<TPixel2>> clonedFrames = this.frames.Select(x => x.CloneAs<TPixel2>());
var target = new Image<TPixel2>(this.configuration, this.MetaData.Clone(), clonedFrames);
return target;
}
where TPixel2 : struct, IPixel<TPixel2> => this.CloneAs<TPixel2>(this.configuration);
/// <summary>
/// Releases managed resources.
/// Returns a copy of the image in the given pixel format.
/// </summary>
public void Dispose()
/// <typeparam name="TPixel2">The pixel format.</typeparam>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <returns>The <see cref="Image{TPixel2}"/></returns>
public Image<TPixel2> CloneAs<TPixel2>(Configuration configuration)
where TPixel2 : struct, IPixel<TPixel2>
{
this.frames.Dispose();
IEnumerable<ImageFrame<TPixel2>> clonedFrames = this.Frames.Select(x => x.CloneAs<TPixel2>(configuration));
return new Image<TPixel2>(configuration, this.MetaData.DeepClone(), clonedFrames);
}
/// <inheritdoc/>
public void Dispose() => this.Frames.Dispose();
/// <inheritdoc/>
public override string ToString() => $"Image<{typeof(TPixel).Name}>: {this.Width}x{this.Height}";
/// <summary>
/// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer.
/// </summary>
@ -229,9 +231,9 @@ namespace SixLabors.ImageSharp
{
Guard.NotNull(pixelSource, nameof(pixelSource));
for (int i = 0; i < this.frames.Count; i++)
for (int i = 0; i < this.Frames.Count; i++)
{
this.frames[i].SwapOrCopyPixelsBufferFrom(pixelSource.frames[i]);
this.Frames[i].SwapOrCopyPixelsBufferFrom(pixelSource.Frames[i]);
}
}
}

20
src/ImageSharp/Memory/Buffer2DExtensions.cs

@ -96,15 +96,14 @@ namespace SixLabors.ImageSharp.Memory
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <param name="rectangle">The rectangle subarea</param>
/// <returns>The <see cref="BufferArea{T}"/></returns>
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, Rectangle rectangle)
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, in Rectangle rectangle)
where T : struct => new BufferArea<T>(buffer, rectangle);
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer, int x, int y, int width, int height)
where T : struct
{
var rectangle = new Rectangle(x, y, width, height);
return new BufferArea<T>(buffer, rectangle);
}
where T : struct => new BufferArea<T>(buffer, new Rectangle(x, y, width, height));
public static BufferArea<T> GetAreaBetweenRows<T>(this Buffer2D<T> buffer, int minY, int maxY)
where T : struct => new BufferArea<T>(buffer, new Rectangle(0, minY, buffer.Width, maxY - minY));
/// <summary>
/// Return a <see cref="BufferArea{T}"/> to the whole area of 'buffer'
@ -114,5 +113,14 @@ namespace SixLabors.ImageSharp.Memory
/// <returns>The <see cref="BufferArea{T}"/></returns>
public static BufferArea<T> GetArea<T>(this Buffer2D<T> buffer)
where T : struct => new BufferArea<T>(buffer);
/// <summary>
/// Gets a span for all the pixels in <paramref name="buffer"/> defined by <paramref name="rows"/>
/// </summary>
public static Span<T> GetMultiRowSpan<T>(this Buffer2D<T> buffer, in RowInterval rows)
where T : struct
{
return buffer.Span.Slice(rows.Min * buffer.Width, rows.Height * buffer.Width);
}
}
}

45
src/ImageSharp/Memory/RowInterval.cs

@ -0,0 +1,45 @@
// Copyright(c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// Represents an interval of rows in a <see cref="Rectangle"/> and/or <see cref="Buffer2D{T}"/>
/// </summary>
internal readonly struct RowInterval
{
/// <summary>
/// Initializes a new instance of the <see cref="RowInterval"/> struct.
/// </summary>
public RowInterval(int min, int max)
{
DebugGuard.MustBeLessThan(min, max, nameof(min));
this.Min = min;
this.Max = max;
}
/// <summary>
/// Gets the INCLUSIVE minimum
/// </summary>
public int Min { get; }
/// <summary>
/// Gets the EXCLUSIVE maximum
/// </summary>
public int Max { get; }
/// <summary>
/// Gets the difference (<see cref="Max"/> - <see cref="Min"/>)
/// </summary>
public int Height => this.Max - this.Min;
/// <inheritdoc />
public override string ToString()
{
return $"RowInterval [{this.Min}->{this.Max}[";
}
}
}

20
src/ImageSharp/MetaData/ImageFrameMetaData.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;
@ -10,9 +9,9 @@ namespace SixLabors.ImageSharp.MetaData
/// <summary>
/// Encapsulates the metadata of an image frame.
/// </summary>
public sealed class ImageFrameMetaData
public sealed class ImageFrameMetaData : IDeepCloneable<ImageFrameMetaData>
{
private readonly Dictionary<IImageFormat, object> formatMetaData = new Dictionary<IImageFormat, object>();
private readonly Dictionary<IImageFormat, IDeepCloneable> formatMetaData = new Dictionary<IImageFormat, IDeepCloneable>();
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrameMetaData"/> class.
@ -32,17 +31,14 @@ namespace SixLabors.ImageSharp.MetaData
{
DebugGuard.NotNull(other, nameof(other));
foreach (KeyValuePair<IImageFormat, object> meta in other.formatMetaData)
foreach (KeyValuePair<IImageFormat, IDeepCloneable> meta in other.formatMetaData)
{
this.formatMetaData.Add(meta.Key, meta.Value);
this.formatMetaData.Add(meta.Key, meta.Value.DeepClone());
}
}
/// <summary>
/// Clones this ImageFrameMetaData.
/// </summary>
/// <returns>The cloned instance.</returns>
public ImageFrameMetaData Clone() => new ImageFrameMetaData(this);
/// <inheritdoc/>
public ImageFrameMetaData DeepClone() => new ImageFrameMetaData(this);
/// <summary>
/// Gets the metadata value associated with the specified key.
@ -55,9 +51,9 @@ namespace SixLabors.ImageSharp.MetaData
/// </returns>
public TFormatFrameMetaData GetFormatMetaData<TFormatMetaData, TFormatFrameMetaData>(IImageFormat<TFormatMetaData, TFormatFrameMetaData> key)
where TFormatMetaData : class
where TFormatFrameMetaData : class
where TFormatFrameMetaData : class, IDeepCloneable
{
if (this.formatMetaData.TryGetValue(key, out object meta))
if (this.formatMetaData.TryGetValue(key, out IDeepCloneable meta))
{
return (TFormatFrameMetaData)meta;
}

38
src/ImageSharp/MetaData/ImageMetaData.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
@ -12,7 +11,7 @@ namespace SixLabors.ImageSharp.MetaData
/// <summary>
/// Encapsulates the metadata of an image.
/// </summary>
public sealed class ImageMetaData
public sealed class ImageMetaData : IDeepCloneable<ImageMetaData>
{
/// <summary>
/// The default horizontal resolution value (dots per inch) in x direction.
@ -26,7 +25,13 @@ namespace SixLabors.ImageSharp.MetaData
/// </summary>
public const double DefaultVerticalResolution = 96;
private readonly Dictionary<IImageFormat, object> formatMetaData = new Dictionary<IImageFormat, object>();
/// <summary>
/// The default pixel resolution units.
/// <remarks>The default value is <see cref="PixelResolutionUnit.PixelsPerInch"/>.</remarks>
/// </summary>
public const PixelResolutionUnit DefaultPixelResolutionUnits = PixelResolutionUnit.PixelsPerInch;
private readonly Dictionary<IImageFormat, IDeepCloneable> formatMetaData = new Dictionary<IImageFormat, IDeepCloneable>();
private double horizontalResolution;
private double verticalResolution;
@ -37,6 +42,7 @@ namespace SixLabors.ImageSharp.MetaData
{
this.horizontalResolution = DefaultHorizontalResolution;
this.verticalResolution = DefaultVerticalResolution;
this.ResolutionUnits = DefaultPixelResolutionUnits;
}
/// <summary>
@ -52,9 +58,9 @@ namespace SixLabors.ImageSharp.MetaData
this.VerticalResolution = other.VerticalResolution;
this.ResolutionUnits = other.ResolutionUnits;
foreach (KeyValuePair<IImageFormat, object> meta in other.formatMetaData)
foreach (KeyValuePair<IImageFormat, IDeepCloneable> meta in other.formatMetaData)
{
this.formatMetaData.Add(meta.Key, meta.Value);
this.formatMetaData.Add(meta.Key, meta.Value.DeepClone());
}
foreach (ImageProperty property in other.Properties)
@ -62,13 +68,8 @@ namespace SixLabors.ImageSharp.MetaData
this.Properties.Add(property);
}
this.ExifProfile = other.ExifProfile != null
? new ExifProfile(other.ExifProfile)
: null;
this.IccProfile = other.IccProfile != null
? new IccProfile(other.IccProfile)
: null;
this.ExifProfile = other.ExifProfile?.DeepClone();
this.IccProfile = other.IccProfile?.DeepClone();
}
/// <summary>
@ -114,7 +115,7 @@ namespace SixLabors.ImageSharp.MetaData
/// 02 : Pixels per centimeter
/// 03 : Pixels per meter
/// </summary>
public PixelResolutionUnit ResolutionUnits { get; set; } = PixelResolutionUnit.PixelsPerInch;
public PixelResolutionUnit ResolutionUnits { get; set; }
/// <summary>
/// Gets or sets the Exif profile.
@ -140,9 +141,9 @@ namespace SixLabors.ImageSharp.MetaData
/// The <typeparamref name="TFormatMetaData"/>.
/// </returns>
public TFormatMetaData GetFormatMetaData<TFormatMetaData>(IImageFormat<TFormatMetaData> key)
where TFormatMetaData : class
where TFormatMetaData : class, IDeepCloneable
{
if (this.formatMetaData.TryGetValue(key, out object meta))
if (this.formatMetaData.TryGetValue(key, out IDeepCloneable meta))
{
return (TFormatMetaData)meta;
}
@ -152,11 +153,8 @@ namespace SixLabors.ImageSharp.MetaData
return newMeta;
}
/// <summary>
/// Clones this into a new instance
/// </summary>
/// <returns>The cloned metadata instance</returns>
public ImageMetaData Clone() => new ImageMetaData(this);
/// <inheritdoc/>
public ImageMetaData DeepClone() => new ImageMetaData(this);
/// <summary>
/// Looks up a property with the provided name.

27
src/ImageSharp/MetaData/Profiles/Exif/ExifProfile.cs

@ -12,23 +12,18 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
/// <summary>
/// Represents an EXIF profile providing access to the collection of values.
/// </summary>
public sealed class ExifProfile
public sealed class ExifProfile : IDeepCloneable<ExifProfile>
{
/// <summary>
/// The byte array to read the EXIF profile from.
/// </summary>
private byte[] data;
private readonly byte[] data;
/// <summary>
/// The collection of EXIF values
/// </summary>
private List<ExifValue> values;
/// <summary>
/// The list of invalid EXIF tags
/// </summary>
private IReadOnlyList<ExifTag> invalidTags;
/// <summary>
/// The thumbnail offset position in the byte stream
/// </summary>
@ -55,7 +50,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
{
this.Parts = ExifParts.All;
this.data = data;
this.invalidTags = new List<ExifTag>();
this.InvalidTags = new List<ExifTag>();
}
/// <summary>
@ -63,22 +58,19 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
/// by making a copy from another EXIF profile.
/// </summary>
/// <param name="other">The other EXIF profile, where the clone should be made from.</param>
/// <exception cref="System.ArgumentNullException"><paramref name="other"/> is null.</exception>
public ExifProfile(ExifProfile other)
private ExifProfile(ExifProfile other)
{
Guard.NotNull(other, nameof(other));
this.Parts = other.Parts;
this.thumbnailLength = other.thumbnailLength;
this.thumbnailOffset = other.thumbnailOffset;
this.invalidTags = new List<ExifTag>(other.invalidTags);
this.InvalidTags = new List<ExifTag>(other.InvalidTags);
if (other.values != null)
{
this.values = new List<ExifValue>(other.Values.Count);
foreach (ExifValue value in other.Values)
{
this.values.Add(new ExifValue(value));
this.values.Add(value.DeepClone());
}
}
@ -97,7 +89,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
/// <summary>
/// Gets the tags that where found but contained an invalid value.
/// </summary>
public IReadOnlyList<ExifTag> InvalidTags => this.invalidTags;
public IReadOnlyList<ExifTag> InvalidTags { get; private set; }
/// <summary>
/// Gets the values of this EXIF profile.
@ -249,6 +241,9 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
return writer.GetData();
}
/// <inheritdoc/>
public ExifProfile DeepClone() => new ExifProfile(this);
/// <summary>
/// Synchronizes the profiles with the specified meta data.
/// </summary>
@ -294,7 +289,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
this.values = reader.ReadValues();
this.invalidTags = new List<ExifTag>(reader.InvalidTags);
this.InvalidTags = new List<ExifTag>(reader.InvalidTags);
this.thumbnailOffset = (int)reader.ThumbnailOffset;
this.thumbnailLength = (int)reader.ThumbnailLength;
}

13
src/ImageSharp/MetaData/Profiles/Exif/ExifReader.cs

@ -127,25 +127,14 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
private unsafe string ConvertToString(ReadOnlySpan<byte> buffer)
{
Span<byte> nullChar = stackalloc byte[1] { 0 };
int nullCharIndex = buffer.IndexOf(nullChar);
int nullCharIndex = buffer.IndexOf((byte)0);
if (nullCharIndex > -1)
{
buffer = buffer.Slice(0, nullCharIndex);
}
#if NETSTANDARD1_1
return Encoding.UTF8.GetString(buffer.ToArray(), 0, buffer.Length);
#elif NETCOREAPP2_1
return Encoding.UTF8.GetString(buffer);
#else
fixed (byte* pointer = &MemoryMarshal.GetReference(buffer))
{
return Encoding.UTF8.GetString(pointer, buffer.Length);
}
#endif
}
/// <summary>

69
src/ImageSharp/MetaData/Profiles/Exif/ExifValue.cs

@ -11,15 +11,30 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
/// <summary>
/// Represent the value of the EXIF profile.
/// </summary>
public sealed class ExifValue : IEquatable<ExifValue>
public sealed class ExifValue : IEquatable<ExifValue>, IDeepCloneable<ExifValue>
{
/// <summary>
/// Initializes a new instance of the <see cref="ExifValue"/> class.
/// </summary>
/// <param name="tag">The tag.</param>
/// <param name="dataType">The data type.</param>
/// <param name="value">The value.</param>
/// <param name="isArray">Whether the value is an array.</param>
internal ExifValue(ExifTag tag, ExifDataType dataType, object value, bool isArray)
{
this.Tag = tag;
this.DataType = dataType;
this.IsArray = isArray && dataType != ExifDataType.Ascii;
this.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExifValue"/> class
/// by making a copy from another exif value.
/// </summary>
/// <param name="other">The other exif value, where the clone should be made from.</param>
/// <exception cref="System.ArgumentNullException"><paramref name="other"/> is null.</exception>
public ExifValue(ExifValue other)
/// <exception cref="ArgumentNullException"><paramref name="other"/> is null.</exception>
private ExifValue(ExifValue other)
{
Guard.NotNull(other, nameof(other));
@ -29,30 +44,17 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
if (!other.IsArray)
{
// All types are value types except for string which is immutable so safe to simply assign.
this.Value = other.Value;
}
else
{
// All array types are value types so Clone() is sufficient here.
var array = (Array)other.Value;
this.Value = array.Clone();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ExifValue"/> class.
/// </summary>
/// <param name="tag">The tag.</param>
/// <param name="dataType">The data type.</param>
/// <param name="value">The value.</param>
/// <param name="isArray">Whether the value is an array.</param>
internal ExifValue(ExifTag tag, ExifDataType dataType, object value, bool isArray)
{
this.Tag = tag;
this.DataType = dataType;
this.IsArray = isArray && dataType != ExifDataType.Ascii;
this.Value = value;
}
/// <summary>
/// Gets the data type of the exif value.
/// </summary>
@ -145,10 +147,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
/// <returns>
/// True if the <paramref name="left"/> parameter is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator ==(ExifValue left, ExifValue right)
{
return ReferenceEquals(left, right) || left.Equals(right);
}
public static bool operator ==(ExifValue left, ExifValue right) => ReferenceEquals(left, right) || left.Equals(right);
/// <summary>
/// Compares two <see cref="ExifValue"/> objects for equality.
@ -162,16 +161,10 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
/// <returns>
/// True if the <paramref name="left"/> parameter is not equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator !=(ExifValue left, ExifValue right)
{
return !(left == right);
}
public static bool operator !=(ExifValue left, ExifValue right) => !(left == right);
/// <inheritdoc />
public override bool Equals(object obj)
{
return obj is ExifValue other && this.Equals(other);
}
public override bool Equals(object obj) => obj is ExifValue other && this.Equals(other);
/// <inheritdoc />
public bool Equals(ExifValue other)
@ -187,9 +180,9 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
}
return
this.Tag == other.Tag &&
this.DataType == other.DataType &&
object.Equals(this.Value, other.Value);
this.Tag == other.Tag
&& this.DataType == other.DataType
&& object.Equals(this.Value, other.Value);
}
/// <summary>
@ -205,10 +198,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.GetHashCode(this);
}
public override int GetHashCode() => this.GetHashCode(this);
/// <inheritdoc/>
public override string ToString()
@ -238,6 +228,9 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
return sb.ToString();
}
/// <inheritdoc/>
public ExifValue DeepClone() => new ExifValue(this);
/// <summary>
/// Creates a new <see cref="ExifValue"/>
/// </summary>
@ -584,7 +577,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
private static ExifValue CreateNumber(ExifTag tag, object value, bool isArray)
{
Type type = value?.GetType();
if (type != null && type.IsArray)
if (type?.IsArray == true)
{
type = type.GetElementType();
}

36
src/ImageSharp/MetaData/Profiles/ICC/IccProfile.cs

@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Icc
/// <summary>
/// Represents an ICC profile
/// </summary>
public sealed class IccProfile
public sealed class IccProfile : IDeepCloneable<IccProfile>
{
/// <summary>
/// The byte array to read the ICC profile from
@ -42,23 +42,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Icc
/// Initializes a new instance of the <see cref="IccProfile"/> class.
/// </summary>
/// <param name="data">The raw ICC profile data</param>
public IccProfile(byte[] data)
{
this.data = data;
}
/// <summary>
/// Initializes a new instance of the <see cref="IccProfile"/> class
/// by making a copy from another ICC profile.
/// </summary>
/// <param name="other">The other ICC profile, where the clone should be made from.</param>
/// <exception cref="ArgumentNullException"><paramref name="other"/> is null.</exception>>
public IccProfile(IccProfile other)
{
Guard.NotNull(other, nameof(other));
this.data = other.ToByteArray();
}
public IccProfile(byte[] data) => this.data = data;
/// <summary>
/// Initializes a new instance of the <see cref="IccProfile"/> class.
@ -74,6 +58,19 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Icc
this.entries = new List<IccTagDataEntry>(entries);
}
/// <summary>
/// Initializes a new instance of the <see cref="IccProfile"/> class
/// by making a copy from another ICC profile.
/// </summary>
/// <param name="other">The other ICC profile, where the clone should be made from.</param>
/// <exception cref="ArgumentNullException"><paramref name="other"/> is null.</exception>>
private IccProfile(IccProfile other)
{
Guard.NotNull(other, nameof(other));
this.data = other.ToByteArray();
}
/// <summary>
/// Gets or sets the profile header
/// </summary>
@ -100,6 +97,9 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Icc
}
}
/// <inheritdoc/>
public IccProfile DeepClone() => new IccProfile(this);
#if !NETSTANDARD1_1
/// <summary>

43
src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs

@ -2,10 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Binarization
@ -56,7 +56,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
public TPixel LowerColor { get; set; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
protected override void OnFrameApply(
ImageFrame<TPixel> source,
Rectangle sourceRectangle,
Configuration configuration)
{
float threshold = this.Threshold * 255F;
TPixel upper = this.UpperColor;
@ -70,25 +73,29 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization
bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8);
ParallelFor.WithConfiguration(
startY,
endY,
var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY);
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
rows =>
{
Span<TPixel> row = source.GetPixelRowSpan(y);
Rgba32 rgba = default;
for (int x = startX; x < endX; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
ref TPixel color = ref row[x];
color.ToRgba32(ref rgba);
Span<TPixel> row = source.GetPixelRowSpan(y);
Rgba32 rgba = default;
for (int x = startX; x < endX; x++)
{
ref TPixel color = ref row[x];
color.ToRgba32(ref rgba);
// Convert to grayscale using ITU-R Recommendation BT.709 if required
float luminance = isAlphaOnly
? rgba.A
: (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B);
color = luminance >= threshold ? upper : lower;
// Convert to grayscale using ITU-R Recommendation BT.709 if required
float luminance = isAlphaOnly
? rgba.A
: (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B);
color = luminance >= threshold ? upper : lower;
}
}
});
}

119
src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs

@ -3,12 +3,12 @@
using System;
using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Convolution
@ -42,7 +42,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
public DenseMatrix<float> KernelY { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
protected override void OnFrameApply(
ImageFrame<TPixel> source,
Rectangle sourceRectangle,
Configuration configuration)
{
int kernelYHeight = this.KernelY.Rows;
int kernelYWidth = this.KernelY.Columns;
@ -58,71 +61,77 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
int maxY = endY - 1;
int maxX = endX - 1;
using (Buffer2D<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height))
using (Buffer2D<TPixel> targetPixels =
configuration.MemoryAllocator.Allocate2D<TPixel>(source.Width, source.Height))
{
source.CopyTo(targetPixels);
ParallelFor.WithConfiguration(
startY,
endY,
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY);
for (int x = startX; x < endX; x++)
ParallelHelper.IterateRows(
workingRectangle,
configuration,
rows =>
{
float rX = 0;
float gX = 0;
float bX = 0;
float rY = 0;
float gY = 0;
float bY = 0;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelYHeight; fy++)
for (int y = rows.Min; y < rows.Max; y++)
{
int fyr = fy - radiusY;
int offsetY = y + fyr;
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
for (int fx = 0; fx < kernelXWidth; fx++)
for (int x = startX; x < endX; x++)
{
int fxr = fx - radiusX;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
if (fy < kernelXHeight)
float rX = 0;
float gX = 0;
float bX = 0;
float rY = 0;
float gY = 0;
float bY = 0;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelYHeight; fy++)
{
Vector4 kx = this.KernelX[fy, fx] * currentColor;
rX += kx.X;
gX += kx.Y;
bX += kx.Z;
int fyr = fy - radiusY;
int offsetY = y + fyr;
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
for (int fx = 0; fx < kernelXWidth; fx++)
{
int fxr = fx - radiusX;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
if (fy < kernelXHeight)
{
Vector4 kx = this.KernelX[fy, fx] * currentColor;
rX += kx.X;
gX += kx.Y;
bX += kx.Z;
}
if (fx < kernelYWidth)
{
Vector4 ky = this.KernelY[fy, fx] * currentColor;
rY += ky.X;
gY += ky.Y;
bY += ky.Z;
}
}
}
if (fx < kernelYWidth)
{
Vector4 ky = this.KernelY[fy, fx] * currentColor;
rY += ky.X;
gY += ky.Y;
bY += ky.Z;
}
float red = MathF.Sqrt((rX * rX) + (rY * rY));
float green = MathF.Sqrt((gX * gX) + (gY * gY));
float blue = MathF.Sqrt((bX * bX) + (bY * bY));
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(
new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply());
}
}
float red = MathF.Sqrt((rX * rX) + (rY * rY));
float green = MathF.Sqrt((gX * gX) + (gY * gY));
float blue = MathF.Sqrt((bX * bX) + (bY * bY));
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply());
}
});
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}

61
src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs

@ -6,6 +6,7 @@ using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors;
@ -82,43 +83,47 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
int maxY = endY - 1;
int maxX = endX - 1;
ParallelFor.WithConfiguration(
startY,
endY,
configuration,
y =>
{
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
var workingRectangle = Rectangle.FromLTRB(startX, startY, endX, endY);
for (int x = startX; x < endX; x++)
ParallelHelper.IterateRows(
workingRectangle,
configuration,
rows =>
{
Vector4 destination = default;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelHeight; fy++)
for (int y = rows.Min; y < rows.Max; y++)
{
int fyr = fy - radiusY;
int offsetY = y + fyr;
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> row = sourcePixels.GetRowSpan(offsetY);
for (int fx = 0; fx < kernelWidth; fx++)
for (int x = startX; x < endX; x++)
{
int fxr = fx - radiusX;
int offsetX = x + fxr;
Vector4 destination = default;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelHeight; fy++)
{
int fyr = fy - radiusY;
int offsetY = y + fyr;
offsetX = offsetX.Clamp(0, maxX);
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> row = sourcePixels.GetRowSpan(offsetY);
Vector4 currentColor = row[offsetX].ToVector4().Premultiply();
destination += kernel[fy, fx] * currentColor;
for (int fx = 0; fx < kernelWidth; fx++)
{
int fxr = fx - radiusX;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = row[offsetX].ToVector4().Premultiply();
destination += kernel[fy, fx] * currentColor;
}
}
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(destination.UnPremultiply());
}
}
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(destination.UnPremultiply());
}
});
});
}
}
}

94
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs

@ -6,6 +6,7 @@ using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors;
@ -52,50 +53,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
{
source.CopyTo(targetPixels);
ParallelFor.WithConfiguration(
startY,
endY,
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = startX; x < endX; x++)
{
float red = 0;
float green = 0;
float blue = 0;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelLength; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
for (int fx = 0; fx < kernelLength; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
currentColor *= this.KernelXY[fy, fx];
red += currentColor.X;
green += currentColor.Y;
blue += currentColor.Z;
}
}
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply());
}
});
var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY);
ParallelHelper.IterateRows(
workingRect,
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = startX; x < endX; x++)
{
float red = 0;
float green = 0;
float blue = 0;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelLength; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
offsetY = offsetY.Clamp(0, maxY);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
for (int fx = 0; fx < kernelLength; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourceOffsetRow[offsetX].ToVector4().Premultiply();
currentColor *= this.KernelXY[fy, fx];
red += currentColor.X;
green += currentColor.Y;
blue += currentColor.Z;
}
}
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(
new Vector4(red, green, blue, sourceRow[x].ToVector4().W).UnPremultiply());
}
}
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}

42
src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors.Filters;
@ -124,6 +125,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
shiftY = 0;
}
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
// Additional runs.
// ReSharper disable once ForCanBeConvertedToForeach
for (int i = 1; i < kernels.Length; i++)
@ -135,30 +138,35 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
Buffer2D<TPixel> passPixels = pass.PixelBuffer;
Buffer2D<TPixel> targetPixels = source.PixelBuffer;
ParallelFor.WithConfiguration(
minY,
maxY,
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
rows =>
{
int offsetY = y - shiftY;
for (int y = rows.Min; y < rows.Max; y++)
{
int offsetY = y - shiftY;
ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(passPixels.GetRowSpan(offsetY));
ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(targetPixels.GetRowSpan(offsetY));
ref TPixel passPixelsBase =
ref MemoryMarshal.GetReference(passPixels.GetRowSpan(offsetY));
ref TPixel targetPixelsBase =
ref MemoryMarshal.GetReference(targetPixels.GetRowSpan(offsetY));
for (int x = minX; x < maxX; x++)
{
int offsetX = x - shiftX;
for (int x = minX; x < maxX; x++)
{
int offsetX = x - shiftX;
// Grab the max components of the two pixels
ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, offsetX);
ref TPixel currentTargetPixel = ref Unsafe.Add(ref targetPixelsBase, offsetX);
// Grab the max components of the two pixels
ref TPixel currentPassPixel = ref Unsafe.Add(ref passPixelsBase, offsetX);
ref TPixel currentTargetPixel =
ref Unsafe.Add(ref targetPixelsBase, offsetX);
var pixelValue = Vector4.Max(
currentPassPixel.ToVector4(),
currentTargetPixel.ToVector4());
var pixelValue = Vector4.Max(
currentPassPixel.ToVector4(),
currentTargetPixel.ToVector4());
currentTargetPixel.PackFromVector4(pixelValue);
currentTargetPixel.PackFromVector4(pixelValue);
}
}
});
}

110
src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs

@ -3,11 +3,11 @@
using System;
using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Effects
@ -49,7 +49,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
public int BrushSize { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
protected override void OnFrameApply(
ImageFrame<TPixel> source,
Rectangle sourceRectangle,
Configuration configuration)
{
if (this.BrushSize <= 0 || this.BrushSize > source.Height || this.BrushSize > source.Width)
{
@ -70,69 +73,74 @@ namespace SixLabors.ImageSharp.Processing.Processors.Effects
{
source.CopyTo(targetPixels);
ParallelFor.WithConfiguration(
startY,
maxY,
var workingRect = Rectangle.FromLTRB(startX, startY, endX, endY);
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = startX; x < endX; x++)
rows =>
{
int maxIntensity = 0;
int maxIndex = 0;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
int[] intensityBin = new int[levels];
float[] redBin = new float[levels];
float[] blueBin = new float[levels];
float[] greenBin = new float[levels];
for (int x = startX; x < endX; x++)
{
int maxIntensity = 0;
int maxIndex = 0;
for (int fy = 0; fy <= radius; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
int[] intensityBin = new int[levels];
float[] redBin = new float[levels];
float[] blueBin = new float[levels];
float[] greenBin = new float[levels];
offsetY = offsetY.Clamp(0, maxY);
for (int fy = 0; fy <= radius; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
offsetY = offsetY.Clamp(0, maxY);
for (int fx = 0; fx <= radius; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Span<TPixel> sourceOffsetRow = source.GetPixelRowSpan(offsetY);
var vector = sourceOffsetRow[offsetX].ToVector4();
for (int fx = 0; fx <= radius; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
float sourceRed = vector.X;
float sourceBlue = vector.Z;
float sourceGreen = vector.Y;
var vector = sourceOffsetRow[offsetX].ToVector4();
int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (levels - 1));
float sourceRed = vector.X;
float sourceBlue = vector.Z;
float sourceGreen = vector.Y;
intensityBin[currentIntensity]++;
blueBin[currentIntensity] += sourceBlue;
greenBin[currentIntensity] += sourceGreen;
redBin[currentIntensity] += sourceRed;
int currentIntensity = (int)MathF.Round(
(sourceBlue + sourceGreen + sourceRed) / 3F * (levels - 1));
if (intensityBin[currentIntensity] > maxIntensity)
{
maxIntensity = intensityBin[currentIntensity];
maxIndex = currentIntensity;
}
}
intensityBin[currentIntensity]++;
blueBin[currentIntensity] += sourceBlue;
greenBin[currentIntensity] += sourceGreen;
redBin[currentIntensity] += sourceRed;
float red = MathF.Abs(redBin[maxIndex] / maxIntensity);
float green = MathF.Abs(greenBin[maxIndex] / maxIntensity);
float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity);
if (intensityBin[currentIntensity] > maxIntensity)
{
maxIntensity = intensityBin[currentIntensity];
maxIndex = currentIntensity;
}
}
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(new Vector4(red, green, blue, sourceRow[x].ToVector4().W));
float red = MathF.Abs(redBin[maxIndex] / maxIntensity);
float green = MathF.Abs(greenBin[maxIndex] / maxIntensity);
float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity);
ref TPixel pixel = ref targetRow[x];
pixel.PackFromVector4(
new Vector4(red, green, blue, sourceRow[x].ToVector4().W));
}
}
}
}
});
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}

28
src/ImageSharp/Processing/Processors/Filters/FilterProcessor.cs

@ -5,6 +5,7 @@ using System;
using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
@ -35,25 +36,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Filters
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
var interest = Rectangle.Intersect(sourceRectangle, source.Bounds());
int startY = interest.Y;
int endY = interest.Bottom;
int startX = interest.X;
int endX = interest.Right;
Matrix4x4 matrix = this.Matrix;
ParallelFor.WithConfiguration(
startY,
endY,
ParallelHelper.IterateRows(
interest,
configuration,
y =>
rows =>
{
Span<TPixel> row = source.GetPixelRowSpan(y);
for (int x = startX; x < endX; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
ref TPixel pixel = ref row[x];
var vector = Vector4.Transform(pixel.ToVector4(), matrix);
pixel.PackFromVector4(vector);
Span<TPixel> row = source.GetPixelRowSpan(y);
for (int x = interest.X; x < interest.Right; x++)
{
ref TPixel pixel = ref row[x];
var vector = Vector4.Transform(pixel.ToVector4(), matrix);
pixel.PackFromVector4(vector);
}
}
});
}

38
src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor.cs

@ -6,6 +6,7 @@ using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -67,6 +68,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
int width = maxX - minX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
using (IMemoryOwner<TPixel> colors = source.MemoryAllocator.Allocate<TPixel>(width))
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width))
{
@ -74,25 +77,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
Span<TPixel> colorSpan = colors.GetSpan();
Span<float> amountSpan = amount.GetSpan();
// TODO: Use Span.Fill?
for (int i = 0; i < width; i++)
{
colorSpan[i] = this.Color;
amountSpan[i] = this.GraphicsOptions.BlendPercentage;
}
colorSpan.Fill(this.Color);
amountSpan.Fill(this.GraphicsOptions.BlendPercentage);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(this.GraphicsOptions);
ParallelFor.WithConfiguration(
minY,
maxY,
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
{
Span<TPixel> destination = source.GetPixelRowSpan(y - startY).Slice(minX - startX, width);
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destination =
source.GetPixelRowSpan(y - startY).Slice(minX - startX, width);
// This switched color & destination in the 2nd and 3rd places because we are applying the target color under the current one
blender.Blend(source.MemoryAllocator, destination, colors.GetSpan(), destination, amount.GetSpan());
});
// This switched color & destination in the 2nd and 3rd places because we are applying the target color under the current one
blender.Blend(
source.MemoryAllocator,
destination,
colors.GetSpan(),
destination,
amount.GetSpan());
}
});
}
}
}

59
src/ImageSharp/Processing/Processors/Overlays/GlowProcessor.cs

@ -7,6 +7,7 @@ using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.Memory;
@ -84,6 +85,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
// TODO: can we simplify the rectangle calculation?
int startY = sourceRectangle.Y;
int endY = sourceRectangle.Bottom;
int startX = sourceRectangle.X;
@ -113,36 +115,43 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
}
int width = maxX - minX;
int offsetX = minX - startX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
using (IMemoryOwner<TPixel> rowColors = source.MemoryAllocator.Allocate<TPixel>(width))
{
// Be careful! Do not capture rowColorsSpan in the lambda below!
Span<TPixel> rowColorsSpan = rowColors.GetSpan();
for (int i = 0; i < width; i++)
{
rowColorsSpan[i] = glowColor;
}
rowColors.GetSpan().Fill(glowColor);
ParallelFor.WithTemporaryBuffer<float>(
minY,
maxY,
ParallelHelper.IterateRowsWithTempBuffer<float>(
workingRect,
configuration,
width,
(y, amounts) =>
{
Span<float> amountsSpan = amounts.GetSpan();
int offsetY = y - startY;
int offsetX = minX - startX;
for (int i = 0; i < width; i++)
(rows, amounts) =>
{
float distance = Vector2.Distance(center, new Vector2(i + offsetX, offsetY));
amountsSpan[i] = (this.GraphicsOptions.BlendPercentage * (1 - (.95F * (distance / maxDistance)))).Clamp(0, 1);
}
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width);
this.blender.Blend(source.MemoryAllocator, destination, destination, rowColors.GetSpan(), amountsSpan);
});
Span<float> amountsSpan = amounts.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
int offsetY = y - startY;
for (int i = 0; i < width; i++)
{
float distance = Vector2.Distance(center, new Vector2(i + offsetX, offsetY));
amountsSpan[i] =
(this.GraphicsOptions.BlendPercentage * (1 - (.95F * (distance / maxDistance))))
.Clamp(0, 1);
}
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width);
this.blender.Blend(
source.MemoryAllocator,
destination,
destination,
rowColors.GetSpan(),
amountsSpan);
}
});
}
}
}

61
src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor.cs

@ -7,6 +7,7 @@ using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.Memory;
@ -115,43 +116,43 @@ namespace SixLabors.ImageSharp.Processing.Processors.Overlays
}
int width = maxX - minX;
int offsetX = minX - startX;
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
using (IMemoryOwner<TPixel> rowColors = source.MemoryAllocator.Allocate<TPixel>(width))
{
// Be careful! Do not capture rowColorsSpan in the lambda below!
Span<TPixel> rowColorsSpan = rowColors.GetSpan();
for (int i = 0; i < width; i++)
{
rowColorsSpan[i] = vignetteColor;
}
rowColors.GetSpan().Fill(vignetteColor);
ParallelFor.WithTemporaryBuffer<float>(
minY,
maxY,
ParallelHelper.IterateRowsWithTempBuffer<float>(
workingRect,
configuration,
width,
(y, amounts) =>
(rows, amounts) =>
{
Span<float> amountsSpan = amounts.GetSpan();
int offsetY = y - startY;
int offsetX = minX - startX;
for (int i = 0; i < width; i++)
Span<float> amountsSpan = amounts.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
float distance = Vector2.Distance(centre, new Vector2(i + offsetX, offsetY));
amountsSpan[i] =
(this.GraphicsOptions.BlendPercentage * (.9F * (distance / maxDistance))).Clamp(
0,
1);
int offsetY = y - startY;
for (int i = 0; i < width; i++)
{
float distance = Vector2.Distance(centre, new Vector2(i + offsetX, offsetY));
amountsSpan[i] =
(this.GraphicsOptions.BlendPercentage * (.9F * (distance / maxDistance))).Clamp(
0,
1);
}
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width);
this.blender.Blend(
source.MemoryAllocator,
destination,
destination,
rowColors.GetSpan(),
amountsSpan);
}
Span<TPixel> destination = source.GetPixelRowSpan(offsetY).Slice(offsetX, width);
this.blender.Blend(
source.MemoryAllocator,
destination,
destination,
rowColors.GetSpan(),
amountsSpan);
});
}
}

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

@ -3,7 +3,7 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
@ -57,8 +57,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Gets the pixels of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public Span<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 <see cref="Span{T}"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public Span<byte> GetRowSpan(int rowIndex) => this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width);
/// <inheritdoc/>
public void Dispose()
{

190
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs

@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -51,10 +52,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
// We will always be creating the clone even for mutate because we may need to resize the canvas
IEnumerable<ImageFrame<TPixel>> frames =
source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.TargetDimensions, x.MetaData.Clone()));
source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.TargetDimensions, x.MetaData.DeepClone()));
// Use the overload to prevent an extra frame being added
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames);
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.DeepClone(), frames);
}
/// <inheritdoc/>
@ -78,23 +79,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler)
{
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
targetBounds,
configuration,
y =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
rows =>
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
for (int y = rows.Min; y < rows.Max; y++)
{
destRow[x] = source[point.X, point.Y];
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
{
destRow[x] = source[point.X, point.Y];
}
}
}
}
});
});
return;
}
@ -116,86 +119,107 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
{
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
targetBounds,
configuration,
y =>
rows =>
{
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
for (int x = 0; x < width; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), matrix);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
// It appears these have to be calculated on-the-fly.
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ref ySpanRef, yLength);
CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, ref xSpanRef, xLength);
}
else
for (int x = 0; x < width; x++)
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), matrix);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
// It appears these have to be calculated on-the-fly.
// Precalculating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(
top,
bottom,
minY,
maxY,
point.Y,
sampler,
yScale,
ref ySpanRef,
yLength);
CalculateWeightsDown(
left,
right,
minX,
maxX,
point.X,
sampler,
xScale,
ref xSpanRef,
xLength);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
var vector = source[i, j].ToVector4();
float yWeight = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
var vector = source[i, j].ToVector4();
// Values are first premultiplied to prevent darkening of edge pixels
Vector4 multiplied = vector.Premultiply();
sum += multiplied * xWeight * yWeight;
// Values are first premultiplied to prevent darkening of edge pixels
Vector4 multiplied = vector.Premultiply();
sum += multiplied * xWeight * yWeight;
}
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
}
}
});
}

36
src/ImageSharp/Processing/Processors/Transforms/CropProcessor.cs

@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
@ -36,10 +36,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle)
{
// We will always be creating the clone even for mutate because we may need to resize the canvas
IEnumerable<ImageFrame<TPixel>> frames = source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.CropRectangle.Width, this.CropRectangle.Height, x.MetaData.Clone()));
IEnumerable<ImageFrame<TPixel>> frames = source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.CropRectangle.Width, this.CropRectangle.Height, x.MetaData.DeepClone()));
// Use the overload to prevent an extra frame being added
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames);
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.DeepClone(), frames);
}
/// <inheritdoc/>
@ -53,21 +53,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return;
}
int minY = Math.Max(this.CropRectangle.Y, sourceRectangle.Y);
int maxY = Math.Min(this.CropRectangle.Bottom, sourceRectangle.Bottom);
int minX = Math.Max(this.CropRectangle.X, sourceRectangle.X);
int maxX = Math.Min(this.CropRectangle.Right, sourceRectangle.Right);
var rect = Rectangle.Intersect(this.CropRectangle, sourceRectangle);
ParallelFor.WithConfiguration(
minY,
maxY,
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(minX);
Span<TPixel> targetRow = destination.GetPixelRowSpan(y - minY);
sourceRow.Slice(0, maxX - minX).CopyTo(targetRow);
});
// Copying is cheap, we should process more pixels per task:
ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4);
ParallelHelper.IterateRows(
rect,
parallelSettings,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(rect.Left);
Span<TPixel> targetRow = destination.GetPixelRowSpan(y - rect.Top);
sourceRow.Slice(0, rect.Width).CopyTo(targetRow);
}
});
}
}
}

64
src/ImageSharp/Processing/Processors/Transforms/FlipProcessor.cs

@ -2,9 +2,11 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -55,27 +57,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private void FlipX(ImageFrame<TPixel> source, Configuration configuration)
{
int height = source.Height;
int halfHeight = (int)Math.Ceiling(source.Height * .5F);
using (Buffer2D<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size()))
using (IMemoryOwner<TPixel> tempBuffer = configuration.MemoryAllocator.Allocate<TPixel>(source.Width))
{
ParallelFor.WithConfiguration(
0,
halfHeight,
configuration,
y =>
{
int newY = height - y - 1;
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> altSourceRow = source.GetPixelRowSpan(newY);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
Span<TPixel> altTargetRow = targetPixels.GetRowSpan(newY);
sourceRow.CopyTo(altTargetRow);
altSourceRow.CopyTo(targetRow);
});
Span<TPixel> temp = tempBuffer.Memory.Span;
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
for (int yTop = 0; yTop < height / 2; yTop++)
{
int yBottom = height - yTop - 1;
Span<TPixel> topRow = source.GetPixelRowSpan(yBottom);
Span<TPixel> bottomRow = source.GetPixelRowSpan(yTop);
topRow.CopyTo(temp);
bottomRow.CopyTo(topRow);
temp.CopyTo(bottomRow);
}
}
}
@ -86,31 +81,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="configuration">The configuration.</param>
private void FlipY(ImageFrame<TPixel> source, Configuration configuration)
{
int width = source.Width;
int height = source.Height;
int halfWidth = (int)Math.Ceiling(width * .5F);
using (Buffer2D<TPixel> targetPixels = configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size()))
{
ParallelFor.WithConfiguration(
0,
height,
configuration,
y =>
ParallelHelper.IterateRows(
source.Bounds(),
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = targetPixels.GetRowSpan(y);
for (int x = 0; x < halfWidth; x++)
{
int newX = width - x - 1;
targetRow[x] = sourceRow[newX];
targetRow[newX] = sourceRow[x];
}
});
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}
source.GetPixelRowSpan(y).Reverse();
}
});
}
}
}

220
src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs

@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -51,10 +52,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
// We will always be creating the clone even for mutate because we may need to resize the canvas
IEnumerable<ImageFrame<TPixel>> frames =
source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.TargetDimensions.Width, this.TargetDimensions.Height, x.MetaData.Clone()));
source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.TargetDimensions.Width, this.TargetDimensions.Height, x.MetaData.DeepClone()));
// Use the overload to prevent an extra frame being added
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames);
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.DeepClone(), frames);
}
/// <inheritdoc/>
@ -75,28 +76,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler)
{
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
targetBounds,
configuration,
y =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
rows =>
{
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
float z = MathF.Max(v3.Z, Epsilon);
int px = (int)MathF.Round(v3.X / z);
int py = (int)MathF.Round(v3.Y / z);
for (int x = 0; x < width; x++)
{
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
if (sourceBounds.Contains(px, py))
{
destRow[x] = source[px, py];
float z = MathF.Max(v3.Z, Epsilon);
int px = (int)MathF.Round(v3.X / z);
int py = (int)MathF.Round(v3.Y / z);
if (sourceBounds.Contains(px, py))
{
destRow[x] = source[px, py];
}
}
}
}
});
});
return;
}
@ -121,92 +124,113 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
{
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
targetBounds,
configuration,
y =>
{
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
for (int x = 0; x < width; x++)
rows =>
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
float z = MathF.Max(v3.Z, Epsilon);
// Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable:
Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z;
// Clamp sampling pixel radial extents to the source image edges
Vector4 maxXY = point + radius;
Vector4 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ref ySpanRef, yLength);
CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, ref xSpanRef, xLength);
}
else
for (int y = rows.Min; y < rows.Max; y++)
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
for (int x = 0; x < width; x++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
var vector = source[i, j].ToVector4();
// Values are first premultiplied to prevent darkening of edge pixels
Vector4 multiplied = vector.Premultiply();
sum += multiplied * xWeight * yWeight;
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
float z = MathF.Max(v3.Z, Epsilon);
// Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable:
Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z;
// Clamp sampling pixel radial extents to the source image edges
Vector4 maxXY = point + radius;
Vector4 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(
top,
bottom,
minY,
maxY,
point.Y,
sampler,
yScale,
ref ySpanRef,
yLength);
CalculateWeightsDown(
left,
right,
minX,
maxX,
point.X,
sampler,
xScale,
ref xSpanRef,
xLength);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
var vector = source[i, j].ToVector4();
// Values are first premultiplied to prevent darkening of edge pixels
Vector4 multiplied = vector.Premultiply();
sum += multiplied * xWeight * yWeight;
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
}
});
});
}
}

152
src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs

@ -11,6 +11,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
@ -215,10 +216,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle)
{
// We will always be creating the clone even for mutate because we may need to resize the canvas
IEnumerable<ImageFrame<TPixel>> frames = source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.Width, this.Height, x.MetaData.Clone()));
IEnumerable<ImageFrame<TPixel>> frames = source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.Width, this.Height, x.MetaData.DeepClone()));
// Use the overload to prevent an extra frame being added
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames);
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.DeepClone(), frames);
}
/// <inheritdoc/>
@ -267,26 +268,31 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler)
{
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
// Scaling factors
float widthFactor = sourceRectangle.Width / (float)this.ResizeRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)this.ResizeRectangle.Height;
ParallelFor.WithConfiguration(
minY,
maxY,
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
{
// Y coordinates of source points
Span<TPixel> sourceRow = source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY));
Span<TPixel> targetRow = destination.GetPixelRowSpan(y);
for (int x = minX; x < maxX; x++)
rows =>
{
// X coordinates of source points
targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)];
}
});
for (int y = rows.Min; y < rows.Max; y++)
{
// Y coordinates of source points
Span<TPixel> sourceRow =
source.GetPixelRowSpan((int)(((y - startY) * heightFactor) + sourceY));
Span<TPixel> targetRow = destination.GetPixelRowSpan(y);
for (int x = minX; x < maxX; x++)
{
// X coordinates of source points
targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)];
}
}
});
return;
}
@ -300,72 +306,88 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
firstPassPixels.MemorySource.Clear();
ParallelFor.WithTemporaryBuffer(
0,
sourceRectangle.Bottom,
var processColsRect = new Rectangle(0, 0, source.Width, sourceRectangle.Bottom);
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
processColsRect,
configuration,
source.Width,
(int y, IMemoryOwner<Vector4> tempRowBuffer) =>
(rows, tempRowBuffer) =>
{
ref Vector4 firstPassRow = ref MemoryMarshal.GetReference(firstPassPixels.GetRowSpan(y));
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.GetSpan();
for (int y = rows.Min; y < rows.Max; y++)
{
ref Vector4 firstPassRow =
ref MemoryMarshal.GetReference(firstPassPixels.GetRowSpan(y));
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.Span;
PixelOperations<TPixel>.Instance.ToVector4(sourceRow, tempRowSpan, sourceRow.Length);
PixelOperations<TPixel>.Instance.ToVector4(sourceRow, tempRowSpan, sourceRow.Length);
if (this.Compand)
{
for (int x = minX; x < maxX; x++)
if (this.Compand)
{
WeightsWindow window = this.horizontalWeights.Weights[x - startX];
Unsafe.Add(ref firstPassRow, x) = window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX);
for (int x = minX; x < maxX; x++)
{
WeightsWindow window = this.horizontalWeights.Weights[x - startX];
Unsafe.Add(ref firstPassRow, x) =
window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX);
}
}
}
else
{
for (int x = minX; x < maxX; x++)
else
{
WeightsWindow window = this.horizontalWeights.Weights[x - startX];
Unsafe.Add(ref firstPassRow, x) = window.ComputeWeightedRowSum(tempRowSpan, sourceX);
for (int x = minX; x < maxX; x++)
{
WeightsWindow window = this.horizontalWeights.Weights[x - startX];
Unsafe.Add(ref firstPassRow, x) =
window.ComputeWeightedRowSum(tempRowSpan, sourceX);
}
}
}
});
var processRowsRect = Rectangle.FromLTRB(0, minY, width, maxY);
// Now process the rows.
ParallelFor.WithConfiguration(
minY,
maxY,
ParallelHelper.IterateRows(
processRowsRect,
configuration,
y =>
{
// Ensure offsets are normalized for cropping and padding.
WeightsWindow window = this.verticalWeights.Weights[y - startY];
ref TPixel targetRow = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
if (this.Compand)
{
for (int x = 0; x < width; x++)
{
// Destination color components
Vector4 destinationVector = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY);
destinationVector = destinationVector.Compress();
ref TPixel pixel = ref Unsafe.Add(ref targetRow, x);
pixel.PackFromVector4(destinationVector);
}
}
else
rows =>
{
for (int x = 0; x < width; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
// Destination color components
Vector4 destinationVector = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY);
// Ensure offsets are normalized for cropping and padding.
WeightsWindow window = this.verticalWeights.Weights[y - startY];
ref TPixel targetRow = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref TPixel pixel = ref Unsafe.Add(ref targetRow, x);
pixel.PackFromVector4(destinationVector);
if (this.Compand)
{
for (int x = 0; x < width; x++)
{
// Destination color components
Vector4 destinationVector = window.ComputeWeightedColumnSum(
firstPassPixels,
x,
sourceY);
destinationVector = destinationVector.Compress();
ref TPixel pixel = ref Unsafe.Add(ref targetRow, x);
pixel.PackFromVector4(destinationVector);
}
}
else
{
for (int x = 0; x < width; x++)
{
// Destination color components
Vector4 destinationVector = window.ComputeWeightedColumnSum(
firstPassPixels,
x,
sourceY);
ref TPixel pixel = ref Unsafe.Add(ref targetRow, x);
pixel.PackFromVector4(destinationVector);
}
}
}
}
});
});
}
}

86
src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs

@ -5,6 +5,7 @@ using System;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
@ -147,25 +148,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int height = source.Height;
Rectangle destinationBounds = destination.Bounds();
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
source.Bounds(),
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
rows =>
{
int newX = height - y - 1;
newX = height - newX - 1;
int newY = width - x - 1;
if (destinationBounds.Contains(newX, newY))
for (int y = rows.Min; y < rows.Max; y++)
{
destination[newX, newY] = sourceRow[x];
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
int newX = height - y - 1;
newX = height - newX - 1;
int newY = width - x - 1;
if (destinationBounds.Contains(newX, newY))
{
destination[newX, newY] = sourceRow[x];
}
}
}
}
});
});
}
/// <summary>
@ -179,20 +182,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int width = source.Width;
int height = source.Height;
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
source.Bounds(),
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = destination.GetPixelRowSpan(height - y - 1);
for (int x = 0; x < width; x++)
rows =>
{
targetRow[width - x - 1] = sourceRow[x];
}
});
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<TPixel> targetRow = destination.GetPixelRowSpan(height - y - 1);
for (int x = 0; x < width; x++)
{
targetRow[width - x - 1] = sourceRow[x];
}
}
});
}
/// <summary>
@ -207,22 +212,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int height = source.Height;
Rectangle destinationBounds = destination.Bounds();
ParallelFor.WithConfiguration(
0,
height,
ParallelHelper.IterateRows(
source.Bounds(),
configuration,
y =>
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
int newX = height - y - 1;
for (int x = 0; x < width; x++)
rows =>
{
if (destinationBounds.Contains(newX, x))
for (int y = rows.Min; y < rows.Max; y++)
{
destination[newX, x] = sourceRow[x];
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
int newX = height - y - 1;
for (int x = 0; x < width; x++)
{
// TODO: Optimize this:
if (destinationBounds.Contains(newX, x))
{
destination[newX, x] = sourceRow[x];
}
}
}
}
});
});
}
}
}

115
tests/ImageSharp.Benchmarks/Codecs/CopyPixels.cs

@ -1,115 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Benchmarks.Codecs
{
public class CopyPixels : BenchmarkBase
{
[Benchmark(Baseline = true, Description = "PixelAccessor Copy by indexer")]
public Rgba32 CopyByPixelAccesor()
{
using (var source = new Image<Rgba32>(1024, 768))
using (var target = new Image<Rgba32>(1024, 768))
{
Buffer2D<Rgba32> sourcePixels = source.GetRootFramePixelBuffer();
Buffer2D<Rgba32> targetPixels = target.GetRootFramePixelBuffer();
ParallelFor.WithConfiguration(
0,
source.Height,
Configuration.Default,
y =>
{
for (int x = 0; x < source.Width; x++)
{
targetPixels[x, y] = sourcePixels[x, y];
}
});
return targetPixels[0, 0];
}
}
[Benchmark(Description = "PixelAccessor Copy by Span")]
public Rgba32 CopyByPixelAccesorSpan()
{
using (var source = new Image<Rgba32>(1024, 768))
using (var target = new Image<Rgba32>(1024, 768))
{
Buffer2D<Rgba32> sourcePixels = source.GetRootFramePixelBuffer();
Buffer2D<Rgba32> targetPixels = target.GetRootFramePixelBuffer();
ParallelFor.WithConfiguration(
0,
source.Height,
Configuration.Default,
y =>
{
Span<Rgba32> sourceRow = sourcePixels.GetRowSpan(y);
Span<Rgba32> targetRow = targetPixels.GetRowSpan(y);
for (int x = 0; x < source.Width; x++)
{
targetRow[x] = sourceRow[x];
}
});
return targetPixels[0, 0];
}
}
[Benchmark(Description = "Copy by indexer")]
public Rgba32 Copy()
{
using (var source = new Image<Rgba32>(1024, 768))
using (var target = new Image<Rgba32>(1024, 768))
{
ParallelFor.WithConfiguration(
0,
source.Height,
Configuration.Default,
y =>
{
for (int x = 0; x < source.Width; x++)
{
target[x, y] = source[x, y];
}
});
return target[0, 0];
}
}
[Benchmark(Description = "Copy by Span")]
public Rgba32 CopySpan()
{
using (var source = new Image<Rgba32>(1024, 768))
using (var target = new Image<Rgba32>(1024, 768))
{
ParallelFor.WithConfiguration(
0,
source.Height,
Configuration.Default,
y =>
{
Span<Rgba32> sourceRow = source.Frames.RootFrame.GetPixelRowSpan(y);
Span<Rgba32> targetRow = target.Frames.RootFrame.GetPixelRowSpan(y);
for (int x = 0; x < source.Width; x++)
{
targetRow[x] = sourceRow[x];
}
});
return target[0, 0];
}
}
}
}

38
tests/ImageSharp.Benchmarks/Samplers/Glow.cs

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.ImageSharp.Processing.Processors.Overlays;
@ -112,26 +113,29 @@ namespace SixLabors.ImageSharp.Benchmarks
Buffer2D<TPixel> sourcePixels = source.PixelBuffer;
rowColors.GetSpan().Fill(glowColor);
ParallelFor.WithConfiguration(
minY,
maxY,
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
ParallelHelper.IterateRows(
workingRect,
configuration,
y =>
rows =>
{
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
int offsetX = x - startX;
float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY));
Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4();
TPixel packed = default(TPixel);
packed.PackFromVector4(
PremultipliedLerp(
sourceColor,
glowColor.ToVector4(),
1 - (.95F * (distance / maxDistance))));
sourcePixels[offsetX, offsetY] = packed;
int offsetY = y - startY;
for (int x = minX; x < maxX; x++)
{
int offsetX = x - startX;
float distance = Vector2.Distance(centre, new Vector2(offsetX, offsetY));
Vector4 sourceColor = sourcePixels[offsetX, offsetY].ToVector4();
TPixel packed = default(TPixel);
packed.PackFromVector4(
PremultipliedLerp(
sourceColor,
glowColor.ToVector4(),
1 - (.95F * (distance / maxDistance))));
sourcePixels[offsetX, offsetY] = packed;
}
}
});
}

2
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Tests
{
// the shallow copy of configuration should behave exactly like the default configuration,
// so by using the copy, we test both the default and the copy.
this.DefaultConfiguration = Configuration.CreateDefaultInstance().ShallowCopy();
this.DefaultConfiguration = Configuration.CreateDefaultInstance().Clone();
this.ConfigurationEmpty = new Configuration();
}

255
tests/ImageSharp.Tests/Drawing/FillPatternTests.cs

@ -54,172 +54,225 @@ namespace SixLabors.ImageSharp.Tests.Drawing
[Fact]
public void ImageShouldBeFloodFilledWithPercent10()
{
this.Test("Percent10", Rgba32.Blue, Brushes.Percent10(Rgba32.HotPink, Rgba32.LimeGreen),
this.Test(
"Percent10",
Rgba32.Blue,
Brushes.Percent10(Rgba32.HotPink, Rgba32.LimeGreen),
new[,]
{
{ Rgba32.HotPink , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink , Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}
});
{
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithPercent10Transparent()
{
Test("Percent10_Transparent", Rgba32.Blue, Brushes.Percent10(Rgba32.HotPink),
new Rgba32[,] {
{ Rgba32.HotPink , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink , Rgba32.Blue},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}
});
this.Test(
"Percent10_Transparent",
Rgba32.Blue,
Brushes.Percent10(Rgba32.HotPink),
new Rgba32[,]
{
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithPercent20()
{
Test("Percent20", Rgba32.Blue, Brushes.Percent20(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ Rgba32.HotPink , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink , Rgba32.LimeGreen},
{ Rgba32.HotPink , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink , Rgba32.LimeGreen}
});
this.Test(
"Percent20",
Rgba32.Blue,
Brushes.Percent20(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen },
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithPercent20_transparent()
{
Test("Percent20_Transparent", Rgba32.Blue, Brushes.Percent20(Rgba32.HotPink),
new Rgba32[,] {
{ Rgba32.HotPink , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink , Rgba32.Blue},
{ Rgba32.HotPink , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink , Rgba32.Blue}
});
this.Test(
"Percent20_Transparent",
Rgba32.Blue,
Brushes.Percent20(Rgba32.HotPink),
new Rgba32[,]
{
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue },
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithHorizontal()
{
Test("Horizontal", Rgba32.Blue, Brushes.Horizontal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink},
{ Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen , Rgba32.LimeGreen}
});
this.Test(
"Horizontal",
Rgba32.Blue,
Brushes.Horizontal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithHorizontal_transparent()
{
Test("Horizontal_Transparent", Rgba32.Blue, Brushes.Horizontal(Rgba32.HotPink),
new Rgba32[,] {
{ Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink},
{ Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue , Rgba32.Blue}
});
this.Test(
"Horizontal_Transparent",
Rgba32.Blue,
Brushes.Horizontal(Rgba32.HotPink),
new Rgba32[,]
{
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithMin()
{
Test("Min", Rgba32.Blue, Brushes.Min(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen , Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen , Rgba32.LimeGreen},
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink}
});
this.Test(
"Min",
Rgba32.Blue,
Brushes.Min(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithMin_transparent()
{
Test("Min_Transparent", Rgba32.Blue, Brushes.Min(Rgba32.HotPink),
new Rgba32[,] {
{ Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue , Rgba32.Blue, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue , Rgba32.Blue},
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink , Rgba32.HotPink},
});
this.Test(
"Min_Transparent",
Rgba32.Blue,
Brushes.Min(Rgba32.HotPink),
new Rgba32[,]
{
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink, Rgba32.HotPink },
});
}
[Fact]
public void ImageShouldBeFloodFilledWithVertical()
{
Test("Vertical", Rgba32.Blue, Brushes.Vertical(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen}
});
this.Test(
"Vertical",
Rgba32.Blue,
Brushes.Vertical(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithVertical_transparent()
{
Test("Vertical_Transparent", Rgba32.Blue, Brushes.Vertical(Rgba32.HotPink),
new Rgba32[,] {
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue}
});
this.Test(
"Vertical_Transparent",
Rgba32.Blue,
Brushes.Vertical(Rgba32.HotPink),
new Rgba32[,]
{
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithForwardDiagonal()
{
Test("ForwardDiagonal", Rgba32.Blue, Brushes.ForwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen}
});
this.Test(
"ForwardDiagonal",
Rgba32.Blue,
Brushes.ForwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent()
{
Test("ForwardDiagonal_Transparent", Rgba32.Blue, Brushes.ForwardDiagonal(Rgba32.HotPink),
new Rgba32[,] {
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue}
});
this.Test(
"ForwardDiagonal_Transparent",
Rgba32.Blue,
Brushes.ForwardDiagonal(Rgba32.HotPink),
new Rgba32[,]
{
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithBackwardDiagonal()
{
Test("BackwardDiagonal", Rgba32.Blue, Brushes.BackwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,] {
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen},
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink}
});
this.Test(
"BackwardDiagonal",
Rgba32.Blue,
Brushes.BackwardDiagonal(Rgba32.HotPink, Rgba32.LimeGreen),
new Rgba32[,]
{
{ Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink, Rgba32.LimeGreen },
{ Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.LimeGreen, Rgba32.HotPink }
});
}
[Fact]
public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent()
{
Test("BackwardDiagonal_Transparent", Rgba32.Blue, Brushes.BackwardDiagonal(Rgba32.HotPink),
new Rgba32[,] {
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue},
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink}
});
this.Test(
"BackwardDiagonal_Transparent",
Rgba32.Blue,
Brushes.BackwardDiagonal(Rgba32.HotPink),
new Rgba32[,]
{
{ Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink, Rgba32.Blue },
{ Rgba32.Blue, Rgba32.Blue, Rgba32.Blue, Rgba32.HotPink }
});
}
}
}
}

44
tests/ImageSharp.Tests/Drawing/FillSolidBrushTests.cs

@ -1,20 +1,21 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using SixLabors.Shapes;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Drawing
{
using System;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
[GroupOutput("Drawing")]
public class FillSolidBrushTests
{
@ -55,7 +56,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing
[Theory]
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")]
[WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")]
public void WhenColorIsOpaque_OverridePreviousColor<TPixel>(TestImageProvider<TPixel> provider, string newColorName)
public void WhenColorIsOpaque_OverridePreviousColor<TPixel>(
TestImageProvider<TPixel> provider,
string newColorName)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
@ -63,7 +66,11 @@ namespace SixLabors.ImageSharp.Tests.Drawing
TPixel color = TestUtils.GetPixelOfNamedColor<TPixel>(newColorName);
image.Mutate(c => c.Fill(color));
image.DebugSave(provider, newColorName, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
image.DebugSave(
provider,
newColorName,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
image.ComparePixelBufferTo(color);
}
}
@ -84,7 +91,12 @@ namespace SixLabors.ImageSharp.Tests.Drawing
[Theory]
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)]
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)]
public void FillRegion_WorksOnWrappedMemoryImage<TPixel>(TestImageProvider<TPixel> provider, int x0, int y0, int w, int h)
public void FillRegion_WorksOnWrappedMemoryImage<TPixel>(
TestImageProvider<TPixel> provider,
int x0,
int y0,
int w,
int h)
where TPixel : struct, IPixel<TPixel>
{
FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})";
@ -105,27 +117,22 @@ namespace SixLabors.ImageSharp.Tests.Drawing
{ false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f },
{ false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f },
{ false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f },
{ false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f },
{ false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f },
{ false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f },
{ false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f },
{ false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f },
{ false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f },
{ false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f },
{ false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f },
{ true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f },
{ true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f },
{ true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f },
{ true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f },
{ true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f },
{ true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f },
{ true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f },
{ true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f },
{ true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f },
{ true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f },
{ true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f },
@ -155,8 +162,7 @@ namespace SixLabors.ImageSharp.Tests.Drawing
var options = new GraphicsOptions(false)
{
ColorBlendingMode = blenderMode,
BlendPercentage = blendPercentage
ColorBlendingMode = blenderMode, BlendPercentage = blendPercentage
};
if (triggerFillRegion)
@ -185,11 +191,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(blenderMode, PixelAlphaCompositionMode.SrcOver);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(
blenderMode,
PixelAlphaCompositionMode.SrcOver);
TPixel expectedPixel = blender.Blend(bgColor, fillColor, blendPercentage);
image.ComparePixelBufferTo(expectedPixel);
}
}
}
}
}

22
tests/ImageSharp.Tests/Formats/Bmp/BmpMetaDataTests.cs

@ -0,0 +1,22 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Bmp;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Bmp
{
public class BmpMetaDataTests
{
[Fact]
public void CloneIsDeep()
{
var meta = new BmpMetaData() { BitsPerPixel = BmpBitsPerPixel.Pixel24 };
var clone = (BmpMetaData)meta.DeepClone();
clone.BitsPerPixel = BmpBitsPerPixel.Pixel32;
Assert.False(meta.BitsPerPixel.Equals(clone.BitsPerPixel));
}
}
}

32
tests/ImageSharp.Tests/Formats/Gif/GifFrameMetaDataTests.cs

@ -0,0 +1,32 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Gif;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Gif
{
public class GifFrameMetaDataTests
{
[Fact]
public void CloneIsDeep()
{
var meta = new GifFrameMetaData()
{
FrameDelay = 1,
DisposalMethod = GifDisposalMethod.RestoreToBackground,
ColorTableLength = 2
};
var clone = (GifFrameMetaData)meta.DeepClone();
clone.FrameDelay = 2;
clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious;
clone.ColorTableLength = 1;
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod));
Assert.False(meta.ColorTableLength.Equals(clone.ColorTableLength));
}
}
}

32
tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs

@ -0,0 +1,32 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Gif;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Gif
{
public class GifMetaDataTests
{
[Fact]
public void CloneIsDeep()
{
var meta = new GifMetaData()
{
RepeatCount = 1,
ColorTableMode = GifColorTableMode.Global,
GlobalColorTableLength = 2
};
var clone = (GifMetaData)meta.DeepClone();
clone.RepeatCount = 2;
clone.ColorTableMode = GifColorTableMode.Local;
clone.GlobalColorTableLength = 1;
Assert.False(meta.RepeatCount.Equals(clone.RepeatCount));
Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode));
Assert.False(meta.GlobalColorTableLength.Equals(clone.GlobalColorTableLength));
}
}
}

33
tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs

@ -2,17 +2,15 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Moq;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Gif;
using Moq;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
@ -27,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests
{
this.DefaultFormatsManager = Configuration.CreateDefaultInstance().ImageFormatsManager;
this.FormatsManagerEmpty = new ImageFormatManager();
}
}
[Fact]
public void IfAutoloadWellKnownFormatsIsTrueAllFormatsAreLoaded()
@ -114,7 +112,7 @@ namespace SixLabors.ImageSharp.Tests
IImageDecoder found2 = this.FormatsManagerEmpty.FindDecoder(TestFormat.GlobalTestFormat);
Assert.Equal(decoder2, found2);
Assert.NotEqual(found, found2);
}
}
[Fact]
public void AddFormatCallsConfig()
@ -125,5 +123,24 @@ namespace SixLabors.ImageSharp.Tests
provider.Verify(x => x.Configure(config));
}
[Fact]
public void DetectFormatAllocatesCleanBuffer()
{
byte[] jpegImage;
using (var buffer = new MemoryStream())
{
using (var image = new Image<Rgba32>(100, 100))
{
image.SaveAsJpeg(buffer);
jpegImage = buffer.ToArray();
}
}
byte[] invalidImage = { 1, 2, 3 };
Assert.Equal(Image.DetectFormat(jpegImage), JpegFormat.Instance);
Assert.True(Image.DetectFormat(invalidImage) is null);
}
}
}

22
tests/ImageSharp.Tests/Formats/Jpg/JpegMetaDataTests.cs

@ -0,0 +1,22 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Jpeg;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
public class JpegMetaDataTests
{
[Fact]
public void CloneIsDeep()
{
var meta = new JpegMetaData() { Quality = 50 };
var clone = (JpegMetaData)meta.DeepClone();
clone.Quality = 99;
Assert.False(meta.Quality.Equals(clone.Quality));
}
}
}

160
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@ -19,10 +18,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
{
public class PngEncoderTests
{
// This is bull. Failing online for no good reason.
// The images are an exact match. Maybe the submodule isn't updating?
private const float ToleranceThresholdForPaletteEncoder = 1.3F / 100;
public static readonly TheoryData<string, PngBitDepth> PngBitDepthFiles =
new TheoryData<string, PngBitDepth>
{
@ -137,19 +132,32 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
[Theory]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.Rgb)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.RgbWithAlpha)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.RgbWithAlpha)]
public void WorksWithBitDepth16<TPixel>(TestImageProvider<TPixel> provider, PngColorType pngColorType)
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Rgb, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.Rgb, PngBitDepth.Bit16)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit1)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit2)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit4)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit1)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit2)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit4)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb48, PngColorType.Grayscale, PngBitDepth.Bit16)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit16)]
public void WorksWithAllBitDepths<TPixel>(TestImageProvider<TPixel> provider, PngColorType pngColorType, PngBitDepth pngBitDepth)
where TPixel : struct, IPixel<TPixel>
{
TestPngEncoderCore(
provider,
pngColorType,
PngFilterMethod.Adaptive,
PngBitDepth.Bit16,
pngBitDepth,
appendPngColorType: true,
appendPixelType: true);
appendPixelType: true,
appendPngBitDepth: true);
}
[Theory]
@ -166,87 +174,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
appendPaletteSize: true);
}
private static bool HasAlpha(PngColorType pngColorType) =>
pngColorType == PngColorType.GrayscaleWithAlpha || pngColorType == PngColorType.RgbWithAlpha;
private static void TestPngEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
PngColorType pngColorType,
PngFilterMethod pngFilterMethod,
PngBitDepth bitDepth,
int compressionLevel = 6,
int paletteSize = 255,
bool appendPngColorType = false,
bool appendPngFilterMethod = false,
bool appendPixelType = false,
bool appendCompressionLevel = false,
bool appendPaletteSize = false)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
if (!HasAlpha(pngColorType))
{
image.Mutate(c => c.MakeOpaque());
}
var encoder = new PngEncoder
{
ColorType = pngColorType,
FilterMethod = pngFilterMethod,
CompressionLevel = compressionLevel,
BitDepth = bitDepth,
Quantizer = new WuQuantizer(paletteSize)
};
string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty;
string pngFilterMethodInfo = appendPngFilterMethod ? pngFilterMethod.ToString() : string.Empty;
string compressionLevelInfo = appendCompressionLevel ? $"_C{compressionLevel}" : string.Empty;
string paletteSizeInfo = appendPaletteSize ? $"_PaletteSize-{paletteSize}" : string.Empty;
string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}";
//string referenceInfo = $"{pngColorTypeInfo}";
// Does DebugSave & load reference CompareToReferenceInput():
string actualOutputFile = ((ITestImageProvider)provider).Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType);
if (TestEnvironment.IsMono)
{
// There are bugs in mono's System.Drawing implementation, reference decoders are not always reliable!
return;
}
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);
string referenceOutputFile = ((ITestImageProvider)provider).Utility.GetReferenceOutputFileName("png", debugInfo, appendPixelType, true);
bool referenceOutputFileExists = File.Exists(referenceOutputFile);
using (var actualImage = Image.Load<TPixel>(actualOutputFile, referenceDecoder))
{
// TODO: Do we still need the reference output files?
Image<TPixel> referenceImage = referenceOutputFileExists
? Image.Load<TPixel>(referenceOutputFile, referenceDecoder)
: image;
float paletteToleranceHack = 80f / paletteSize;
paletteToleranceHack *= paletteToleranceHack;
ImageComparer comparer = pngColorType == PngColorType.Palette
? ImageComparer.Tolerant(ToleranceThresholdForPaletteEncoder * paletteToleranceHack)
: ImageComparer.Exact;
try
{
comparer.VerifySimilarity(referenceImage, actualImage);
}
finally
{
if (referenceOutputFileExists)
{
referenceImage.Dispose();
}
}
}
}
}
[Theory]
[WithBlankImages(1, 1, PixelTypes.Rgba32)]
public void WritesFileMarker<TPixel>(TestImageProvider<TPixel> provider)
@ -321,5 +248,54 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
}
private static void TestPngEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
PngColorType pngColorType,
PngFilterMethod pngFilterMethod,
PngBitDepth bitDepth,
int compressionLevel = 6,
int paletteSize = 255,
bool appendPngColorType = false,
bool appendPngFilterMethod = false,
bool appendPixelType = false,
bool appendCompressionLevel = false,
bool appendPaletteSize = false,
bool appendPngBitDepth = false)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var encoder = new PngEncoder
{
ColorType = pngColorType,
FilterMethod = pngFilterMethod,
CompressionLevel = compressionLevel,
BitDepth = bitDepth,
Quantizer = new WuQuantizer(paletteSize)
};
string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty;
string pngFilterMethodInfo = appendPngFilterMethod ? pngFilterMethod.ToString() : string.Empty;
string compressionLevelInfo = appendCompressionLevel ? $"_C{compressionLevel}" : string.Empty;
string paletteSizeInfo = appendPaletteSize ? $"_PaletteSize-{paletteSize}" : string.Empty;
string pngBitDepthInfo = appendPngBitDepth ? bitDepth.ToString() : string.Empty;
string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}{pngBitDepthInfo}";
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType);
// Compare to the Magick reference decoder.
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);
// We compare using both our decoder and the reference decoder as pixel transformation
// occurrs within the encoder itself leaving the input image unaffected.
// This means we are benefiting from testing our decoder also.
using (var imageSharpImage = Image.Load<TPixel>(actualOutputFile, new PngDecoder()))
using (var referenceImage = Image.Load<TPixel>(actualOutputFile, referenceDecoder))
{
ImageComparer.Exact.VerifySimilarity(referenceImage, imageSharpImage);
}
}
}
}
}

31
tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Png;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
public class PngMetaDataTests
{
[Fact]
public void CloneIsDeep()
{
var meta = new PngMetaData()
{
BitDepth = PngBitDepth.Bit16,
ColorType = PngColorType.GrayscaleWithAlpha,
Gamma = 2
};
var clone = (PngMetaData)meta.DeepClone();
clone.BitDepth = PngBitDepth.Bit2;
clone.ColorType = PngColorType.Palette;
clone.Gamma = 1;
Assert.False(meta.BitDepth.Equals(clone.BitDepth));
Assert.False(meta.ColorType.Equals(clone.ColorType));
Assert.False(meta.Gamma.Equals(clone.Gamma));
}
}
}

338
tests/ImageSharp.Tests/Helpers/ParallelHelperTests.cs

@ -0,0 +1,338 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Numerics;
using System.Threading;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.Memory;
using SixLabors.Primitives;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Helpers
{
public class ParallelHelperTests
{
private readonly ITestOutputHelper Output;
public ParallelHelperTests(ITestOutputHelper output)
{
this.Output = output;
}
/// <summary>
/// maxDegreeOfParallelism, minY, maxY, expectedStepLength, expectedLastStepLength
/// </summary>
public static TheoryData<int, int, int, int, int> IterateRows_OverMinimumPixelsLimit_Data =
new TheoryData<int, int, int, int, int>()
{
{ 1, 0, 100, -1, 100 },
{ 2, 0, 9, 5, 4 },
{ 4, 0, 19, 5, 4 },
{ 2, 10, 19, 5, 4 },
{ 4, 0, 200, 50, 50 },
{ 4, 123, 323, 50, 50 },
{ 4, 0, 1201, 301, 298 },
{ 8, 10, 236, 29, 23 }
};
[Theory]
[MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))]
public void IterateRows_OverMinimumPixelsLimit_IntervalsAreCorrect(
int maxDegreeOfParallelism,
int minY,
int maxY,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
1,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, minY, 10, maxY - minY);
int actualNumberOfSteps = 0;
ParallelHelper.IterateRows(
rectangle,
parallelSettings,
rows =>
{
Assert.True(rows.Min >= minY);
Assert.True(rows.Max <= maxY);
int step = rows.Max - rows.Min;
int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step);
});
Assert.Equal(maxDegreeOfParallelism, actualNumberOfSteps);
}
[Theory]
[MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))]
public void IterateRows_OverMinimumPixelsLimit_ShouldVisitAllRows(
int maxDegreeOfParallelism,
int minY,
int maxY,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
1,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, minY, 10, maxY - minY);
int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray();
int[] actualData = new int[maxY];
ParallelHelper.IterateRows(
rectangle,
parallelSettings,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
actualData[y] = y;
}
});
Assert.Equal(expectedData, actualData);
}
[Theory]
[MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))]
public void IterateRowsWithTempBuffer_OverMinimumPixelsLimit(
int maxDegreeOfParallelism,
int minY,
int maxY,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
1,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, minY, 10, maxY - minY);
var bufferHashes = new ConcurrentBag<int>();
int actualNumberOfSteps = 0;
ParallelHelper.IterateRowsWithTempBuffer(
rectangle,
parallelSettings,
(RowInterval rows, Memory<Vector4> buffer) =>
{
Assert.True(rows.Min >= minY);
Assert.True(rows.Max <= maxY);
bufferHashes.Add(buffer.GetHashCode());
int step = rows.Max - rows.Min;
int expected = rows.Max < maxY ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step);
});
Assert.Equal(maxDegreeOfParallelism, actualNumberOfSteps);
int numberOfDifferentBuffers = bufferHashes.Distinct().Count();
Assert.Equal(actualNumberOfSteps, numberOfDifferentBuffers);
}
[Theory]
[MemberData(nameof(IterateRows_OverMinimumPixelsLimit_Data))]
public void IterateRowsWithTempBuffer_OverMinimumPixelsLimit_ShouldVisitAllRows(
int maxDegreeOfParallelism,
int minY,
int maxY,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
1,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, minY, 10, maxY - minY);
int[] expectedData = Enumerable.Repeat(0, minY).Concat(Enumerable.Range(minY, maxY - minY)).ToArray();
int[] actualData = new int[maxY];
ParallelHelper.IterateRowsWithTempBuffer(
rectangle,
parallelSettings,
(RowInterval rows, Memory<Vector4> buffer) =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
actualData[y] = y;
}
});
Assert.Equal(expectedData, actualData);
}
public static TheoryData<int, int, int, int, int, int, int> IterateRows_WithEffectiveMinimumPixelsLimit_Data =
new TheoryData<int, int, int, int, int, int, int>()
{
{ 2, 200, 50, 2, 1, -1, 2 },
{ 2, 200, 200, 1, 1, -1, 1 },
{ 4, 200, 100, 4, 2, 2, 2 },
{ 4, 300, 100, 8, 3, 3, 2 },
{ 2, 5000, 1, 4500, 1, -1, 4500 },
{ 2, 5000, 1, 5000, 1, -1, 5000 },
{ 2, 5000, 1, 5001, 2, 2501, 2500 },
};
[Theory]
[MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))]
public void IterateRows_WithEffectiveMinimumPixelsLimit(
int maxDegreeOfParallelism,
int minimumPixelsProcessedPerTask,
int width,
int height,
int expectedNumberOfSteps,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
minimumPixelsProcessedPerTask,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, 0, width, height);
int actualNumberOfSteps = 0;
ParallelHelper.IterateRows(
rectangle,
parallelSettings,
rows =>
{
Assert.True(rows.Min >= 0);
Assert.True(rows.Max <= height);
int step = rows.Max - rows.Min;
int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step);
});
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
}
[Theory]
[MemberData(nameof(IterateRows_WithEffectiveMinimumPixelsLimit_Data))]
public void IterateRowsWithTempBuffer_WithEffectiveMinimumPixelsLimit(
int maxDegreeOfParallelism,
int minimumPixelsProcessedPerTask,
int width,
int height,
int expectedNumberOfSteps,
int expectedStepLength,
int expectedLastStepLength)
{
var parallelSettings = new ParallelExecutionSettings(
maxDegreeOfParallelism,
minimumPixelsProcessedPerTask,
Configuration.Default.MemoryAllocator);
var rectangle = new Rectangle(0, 0, width, height);
int actualNumberOfSteps = 0;
ParallelHelper.IterateRowsWithTempBuffer(
rectangle,
parallelSettings,
(RowInterval rows, Memory<Vector4> buffer) =>
{
Assert.True(rows.Min >= 0);
Assert.True(rows.Max <= height);
int step = rows.Max - rows.Min;
int expected = rows.Max < height ? expectedStepLength : expectedLastStepLength;
Interlocked.Increment(ref actualNumberOfSteps);
Assert.Equal(expected, step);
});
Assert.Equal(expectedNumberOfSteps, actualNumberOfSteps);
}
public static readonly TheoryData<int, int, int, int, int, int, int> IterateRectangularBuffer_Data =
new TheoryData<int, int, int, int, int, int, int>()
{
{ 8, 582, 453, 10, 10, 291, 226 }, // boundary data from DetectEdgesTest.DetectEdges_InBox
{ 2, 582, 453, 10, 10, 291, 226 },
{ 16, 582, 453, 10, 10, 291, 226 },
{ 16, 582, 453, 10, 10, 1, 226 },
{ 16, 1, 453, 0, 10, 1, 226 },
};
[Theory]
[MemberData(nameof(IterateRectangularBuffer_Data))]
public void IterateRectangularBuffer(
int maxDegreeOfParallelism,
int bufferWidth,
int bufferHeight,
int rectX,
int rectY,
int rectWidth,
int rectHeight)
{
MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator;
using (Buffer2D<Point> expected = memoryAllocator.Allocate2D<Point>(bufferWidth, bufferHeight, AllocationOptions.Clean))
using (Buffer2D<Point> actual = memoryAllocator.Allocate2D<Point>(bufferWidth, bufferHeight, AllocationOptions.Clean))
{
var rect = new Rectangle(rectX, rectY, rectWidth, rectHeight);
void FillRow(int y, Buffer2D<Point> buffer)
{
for (int x = rect.Left; x < rect.Right; x++)
{
buffer[x, y] = new Point(x, y);
}
}
// Fill Expected data:
for (int y = rectY; y < rect.Bottom; y++)
{
FillRow(y, expected);
}
// Fill actual data using IterateRows:
var settings = new ParallelExecutionSettings(maxDegreeOfParallelism, memoryAllocator);
ParallelHelper.IterateRows(rect, settings,
rows =>
{
this.Output.WriteLine(rows.ToString());
for (int y = rows.Min; y < rows.Max; y++)
{
FillRow(y, actual);
}
});
// Assert:
TestImageExtensions.CompareBuffers(expected.Span, actual.Span);
}
}
}
}

38
tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs

@ -0,0 +1,38 @@
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Helpers
{
public class RowIntervalTests
{
[Theory]
[InlineData(10, 20, 5, 10)]
[InlineData(1, 10, 0, 10)]
[InlineData(1, 10, 5, 8)]
[InlineData(1, 1, 0, 1)]
[InlineData(10, 20, 9, 10)]
[InlineData(10, 20, 0, 1)]
public void GetMultiRowSpan(int width, int height, int min, int max)
{
using (Buffer2D<int> buffer = Configuration.Default.MemoryAllocator.Allocate2D<int>(width, height))
{
var rows = new RowInterval(min, max);
Span<int> span = buffer.GetMultiRowSpan(rows);
ref int expected0 = ref buffer.Span[min * width];
int expectedLength = (max - min) * width;
ref int actual0 = ref span[0];
Assert.Equal(span.Length, expectedLength);
Assert.True(Unsafe.AreSame(ref expected0, ref actual0));
}
}
}
}

2
tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs

@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void WrapMemory_CreatedImageIsCorrect()
{
Configuration cfg = Configuration.Default.ShallowCopy();
Configuration cfg = Configuration.Default.Clone();
var metaData = new ImageMetaData();
var array = new Rgba32[25];

4
tests/ImageSharp.Tests/Image/ImageTests.cs

@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void Configuration_Width_Height()
{
Configuration configuration = Configuration.Default.ShallowCopy();
Configuration configuration = Configuration.Default.Clone();
using (var image = new Image<Rgba32>(configuration, 11, 23))
{
@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void Configuration_Width_Height_BackroundColor()
{
Configuration configuration = Configuration.Default.ShallowCopy();
Configuration configuration = Configuration.Default.Clone();
Rgba32 color = Rgba32.Aquamarine;
using (var image = new Image<Rgba32>(configuration, 11, 23, color))

8
tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs

@ -32,5 +32,13 @@ namespace SixLabors.ImageSharp.Tests
Assert.Equal(colorTableLength, cloneGifFrameMetaData.ColorTableLength);
Assert.Equal(disposalMethod, cloneGifFrameMetaData.DisposalMethod);
}
[Fact]
public void CloneIsDeep()
{
var metaData = new ImageFrameMetaData();
ImageFrameMetaData clone = metaData.DeepClone();
Assert.False(metaData.GetFormatMetaData(GifFormat.Instance).Equals(clone.GetFormatMetaData(GifFormat.Instance)));
}
}
}

34
tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests
metaData.VerticalResolution = 2;
metaData.Properties.Add(imageProperty);
ImageMetaData clone = metaData.Clone();
ImageMetaData clone = metaData.DeepClone();
Assert.Equal(exifProfile.ToByteArray(), clone.ExifProfile.ToByteArray());
Assert.Equal(4, clone.HorizontalResolution);
@ -37,19 +37,43 @@ namespace SixLabors.ImageSharp.Tests
Assert.Equal(imageProperty, clone.Properties[0]);
}
[Fact]
public void CloneIsDeep()
{
var metaData = new ImageMetaData();
var exifProfile = new ExifProfile();
var imageProperty = new ImageProperty("name", "value");
metaData.ExifProfile = exifProfile;
metaData.HorizontalResolution = 4;
metaData.VerticalResolution = 2;
metaData.Properties.Add(imageProperty);
ImageMetaData clone = metaData.DeepClone();
clone.HorizontalResolution = 2;
clone.VerticalResolution = 4;
Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile));
Assert.False(metaData.HorizontalResolution.Equals(clone.HorizontalResolution));
Assert.False(metaData.VerticalResolution.Equals(clone.VerticalResolution));
Assert.False(metaData.Properties.Equals(clone.Properties));
Assert.False(metaData.GetFormatMetaData(GifFormat.Instance).Equals(clone.GetFormatMetaData(GifFormat.Instance)));
}
[Fact]
public void HorizontalResolution()
{
var metaData = new ImageMetaData();
Assert.Equal(96, metaData.HorizontalResolution);
metaData.HorizontalResolution=0;
metaData.HorizontalResolution = 0;
Assert.Equal(96, metaData.HorizontalResolution);
metaData.HorizontalResolution=-1;
metaData.HorizontalResolution = -1;
Assert.Equal(96, metaData.HorizontalResolution);
metaData.HorizontalResolution=1;
metaData.HorizontalResolution = 1;
Assert.Equal(1, metaData.HorizontalResolution);
}

28
tests/ImageSharp.Tests/MetaData/Profiles/Exif/ExifProfileTests.cs

@ -67,16 +67,16 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void ConstructorCopy()
{
Assert.Throws<ArgumentNullException>(() => { new ExifProfile((ExifProfile)null); });
Assert.Throws<NullReferenceException>(() => ((ExifProfile)null).DeepClone());
ExifProfile profile = GetExifProfile();
var clone = new ExifProfile(profile);
ExifProfile clone = profile.DeepClone();
TestProfile(clone);
profile.SetValue(ExifTag.ColorSpace, (ushort)2);
clone = new ExifProfile(profile);
clone = profile.DeepClone();
TestProfile(clone);
}
@ -234,10 +234,12 @@ namespace SixLabors.ImageSharp.Tests
exifProfile.SetValue(ExifTag.XResolution, new Rational(200));
exifProfile.SetValue(ExifTag.YResolution, new Rational(300));
var metaData = new ImageMetaData();
metaData.ExifProfile = exifProfile;
metaData.HorizontalResolution = 200;
metaData.VerticalResolution = 300;
var metaData = new ImageMetaData
{
ExifProfile = exifProfile,
HorizontalResolution = 200,
VerticalResolution = 300
};
metaData.HorizontalResolution = 100;
@ -355,11 +357,11 @@ namespace SixLabors.ImageSharp.Tests
// act
Image<Rgba32> reloadedImage = WriteAndRead(image, imageFormat);
// assert
ExifProfile actual = reloadedImage.MetaData.ExifProfile;
Assert.NotNull(actual);
foreach(KeyValuePair<ExifTag, object> expectedProfileValue in TestProfileValues)
foreach (KeyValuePair<ExifTag, object> expectedProfileValue in TestProfileValues)
{
ExifValue actualProfileValue = actual.GetValue(expectedProfileValue.Key);
Assert.NotNull(actualProfileValue);
@ -371,7 +373,7 @@ namespace SixLabors.ImageSharp.Tests
public void ProfileToByteArray()
{
// arrange
byte[] exifBytesWithExifCode = ProfileResolver.ExifMarker.Concat(ExifConstants.LittleEndianByteOrderMarker).ToArray();
byte[] exifBytesWithExifCode = ProfileResolver.ExifMarker.Concat(ExifConstants.LittleEndianByteOrderMarker).ToArray();
byte[] exifBytesWithoutExifCode = ExifConstants.LittleEndianByteOrderMarker;
ExifProfile expectedProfile = CreateExifProfile();
var expectedProfileTags = expectedProfile.Values.Select(x => x.Tag).ToList();
@ -384,7 +386,7 @@ namespace SixLabors.ImageSharp.Tests
Assert.NotNull(actualBytes);
Assert.NotEmpty(actualBytes);
Assert.Equal(exifBytesWithoutExifCode, actualBytes.Take(exifBytesWithoutExifCode.Length).ToArray());
foreach(ExifTag expectedProfileTag in expectedProfileTags)
foreach (ExifTag expectedProfileTag in expectedProfileTags)
{
ExifValue actualProfileValue = actualProfile.GetValue(expectedProfileTag);
ExifValue expectedProfileValue = expectedProfile.GetValue(expectedProfileTag);
@ -396,7 +398,7 @@ namespace SixLabors.ImageSharp.Tests
{
var profile = new ExifProfile();
foreach(KeyValuePair<ExifTag, object> exifProfileValue in TestProfileValues)
foreach (KeyValuePair<ExifTag, object> exifProfileValue in TestProfileValues)
{
profile.SetValue(exifProfileValue.Key, exifProfileValue.Value);
}
@ -416,7 +418,7 @@ namespace SixLabors.ImageSharp.Tests
private static Image<Rgba32> WriteAndRead(Image<Rgba32> image, TestImageWriteFormat imageFormat)
{
switch(imageFormat)
switch (imageFormat)
{
case TestImageWriteFormat.Jpeg:
return WriteAndReadJpeg(image);

2
tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs

@ -10,8 +10,6 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution
{
using SixLabors.ImageSharp.Advanced;
public class DetectEdgesTest : FileTestBase
{
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F);

24
tests/ImageSharp.Tests/Processing/Processors/Transforms/CropTest.cs

@ -1,24 +1,34 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
[GroupOutput("Transforms")]
public class CropTest : FileTestBase
{
[Theory]
[WithFileCollection(nameof(DefaultFiles), DefaultPixelType)]
public void ImageShouldCrop<TPixel>(TestImageProvider<TPixel> provider)
[WithTestPatternImages(70, 30, PixelTypes.Rgba32, 0, 0, 70, 30)]
[WithTestPatternImages(50, 50, PixelTypes.Rgba32, -1, -1, 100, 200)]
[WithTestPatternImages(30, 70, PixelTypes.Rgba32, 7, 13, 20, 50)]
public void Crop<TPixel>(TestImageProvider<TPixel> provider, int x, int y, int w, int h)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Crop(image.Width / 2, image.Height / 2));
image.DebugSave(provider);
}
var rect = new Rectangle(x, y, w, h);
FormattableString info = $"X{x}Y{y}.W{w}H{h}";
provider.RunValidatingProcessorTest(
ctx => ctx.Crop(rect),
info,
appendPixelTypeToFileName: false,
comparer: ImageComparer.Exact);
}
}
}

19
tests/ImageSharp.Tests/Processing/Processors/Transforms/FlipTests.cs

@ -5,23 +5,24 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
using SixLabors.ImageSharp.Processing;
[GroupOutput("Transforms")]
public class FlipTests
{
public static readonly TheoryData<FlipMode> FlipValues
= new TheoryData<FlipMode>
{
{ FlipMode.None },
{ FlipMode.Vertical },
{ FlipMode.Horizontal },
};
public static readonly TheoryData<FlipMode> FlipValues =
new TheoryData<FlipMode>
{
FlipMode.None,
FlipMode.Vertical,
FlipMode.Horizontal,
};
[Theory]
[WithTestPatternImages(nameof(FlipValues), 20, 37, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(FlipValues), 53, 37, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(FlipValues), 17, 32, PixelTypes.Rgba32)]
public void Flip<TPixel>(TestImageProvider<TPixel> provider, FlipMode flipMode)

23
tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs

@ -7,7 +7,8 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public class RotateTests : FileTestBase
[GroupOutput("Transforms")]
public class RotateTests
{
public static readonly TheoryData<float> RotateAngles
= new TheoryData<float>
@ -25,29 +26,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
};
[Theory]
[WithTestPatternImages(nameof(RotateAngles), 100, 50, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateAngles), 50, 100, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateAngles), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateAngles), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithAngle<TPixel>(TestImageProvider<TPixel> provider, float value)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Rotate(value));
image.DebugSave(provider, value);
}
provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
}
[Theory]
[WithTestPatternImages(nameof(RotateEnumValues), 100, 50, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateEnumValues), 50, 100, DefaultPixelType)]
[WithTestPatternImages(nameof(RotateEnumValues), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateEnumValues), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithRotateTypeEnum<TPixel>(TestImageProvider<TPixel> provider, RotateMode value)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.Rotate(value));
image.DebugSave(provider, value);
}
provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
}
}
}

2
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3);
private ITestOutputHelper Output { get; }
public static readonly TheoryData<string> ResamplerNames = new TheoryData<string>
{
nameof(KnownResamplers.Bicubic),

2
tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs

@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Tests
public virtual string SourceFileOrDescription => "";
public Configuration Configuration { get; set; } = Configuration.Default.ShallowCopy();
public Configuration Configuration { get; set; } = Configuration.Default.Clone();
/// <summary>
/// Utility instance to provide informations about the test image & manage input/output

6
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -29,13 +29,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs
{
if (magickImage.Depth == 8)
{
byte[] data = pixels.ToByteArray("RGBA");
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);
PixelOperations<TPixel>.Instance.PackFromRgba32Bytes(data, resultPixels, resultPixels.Length);
}
else if (magickImage.Depth == 16)
{
ushort[] data = pixels.ToShortArray("RGBA");
ushort[] data = pixels.ToShortArray(PixelMapping.RGBA);
Span<byte> bytes = MemoryMarshal.Cast<ushort, byte>(data.AsSpan());
PixelOperations<TPixel>.Instance.PackFromRgba64Bytes(bytes, resultPixels, resultPixels.Length);

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

@ -441,14 +441,23 @@ namespace SixLabors.ImageSharp.Tests
{
Span<TPixel> actualPixels = image.GetPixelSpan();
Assert.True(expectedPixels.Length == actualPixels.Length, "Buffer sizes are not equal!");
CompareBuffers(expectedPixels, actualPixels);
for (int i = 0; i < expectedPixels.Length; i++)
return image;
}
public static void CompareBuffers<T>(Span<T> expected, Span<T> actual)
where T : struct, IEquatable<T>
{
Assert.True(expected.Length == actual.Length, "Buffer sizes are not equal!");
for (int i = 0; i < expected.Length; i++)
{
Assert.True(expectedPixels[i].Equals(actualPixels[i]), $"Pixels are different on position {i}!");
}
T x = expected[i];
T a = actual[i];
return image;
Assert.True(x.Equals(a), $"Buffers differ at position {i}! Expected: {x} | Actual: {a}");
}
}
/// <summary>

8
tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs

@ -14,10 +14,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests
public class MagickReferenceCodecTests
{
public MagickReferenceCodecTests(ITestOutputHelper output)
{
this.Output = output;
}
public MagickReferenceCodecTests(ITestOutputHelper output) => this.Output = output;
private ITestOutputHelper Output { get; }
@ -61,6 +58,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests
[WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Rgb48Bpp)]
[WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Rgb48BppInterlaced)]
[WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Rgb48BppTrans)]
[WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Gray16Bit)]
public void MagickDecode_16BitDepthImage_IsApproximatelyEquivalentTo_SystemDrawingResult<TPixel>(TestImageProvider<TPixel> dummyProvider, string testImage)
where TPixel : struct, IPixel<TPixel>
{
@ -71,7 +69,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests
// 1020 == 4 * 255 (Equivalent to manhattan distance of 1+1+1+1=4 in Rgba32 space)
var comparer = ImageComparer.TolerantPercentage(1, 1020);
using (var mImage = Image.Load<TPixel>(path, magickDecoder))
using (var sdImage = Image.Load<TPixel>(path, sdDecoder))
{

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

@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests
private ITestOutputHelper Output { get; }
public const string SkipBenchmarks =
#if false
#if true
"Benchmark, enable manually!";
#else
null;

2
tests/Images/External

@ -1 +1 @@
Subproject commit 6abc3bc0ac253a24c9e88e68d7b7d853350a85da
Subproject commit 7f4d2d64c6b820ca2b6827e6a8540a1013305ccf
Loading…
Cancel
Save