Browse Source

Merge branch 'main' into js/smaller-aot

pull/2514/head
James Jackson-South 3 years ago
parent
commit
d1ab884b54
  1. 5
      .github/workflows/build-and-test.yml
  2. 2
      .github/workflows/code-coverage.yml
  3. 10
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  4. 4
      src/ImageSharp/Color/Color.Conversions.cs
  5. 27
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  6. 8
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  7. 3
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  8. 70
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  9. 612
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  10. 30
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  11. 20
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  12. 4
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  13. 9
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  14. 7
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs
  15. 35
      src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
  16. 9
      src/ImageSharp/Formats/Png/PngEncoder.cs
  17. 3
      src/ImageSharp/Formats/QuantizingImageEncoder.cs
  18. 6
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  19. 10
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  20. 66
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  21. 10
      src/ImageSharp/ImageFrame{TPixel}.cs
  22. 11
      src/ImageSharp/Memory/Buffer2D{T}.cs
  23. 47
      src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs
  24. 56
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  25. 15
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  26. 17
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
  27. 8
      src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs
  28. 19
      tests/ImageSharp.Benchmarks/Processing/OilPaint.cs
  29. 39
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  30. 14
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  31. 44
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  32. 11
      tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs
  33. 15
      tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
  34. 17
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  35. 5
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  36. 76
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  37. 91
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  38. 39
      tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
  39. 9
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
  40. 19
      tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs
  41. 13
      tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs
  42. 4
      tests/ImageSharp.Tests/TestImages.cs
  43. 47
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs
  44. 50
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
  45. 34
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  46. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/00.png
  47. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/08.png
  48. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/104.png
  49. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/112.png
  50. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/120.png
  51. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/128.png
  52. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/136.png
  53. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/144.png
  54. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/152.png
  55. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/16.png
  56. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/24.png
  57. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/32.png
  58. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/40.png
  59. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/48.png
  60. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/56.png
  61. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/64.png
  62. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/72.png
  63. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/80.png
  64. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/88.png
  65. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/96.png
  66. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/00.png
  67. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/08.png
  68. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/16.png
  69. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/24.png
  70. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/32.png
  71. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/40.png
  72. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/48.png
  73. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/56.png
  74. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/64.png
  75. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/72.png
  76. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/80.png
  77. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/88.png
  78. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/96.png
  79. 3
      tests/Images/Input/Gif/issues/issue_2198.gif
  80. 3
      tests/Images/Input/Gif/issues/issue_2450.gif
  81. 3
      tests/Images/Input/Gif/issues/issue_2450_2.gif
  82. 3
      tests/Images/Input/Jpg/issues/Hang_C438A851.jpg

5
.github/workflows/build-and-test.yml

@ -9,6 +9,7 @@ on:
pull_request:
branches:
- main
- release/*
types: [ labeled, opened, synchronize, reopened ]
jobs:
Build:
@ -75,7 +76,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@ -171,7 +172,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive

2
.github/workflows/code-coverage.yml

@ -24,7 +24,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive

10
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -50,7 +50,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
@ -115,7 +115,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
@ -180,7 +180,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
@ -242,7 +242,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
@ -270,7 +270,7 @@ public static partial class ParallelRowIterator
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
private static void ValidateRectangle(Rectangle rectangle)
{

4
src/ImageSharp/Color/Color.Conversions.cs

@ -139,7 +139,7 @@ public readonly partial struct Color
/// </summary>
/// <param name="color">The <see cref="Color"/>.</param>
/// <returns>The <see cref="Vector4"/>.</returns>
public static explicit operator Vector4(Color color) => color.ToVector4();
public static explicit operator Vector4(Color color) => color.ToScaledVector4();
/// <summary>
/// Converts an <see cref="Vector4"/> to <see cref="Color"/>.
@ -228,7 +228,7 @@ public readonly partial struct Color
}
[MethodImpl(InliningOptions.ShortMethod)]
internal Vector4 ToVector4()
internal Vector4 ToScaledVector4()
{
if (this.boxedHighPrecisionPixel is null)
{

27
src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs

@ -629,6 +629,33 @@ internal static partial class SimdUtils
return Avx.Subtract(c, Avx.Multiply(a, b));
}
/// <summary>
/// Blend packed 8-bit integers from <paramref name="left"/> and <paramref name="right"/> using <paramref name="mask"/>.
/// The high bit of each corresponding <paramref name="mask"/> byte determines the selection.
/// If the high bit is set the element of <paramref name="left"/> is selected.
/// The element of <paramref name="right"/> is selected otherwise.
/// </summary>
/// <param name="left">The left vector.</param>
/// <param name="right">The right vector.</param>
/// <param name="mask">The mask vector.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> BlendVariable(Vector128<byte> left, Vector128<byte> right, Vector128<byte> mask)
{
if (Sse41.IsSupported)
{
return Sse41.BlendVariable(left, right, mask);
}
else if (Sse2.IsSupported)
{
return Sse2.Or(Sse2.And(right, mask), Sse2.AndNot(mask, left));
}
// Use a signed shift right to create a mask with the sign bit.
Vector128<short> signedMask = AdvSimd.ShiftRightArithmetic(mask.AsInt16(), 7);
return AdvSimd.BitwiseSelect(signedMask, right.AsInt16(), left.AsInt16()).AsByte();
}
/// <summary>
/// <see cref="ByteToNormalizedFloat"/> as many elements as possible, slicing them down (keeping the remainder).
/// </summary>

8
src/ImageSharp/Formats/Bmp/BmpEncoder.cs

@ -1,6 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Bmp;
/// <summary>
@ -8,6 +11,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp;
/// </summary>
public sealed class BmpEncoder : QuantizingImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="BmpEncoder"/> class.
/// </summary>
public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree;
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>

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

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp;
@ -100,7 +101,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel;
this.quantizer = encoder.Quantizer;
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
}

70
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -29,6 +29,16 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// </summary>
private IMemoryOwner<byte>? globalColorTable;
/// <summary>
/// The current local color table.
/// </summary>
private IMemoryOwner<byte>? currentLocalColorTable;
/// <summary>
/// Gets the size in bytes of the current local color table.
/// </summary>
private int currentLocalColorTableSize;
/// <summary>
/// The area to restore.
/// </summary>
@ -159,6 +169,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
finally
{
this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
}
if (image is null)
@ -229,6 +240,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
finally
{
this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
}
if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
@ -332,7 +344,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span.Slice(1)).RepeatCount;
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
stream.Skip(1); // Skip the terminator.
return;
}
@ -415,25 +427,27 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{
this.ReadImageDescriptor(stream);
IMemoryOwner<byte>? localColorTable = null;
Buffer2D<byte>? indices = null;
try
{
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
if (this.imageDescriptor.LocalColorTableFlag)
bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag;
if (hasLocalColorTable)
{
int length = this.imageDescriptor.LocalColorTableSize * 3;
localColorTable = this.configuration.MemoryAllocator.Allocate<byte>(length, AllocationOptions.Clean);
stream.Read(localColorTable.GetSpan());
// Read and store the local color table. We allocate the maximum possible size and slice to match.
int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
indices = this.configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(stream, indices);
Span<byte> rawColorTable = default;
if (localColorTable != null)
if (hasLocalColorTable)
{
rawColorTable = localColorTable.GetSpan();
rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize];
}
else if (this.globalColorTable != null)
{
@ -448,7 +462,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
}
finally
{
localColorTable?.Dispose();
indices?.Dispose();
}
}
@ -509,7 +522,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
prevFrame = previousFrame;
}
currentFrame = image!.Frames.CreateFrame();
// We create a clone of the frame and add it.
// We will overpaint the difference of pixels on the current frame to create a complete image.
// This ensures that we have enough pixel data to process without distortion. #2450
currentFrame = image!.Frames.AddFrame(previousFrame);
this.SetFrameMetadata(currentFrame.Metadata);
@ -631,7 +647,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
// Skip the color table for this frame if local.
if (this.imageDescriptor.LocalColorTableFlag)
{
stream.Skip(this.imageDescriptor.LocalColorTableSize * 3);
// Read and store the local color table. We allocate the maximum possible size and slice to match.
int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
// Skip the frame indices. Pixels length + mincode size.
@ -682,7 +701,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global;
gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
}
if (this.imageDescriptor.LocalColorTableFlag
@ -690,13 +708,23 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local;
gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize;
Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize];
ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]);
for (int i = 0; i < colorTable.Length; i++)
{
colorTable[i] = new Color(rgbTable[i]);
}
gifMeta.LocalColorTable = colorTable;
}
// Graphics control extensions is optional.
if (this.graphicsControlExtension != default)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag;
gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex;
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
}
@ -751,14 +779,22 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
this.gifMetadata.GlobalColorTableLength = globalColorTableLength;
if (globalColorTableLength > 0)
{
this.globalColorTable = this.memoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean);
// Read the global color table data from the stream
stream.Read(this.globalColorTable.GetSpan());
// Read the global color table data from the stream and preserve it in the gif metadata
Span<byte> globalColorTableSpan = this.globalColorTable.GetSpan();
stream.Read(globalColorTableSpan);
Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize];
ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(globalColorTableSpan);
for (int i = 0; i < colorTable.Length; i++)
{
colorTable[i] = new Color(rgbTable[i]);
}
this.gifMetadata.GlobalColorTable = colorTable;
}
}
}

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

@ -2,13 +2,17 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif;
@ -36,17 +40,17 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// The quantizer used to generate the color palette.
/// </summary>
private readonly IQuantizer quantizer;
private IQuantizer? quantizer;
/// <summary>
/// The color table mode: Global or local.
/// Whether the quantizer was supplied via options.
/// </summary>
private GifColorTableMode? colorTableMode;
private readonly bool hasQuantizer;
/// <summary>
/// The number of bits requires to store the color palette.
/// The color table mode: Global or local.
/// </summary>
private int bitDepth;
private GifColorTableMode? colorTableMode;
/// <summary>
/// The pixel sampling strategy for global quantization.
@ -56,7 +60,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="encoder">The encoder with options.</param>
public GifEncoderCore(Configuration configuration, GifEncoder encoder)
{
@ -64,6 +68,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.memoryAllocator = configuration.MemoryAllocator;
this.skipMetadata = encoder.SkipMetadata;
this.quantizer = encoder.Quantizer;
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
}
@ -86,8 +91,28 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette.
IndexedImageFrame<TPixel>? quantized;
// Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized = null;
// Work out if there is an explicit transparent index set for the frame. We use that to ensure the
// correct value is set for the background index when quantizing.
image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
if (this.quantizer is null)
{
// Is this a gif with color information. If so use that, otherwise use octree.
if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{
// We avoid dithering by default to preserve the original colors.
this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }, transparencyIndex);
}
else
{
this.quantizer = KnownQuantizers.Octree;
}
}
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{
if (useGlobalTable)
@ -102,19 +127,24 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
// Get the number of bits.
this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
// Write the header.
WriteHeader(stream);
// Write the LSD.
int index = GetTransparentIndex(quantized);
this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, index, useGlobalTable, stream);
transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
byte backgroundIndex = unchecked((byte)transparencyIndex);
if (transparencyIndex == -1)
{
backgroundIndex = gifMetadata.BackgroundColorIndex;
}
// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
if (useGlobalTable)
{
this.WriteColorTable(quantized, stream);
this.WriteColorTable(quantized, bitDepth, stream);
}
if (!this.skipMetadata)
@ -127,41 +157,68 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
}
this.EncodeFrames(stream, image, quantized, quantized.Palette.ToArray());
this.EncodeFirstFrame(stream, frameMetadata, quantized, transparencyIndex);
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? Array.Empty<TPixel>() : quantized.Palette.ToArray();
quantized.Dispose();
this.EncodeAdditionalFrames(stream, image, globalPalette);
stream.WriteByte(GifConstants.EndIntroducer);
}
private void EncodeFrames<TPixel>(
private void EncodeAdditionalFrames<TPixel>(
Stream stream,
Image<TPixel> image,
IndexedImageFrame<TPixel> quantized,
ReadOnlyMemory<TPixel> palette)
ReadOnlyMemory<TPixel> globalPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
{
return;
}
PaletteQuantizer<TPixel> paletteQuantizer = default;
bool hasPaletteQuantizer = false;
for (int i = 0; i < image.Frames.Count; i++)
// Store the first frame as a reference for de-duplication comparison.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
// This is more expensive memory-wise than de-duplicating indexed buffer but allows us to deduplicate
// frames using both local and global palettes.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.GetConfiguration(), previousFrame.Size());
for (int i = 1; i < image.Frames.Count; i++)
{
// Gather the metadata for this frame.
ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata;
bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata!.ColorTableMode == GifColorTableMode.Local);
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrameMetadata metadata = currentFrame.Metadata;
metadata.TryGetGifMetadata(out GifFrameMetadata? gifMetadata);
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
// The palette quantizer can reuse the same pixel map across multiple frames
// since the palette is unchanging. This allows a reduction of memory usage across
// multi frame gifs using a global palette.
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// This allows a reduction of memory usage across multi-frame gifs using a global palette
// and also allows use to reuse the cache from previous runs.
int transparencyIndex = gifMetadata?.HasTransparency == true ? gifMetadata.TransparencyIndex : -1;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
paletteQuantizer = new(this.configuration, this.quantizer.Options, palette);
}
this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, ref quantized!, ref paletteQuantizer);
this.EncodeAdditionalFrame(
stream,
previousFrame,
currentFrame,
encodingFrame,
useLocal,
gifMetadata,
paletteQuantizer);
// Clean up for the next run.
quantized.Dispose();
previousFrame = currentFrame;
}
if (hasPaletteQuantizer)
@ -170,88 +227,419 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
private void EncodeFrame<TPixel>(
private void EncodeFirstFrame<TPixel>(
Stream stream,
ImageFrame<TPixel> frame,
int frameIndex,
GifFrameMetadata? metadata,
IndexedImageFrame<TPixel> quantized,
int transparencyIndex)
where TPixel : unmanaged, IPixel<TPixel>
{
this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
Buffer2D<byte> indices = ((IPixelSource)quantized).PixelBuffer;
Rectangle interest = indices.FullRectangle();
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata?.ColorTableMode == GifColorTableMode.Local);
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
if (useLocal)
{
this.WriteColorTable(quantized, bitDepth, stream);
}
this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex);
}
private void EncodeAdditionalFrame<TPixel>(
Stream stream,
ImageFrame<TPixel> previousFrame,
ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel> encodingFrame,
bool useLocal,
GifFrameMetadata? metadata,
ref IndexedImageFrame<TPixel> quantized,
ref PaletteQuantizer<TPixel> paletteQuantizer)
PaletteQuantizer<TPixel> globalPaletteQuantizer)
where TPixel : unmanaged, IPixel<TPixel>
{
// The first frame has already been quantized so we do not need to do so again.
if (frameIndex > 0)
// Capture any explicit transparency index from the metadata.
// We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata?.HasTransparency == true ? metadata.TransparencyIndex : -1;
Vector4 replacement = Vector4.Zero;
if (transparencyIndex >= 0)
{
if (useLocal)
{
// Reassign using the current frame and details.
QuantizerOptions? options = null;
int colorTableLength = metadata?.ColorTableLength ?? 0;
if (colorTableLength > 0)
if (metadata?.LocalColorTable?.Length > 0)
{
options = new()
ReadOnlySpan<Color> palette = metadata.LocalColorTable.Value.Span;
if (transparencyIndex < palette.Length)
{
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale,
MaxColors = colorTableLength
};
replacement = palette[transparencyIndex].ToScaledVector4();
}
}
}
else
{
ReadOnlySpan<TPixel> palette = globalPaletteQuantizer.Palette.Span;
if (transparencyIndex < palette.Length)
{
replacement = palette[transparencyIndex].ToScaledVector4();
}
}
}
this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options ?? this.quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
IndexedImageFrame<TPixel> quantized;
if (useLocal)
{
// Reassign using the current frame and details.
if (metadata?.LocalColorTable?.Length > 0)
{
// We can use the color data from the decoded metadata here.
// We avoid dithering by default to preserve the original colors.
ReadOnlyMemory<Color> palette = metadata.LocalColorTable.Value;
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds());
}
else
{
// Quantize the image using the global palette.
quantized = paletteQuantizer.QuantizeFrame(frame, frame.Bounds());
// We must quantize the frame to generate a local color table.
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds());
}
}
else
{
// Quantize the image using the global palette.
// Individual frames, though using the shared palette, can use a different transparent index to represent transparency.
globalPaletteQuantizer.SetTransparentIndex(transparencyIndex);
quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds());
}
// Recalculate the transparency index as depending on the quantizer used could have a new value.
transparencyIndex = GetTransparentIndex(quantized, metadata);
this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
// Trim down the buffer to the minimum size required.
Buffer2D<byte> indices = ((IPixelSource)quantized).PixelBuffer;
Rectangle interest = TrimTransparentPixels(indices, transparencyIndex);
this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
if (useLocal)
{
this.WriteColorTable(quantized, bitDepth, stream);
}
// Do we have extension information to write?
int index = GetTransparentIndex(quantized);
if (metadata != null || index > -1)
this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex);
}
private void DeDuplicatePixels<TPixel>(
ImageFrame<TPixel> backgroundFrame,
ImageFrame<TPixel> sourceFrame,
ImageFrame<TPixel> resultFrame,
Vector4 replacement)
where TPixel : unmanaged, IPixel<TPixel>
{
IMemoryOwner<Vector4> buffers = this.memoryAllocator.Allocate<Vector4>(backgroundFrame.Width * 3);
Span<Vector4> background = buffers.GetSpan()[..backgroundFrame.Width];
Span<Vector4> source = buffers.GetSpan()[backgroundFrame.Width..];
Span<Vector4> result = buffers.GetSpan()[(backgroundFrame.Width * 2)..];
// TODO: This algorithm is greedy and will always replace matching colors, however, theoretically, if the proceeding color
// is the same, but not replaced, you would actually be better of not replacing it since longer runs compress better.
// This would require a more complex algorithm.
for (int y = 0; y < backgroundFrame.Height; y++)
{
this.WriteGraphicalControlExtension(metadata ?? new(), index, stream);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, backgroundFrame.DangerousGetPixelRowMemory(y).Span, background, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceFrame.DangerousGetPixelRowMemory(y).Span, source, PixelConversionModifiers.Scale);
ref Vector256<float> backgroundBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(background));
ref Vector256<float> sourceBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(source));
ref Vector256<float> resultBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(result));
uint x = 0;
int remaining = background.Length;
if (Avx2.IsSupported && remaining >= 2)
{
Vector256<float> replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
while (remaining >= 2)
{
Vector256<float> b = Unsafe.Add(ref backgroundBase, x);
Vector256<float> s = Unsafe.Add(ref sourceBase, x);
Vector256<int> m = Avx.CompareEqual(b, s).AsInt32();
m = Avx2.HorizontalAdd(m, m);
m = Avx2.HorizontalAdd(m, m);
m = Avx2.CompareEqual(m, Vector256.Create(-4));
Unsafe.Add(ref resultBase, x) = Avx.BlendVariable(s, replacement256, m.AsSingle());
x++;
remaining -= 2;
}
}
for (int i = remaining; i >= 0; i--)
{
x = (uint)i;
Vector4 b = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref backgroundBase), x);
Vector4 s = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref sourceBase), x);
ref Vector4 r = ref Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref resultBase), x);
r = (b == s) ? replacement : s;
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale);
}
}
this.WriteImageDescriptor(frame, useLocal, stream);
private static Rectangle TrimTransparentPixels(Buffer2D<byte> buffer, int transparencyIndex)
{
if (transparencyIndex < 0)
{
return buffer.FullRectangle();
}
if (useLocal)
byte trimmableIndex = unchecked((byte)transparencyIndex);
int top = int.MinValue;
int bottom = int.MaxValue;
int left = int.MaxValue;
int right = int.MinValue;
int minY = -1;
bool isTransparentRow = true;
// Run through the buffer in a single pass. Use variables to track the min/max values.
for (int y = 0; y < buffer.Height; y++)
{
isTransparentRow = true;
Span<byte> rowSpan = buffer.DangerousGetRowSpan(y);
ref byte rowPtr = ref MemoryMarshal.GetReference(rowSpan);
nint rowLength = (nint)(uint)rowSpan.Length;
nint x = 0;
#if NET7_0_OR_GREATER
if (Vector128.IsHardwareAccelerated && rowLength >= Vector128<byte>.Count)
{
Vector256<byte> trimmableVec256 = Vector256.Create(trimmableIndex);
if (Vector256.IsHardwareAccelerated && rowLength >= Vector256<byte>.Count)
{
do
{
Vector256<byte> vec = Vector256.LoadUnsafe(ref rowPtr, (nuint)x);
Vector256<byte> notEquals = ~Vector256.Equals(vec, trimmableVec256);
uint mask = notEquals.ExtractMostSignificantBits();
if (mask != 0)
{
isTransparentRow = false;
nint start = x + (nint)uint.TrailingZeroCount(mask);
nint end = (nint)uint.LeadingZeroCount(mask);
// end is from the end, but we need the index from the beginning
end = x + Vector256<byte>.Count - 1 - end;
left = Math.Min(left, (int)start);
right = Math.Max(right, (int)end);
}
x += Vector256<byte>.Count;
}
while (x <= rowLength - Vector256<byte>.Count);
}
Vector128<byte> trimmableVec = Vector256.IsHardwareAccelerated
? trimmableVec256.GetLower()
: Vector128.Create(trimmableIndex);
while (x <= rowLength - Vector128<byte>.Count)
{
Vector128<byte> vec = Vector128.LoadUnsafe(ref rowPtr, (nuint)x);
Vector128<byte> notEquals = ~Vector128.Equals(vec, trimmableVec);
uint mask = notEquals.ExtractMostSignificantBits();
if (mask != 0)
{
isTransparentRow = false;
nint start = x + (nint)uint.TrailingZeroCount(mask);
nint end = (nint)uint.LeadingZeroCount(mask) - Vector128<byte>.Count;
// end is from the end, but we need the index from the beginning
end = x + Vector128<byte>.Count - 1 - end;
left = Math.Min(left, (int)start);
right = Math.Max(right, (int)end);
}
x += Vector128<byte>.Count;
}
}
#else
if (Sse41.IsSupported && rowLength >= Vector128<byte>.Count)
{
Vector256<byte> trimmableVec256 = Vector256.Create(trimmableIndex);
if (Avx2.IsSupported && rowLength >= Vector256<byte>.Count)
{
do
{
Vector256<byte> vec = Unsafe.ReadUnaligned<Vector256<byte>>(ref Unsafe.Add(ref rowPtr, x));
Vector256<byte> notEquals = Avx2.CompareEqual(vec, trimmableVec256);
notEquals = Avx2.Xor(notEquals, Vector256<byte>.AllBitsSet);
int mask = Avx2.MoveMask(notEquals);
if (mask != 0)
{
isTransparentRow = false;
nint start = x + (nint)(uint)BitOperations.TrailingZeroCount(mask);
nint end = (nint)(uint)BitOperations.LeadingZeroCount((uint)mask);
// end is from the end, but we need the index from the beginning
end = x + Vector256<byte>.Count - 1 - end;
left = Math.Min(left, (int)start);
right = Math.Max(right, (int)end);
}
x += Vector256<byte>.Count;
}
while (x <= rowLength - Vector256<byte>.Count);
}
Vector128<byte> trimmableVec = Sse41.IsSupported
? trimmableVec256.GetLower()
: Vector128.Create(trimmableIndex);
while (x <= rowLength - Vector128<byte>.Count)
{
Vector128<byte> vec = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref rowPtr, x));
Vector128<byte> notEquals = Sse2.CompareEqual(vec, trimmableVec);
notEquals = Sse2.Xor(notEquals, Vector128<byte>.AllBitsSet);
int mask = Sse2.MoveMask(notEquals);
if (mask != 0)
{
isTransparentRow = false;
nint start = x + (nint)(uint)BitOperations.TrailingZeroCount(mask);
nint end = (nint)(uint)BitOperations.LeadingZeroCount((uint)mask) - Vector128<byte>.Count;
// end is from the end, but we need the index from the beginning
end = x + Vector128<byte>.Count - 1 - end;
left = Math.Min(left, (int)start);
right = Math.Max(right, (int)end);
}
x += Vector128<byte>.Count;
}
}
#endif
for (; x < rowLength; ++x)
{
if (Unsafe.Add(ref rowPtr, x) != trimmableIndex)
{
isTransparentRow = false;
left = Math.Min(left, (int)x);
right = Math.Max(right, (int)x);
}
}
if (!isTransparentRow)
{
if (y == 0)
{
// First row is opaque.
// Capture to prevent over assignment when a match is found below.
top = 0;
}
// The minimum top bounds have already been captured.
// Increment the bottom to include the current opaque row.
if (minY < 0 && top != 0)
{
// Increment to the first opaque row.
top++;
}
minY = top;
bottom = y;
}
else
{
// We've yet to hit an opaque row. Capture the top position.
if (minY < 0)
{
top = Math.Max(top, y);
}
bottom = Math.Min(bottom, y);
}
}
if (left == int.MaxValue)
{
left = 0;
}
if (right == int.MinValue)
{
this.WriteColorTable(quantized, stream);
right = buffer.Width;
}
this.WriteImageData(quantized, stream);
if (top == bottom || left == right)
{
// The entire image is transparent.
return buffer.FullRectangle();
}
if (!isTransparentRow)
{
// Last row is opaque.
bottom = buffer.Height;
}
return Rectangle.FromLTRB(left, top, Math.Min(right + 1, buffer.Width), Math.Min(bottom + 1, buffer.Height));
}
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// </summary>
/// <param name="quantized">The quantized frame.</param>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The current gif frame metadata.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized)
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
// Transparent pixels are much more likely to be found at the end of a palette.
int index = -1;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteSpan.Length);
Span<Rgba32> rgbaSpan = rgbaOwner.GetSpan();
PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
if (metadata?.HasTransparency == true)
{
return metadata.TransparencyIndex;
}
for (int i = rgbaSpan.Length - 1; i >= 0; i--)
int index = -1;
if (quantized != null)
{
if (Unsafe.Add(ref rgbaSpanRef, (uint)i).Equals(default))
TPixel transparentPixel = default;
transparentPixel.FromScaledVector4(Vector4.Zero);
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
// Transparent pixels are much more likely to be found at the end of a palette.
for (int i = palette.Length - 1; i >= 0; i--)
{
index = i;
if (palette[i].Equals(transparentPixel))
{
index = i;
}
}
}
@ -271,18 +659,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <param name="metadata">The image metadata.</param>
/// <param name="width">The image width.</param>
/// <param name="height">The image height.</param>
/// <param name="transparencyIndex">The transparency index to set the default background index to.</param>
/// <param name="backgroundIndex">The index to set the default background index to.</param>
/// <param name="useGlobalTable">Whether to use a global or local color table.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor(
ImageMetadata metadata,
int width,
int height,
int transparencyIndex,
byte backgroundIndex,
bool useGlobalTable,
int bitDepth,
Stream stream)
{
byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1);
byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, bitDepth - 1, false, bitDepth - 1);
// The Pixel Aspect Ratio is defined to be the quotient of the pixel's
// width over its height. The value range in this field allows
@ -316,7 +706,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
width: (ushort)width,
height: (ushort)height,
packed: packedValue,
backgroundColorIndex: unchecked((byte)transparencyIndex),
backgroundColorIndex: backgroundIndex,
ratio);
Span<byte> buffer = stackalloc byte[20];
@ -412,16 +802,28 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <param name="metadata">The metadata of the image or frame.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteGraphicalControlExtension(GifFrameMetadata metadata, int transparencyIndex, Stream stream)
private void WriteGraphicalControlExtension(GifFrameMetadata? metadata, int transparencyIndex, Stream stream)
{
GifFrameMetadata? data = metadata;
bool hasTransparency;
if (metadata is null)
{
data = new();
hasTransparency = transparencyIndex >= 0;
}
else
{
hasTransparency = metadata.HasTransparency;
}
byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metadata.DisposalMethod,
transparencyFlag: transparencyIndex > -1);
disposalMethod: data!.DisposalMethod,
transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new(
packed: packedValue,
delayTime: (ushort)metadata.FrameDelay,
transparencyIndex: unchecked((byte)transparencyIndex));
delayTime: (ushort)data.FrameDelay,
transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue);
this.WriteExtension(extension, stream);
}
@ -443,7 +845,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
IMemoryOwner<byte>? owner = null;
Span<byte> extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
Span<byte> extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
if (extensionSize > 128)
{
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
@ -466,26 +868,25 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
/// <summary>
/// Writes the image descriptor to the stream.
/// Writes the image frame descriptor to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
/// <param name="rectangle">The frame location and size.</param>
/// <param name="hasColorTable">Whether to use the global color table.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, bool hasColorTable, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, int bitDepth, Stream stream)
{
byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: hasColorTable,
interfaceFlag: false,
sortFlag: false,
localColorTableSize: this.bitDepth - 1);
localColorTableSize: bitDepth - 1);
GifImageDescriptor descriptor = new(
left: 0,
top: 0,
width: (ushort)image.Width,
height: (ushort)image.Height,
left: (ushort)rectangle.X,
top: (ushort)rectangle.Y,
width: (ushort)rectangle.Width,
height: (ushort)rectangle.Height,
packed: packedValue);
Span<byte> buffer = stackalloc byte[20];
@ -499,12 +900,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteColorTable<TPixel>(IndexedImageFrame<TPixel> image, Stream stream)
private void WriteColorTable<TPixel>(IndexedImageFrame<TPixel> image, int bitDepth, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// The maximum number of colors for the bit depth
int colorTableLength = ColorNumerics.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf<Rgb24>();
int colorTableLength = ColorNumerics.GetColorCountForBitDepth(bitDepth) * Unsafe.SizeOf<Rgb24>();
using IMemoryOwner<byte> colorTable = this.memoryAllocator.Allocate<byte>(colorTableLength, AllocationOptions.Clean);
Span<byte> colorTableSpan = colorTable.GetSpan();
@ -521,13 +923,23 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="IndexedImageFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="indices">The <see cref="Buffer2DRegion{Byte}"/> containing indexed pixels.</param>
/// <param name="interest">The region of interest.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageData<TPixel>(IndexedImageFrame<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
/// <param name="paletteLength">The length of the frame color palette.</param>
/// <param name="transparencyIndex">The index of the color used to represent transparency.</param>
private void WriteImageData(Buffer2D<byte> indices, Rectangle interest, Stream stream, int paletteLength, int transparencyIndex)
{
using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth);
encoder.Encode(((IPixelSource)image).PixelBuffer, stream);
Buffer2DRegion<byte> region = indices.GetRegion(interest);
// Pad the bit depth when required for encoding the image data.
// This is a common trick which allows to use out of range indexes for transparency and avoid allocating a larger color palette
// as decoders skip indexes that are out of range.
int padding = transparencyIndex >= paletteLength
? 1
: 0;
using LzwEncoder encoder = new(this.memoryAllocator, ColorNumerics.GetBitsNeededForColorDepth(paletteLength + padding));
encoder.Encode(region, stream);
}
}

30
src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
@ -22,9 +24,16 @@ public class GifFrameMetadata : IDeepCloneable
private GifFrameMetadata(GifFrameMetadata other)
{
this.ColorTableMode = other.ColorTableMode;
this.ColorTableLength = other.ColorTableLength;
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
if (other.LocalColorTable?.Length > 0)
{
this.LocalColorTable = other.LocalColorTable.Value.ToArray();
}
this.HasTransparency = other.HasTransparency;
this.TransparencyIndex = other.TransparencyIndex;
}
/// <summary>
@ -33,11 +42,22 @@ public class GifFrameMetadata : IDeepCloneable
public GifColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the length of the color table.
/// If not 0, then this field indicates the maximum number of colors to use when quantizing the
/// image frame.
/// Gets or sets the local color table, if any.
/// The underlying pixel format is represented by <see cref="Rgb24"/>.
/// </summary>
public ReadOnlyMemory<Color>? LocalColorTable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame has transparency
/// </summary>
public bool HasTransparency { get; set; }
/// <summary>
/// Gets or sets the transparency index.
/// When <see cref="HasTransparency"/> is set to <see langword="true"/> this value indicates the index within
/// the color palette at which the transparent color is located.
/// </summary>
public int ColorTableLength { get; set; }
public byte TransparencyIndex { get; set; }
/// <summary>
/// Gets or sets the frame delay for animated images.

20
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
@ -23,7 +25,12 @@ public class GifMetadata : IDeepCloneable
{
this.RepeatCount = other.RepeatCount;
this.ColorTableMode = other.ColorTableMode;
this.GlobalColorTableLength = other.GlobalColorTableLength;
this.BackgroundColorIndex = other.BackgroundColorIndex;
if (other.GlobalColorTable?.Length > 0)
{
this.GlobalColorTable = other.GlobalColorTable.Value.ToArray();
}
for (int i = 0; i < other.Comments.Count; i++)
{
@ -45,9 +52,16 @@ public class GifMetadata : IDeepCloneable
public GifColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the length of the global color table if present.
/// Gets or sets the global color table, if any.
/// The underlying pixel format is represented by <see cref="Rgb24"/>.
/// </summary>
public ReadOnlyMemory<Color>? GlobalColorTable { get; set; }
/// <summary>
/// Gets or sets the index at the <see cref="GlobalColorTable"/> for the background color.
/// The background color is the color used for those pixels on the screen that are not covered by an image.
/// </summary>
public int GlobalColorTableLength { get; set; }
public byte BackgroundColorIndex { get; set; }
/// <summary>
/// Gets or sets the collection of comments about the graphics, credits, descriptions or any

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

@ -186,7 +186,7 @@ internal sealed class LzwEncoder : IDisposable
/// </summary>
/// <param name="indexedPixels">The 2D buffer of indexed pixels.</param>
/// <param name="stream">The stream to write to.</param>
public void Encode(Buffer2D<byte> indexedPixels, Stream stream)
public void Encode(Buffer2DRegion<byte> indexedPixels, Stream stream)
{
// Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize);
@ -249,7 +249,7 @@ internal sealed class LzwEncoder : IDisposable
/// <param name="indexedPixels">The 2D buffer of indexed pixels.</param>
/// <param name="initialBits">The initial bits.</param>
/// <param name="stream">The stream to write to.</param>
private void Compress(Buffer2D<byte> indexedPixels, int initialBits, Stream stream)
private void Compress(Buffer2DRegion<byte> indexedPixels, int initialBits, Stream stream)
{
// Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = initialBits;

9
src/ImageSharp/Formats/Gif/MetadataExtensions.cs

@ -17,14 +17,16 @@ public static partial class MetadataExtensions
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifMetadata"/>.</returns>
public static GifMetadata GetGifMetadata(this ImageMetadata source) => source.GetFormatMetadata(GifFormat.Instance);
public static GifMetadata GetGifMetadata(this ImageMetadata source)
=> source.GetFormatMetadata(GifFormat.Instance);
/// <summary>
/// Gets the gif format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifFrameMetadata"/>.</returns>
public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(GifFormat.Instance);
public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source)
=> source.GetFormatMetadata(GifFormat.Instance);
/// <summary>
/// Gets the gif format specific metadata for the image frame.
@ -38,5 +40,6 @@ public static partial class MetadataExtensions
/// <returns>
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata) => source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
}

7
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs

@ -212,7 +212,12 @@ internal struct JpegBitReader
private int ReadStream()
{
int value = this.badData ? 0 : this.stream.ReadByte();
if (value == -1)
// We've encountered the end of the file stream which means there's no EOI marker or the marker has been read
// during decoding of the SOS marker.
// When reading individual bits 'badData' simply means we have hit a marker, When data is '0' and the stream is exhausted
// we know we have hit the EOI and completed decoding the scan buffer.
if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length))
{
// We've encountered the end of the file stream which means there's no EOI marker
// in the image or the SOS marker has the wrong dimensions set.

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

@ -35,9 +35,9 @@ internal static class PaethFilter
// row: a d
// The Paeth function predicts d to be whichever of a, b, or c is nearest to
// p = a + b - c.
if (Sse41.IsSupported && bytesPerPixel is 4)
if (Sse2.IsSupported && bytesPerPixel is 4)
{
DecodeSse41(scanline, previousScanline);
DecodeSse3(scanline, previousScanline);
}
else if (AdvSimd.Arm64.IsSupported && bytesPerPixel is 4)
{
@ -50,7 +50,7 @@ internal static class PaethFilter
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeSse41(Span<byte> scanline, Span<byte> previousScanline)
private static void DecodeSse3(Span<byte> scanline, Span<byte> previousScanline)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
@ -90,8 +90,8 @@ internal static class PaethFilter
Vector128<short> smallest = Sse2.Min(pc, Sse2.Min(pa, pb));
// Paeth breaks ties favoring a over b over c.
Vector128<byte> mask = Sse41.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = Sse41.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte());
Vector128<byte> mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte());
// Note `_epi8`: we need addition to wrap modulo 255.
d = Sse2.Add(d, nearest);
@ -143,8 +143,8 @@ internal static class PaethFilter
Vector128<short> smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb));
// Paeth breaks ties favoring a over b over c.
Vector128<byte> mask = BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte());
Vector128<byte> mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte());
d = AdvSimd.Add(d, nearest);
@ -157,27 +157,6 @@ internal static class PaethFilter
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> BlendVariable(Vector128<byte> a, Vector128<byte> b, Vector128<byte> c)
{
// Equivalent of Sse41.BlendVariable:
// Blend packed 8-bit integers from a and b using mask, and store the results in
// dst.
//
// FOR j := 0 to 15
// i := j*8
// IF mask[i+7]
// dst[i+7:i] := b[i+7:i]
// ELSE
// dst[i+7:i] := a[i+7:i]
// FI
// ENDFOR
//
// Use a signed shift right to create a mask with the sign bit.
Vector128<short> mask = AdvSimd.ShiftRightArithmetic(c.AsInt16(), 7);
return AdvSimd.BitwiseSelect(mask, b.AsInt16(), a.AsInt16()).AsByte();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, uint bytesPerPixel)
{

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

@ -9,15 +9,6 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary>
public class PngEncoder : QuantizingImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
/// </summary>
public PngEncoder() =>
// We set the quantizer to null here to allow the underlying encoder to create a
// quantizer with options appropriate to the encoding bit depth.
this.Quantizer = null;
/// <summary>
/// Gets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType" /> values.

3
src/ImageSharp/Formats/QuantizingImageEncoder.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
@ -14,7 +13,7 @@ public abstract class QuantizingImageEncoder : ImageEncoder
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
public IQuantizer Quantizer { get; init; } = KnownQuantizers.Octree;
public IQuantizer? Quantizer { get; init; }
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.

6
src/ImageSharp/Formats/Tiff/TiffEncoder.cs

@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Tiff;
@ -11,6 +12,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff;
/// </summary>
public class TiffEncoder : QuantizingImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoder"/> class.
/// </summary>
public TiffEncoder() => this.Quantizer = KnownQuantizers.Octree;
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>

10
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff;
@ -85,7 +86,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
{
this.memoryAllocator = memoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = options.PixelSamplingStrategy;
this.BitsPerPixel = options.BitsPerPixel;
this.HorizontalPredictor = options.HorizontalPredictor;
@ -157,6 +158,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
long ifdMarker = WriteHeader(writer, buffer);
Image<TPixel> metadataImage = image;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
cancellationToken.ThrowIfCancellationRequested();
@ -235,9 +237,13 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
if (image != null)
{
// Write the metadata for the root image
entriesCollector.ProcessMetadata(image, this.skipMetadata);
}
// Write the metadata for the frame
entriesCollector.ProcessMetadata(frame, this.skipMetadata);
entriesCollector.ProcessFrameInfo(frame, imageMetadata);
entriesCollector.ProcessImageFormat(this);
@ -320,7 +326,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
{
int sz = ExifWriter.WriteValue(entry, buffer, 0);
DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written");
writer.WritePadded(buffer.Slice(0, sz));
writer.WritePadded(buffer[..sz]);
}
else
{

66
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -6,6 +6,8 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Tiff;
@ -19,6 +21,9 @@ internal class TiffEncoderEntriesCollector
public void ProcessMetadata(Image image, bool skipMetadata)
=> new MetadataProcessor(this).Process(image, skipMetadata);
public void ProcessMetadata(ImageFrame frame, bool skipMetadata)
=> new MetadataProcessor(this).Process(frame, skipMetadata);
public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata)
=> new FrameInfoProcessor(this).Process(frame, imageMetadata);
@ -56,15 +61,29 @@ internal class TiffEncoderEntriesCollector
public void Process(Image image, bool skipMetadata)
{
ImageFrame rootFrame = image.Frames.RootFrame;
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile;
XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile;
this.ProcessProfiles(image.Metadata, skipMetadata);
this.ProcessProfiles(image.Metadata, skipMetadata, rootFrameExifProfile, rootFrameXmpProfile);
if (!skipMetadata)
{
this.ProcessMetadata(image.Metadata.ExifProfile ?? new ExifProfile());
}
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software))
{
this.Collector.Add(new ExifString(ExifTagValue.Software)
{
Value = SoftwareValue
});
}
}
public void Process(ImageFrame frame, bool skipMetadata)
{
this.ProcessProfiles(frame.Metadata, skipMetadata);
if (!skipMetadata)
{
this.ProcessMetadata(rootFrameExifProfile ?? new ExifProfile());
this.ProcessMetadata(frame.Metadata.ExifProfile ?? new ExifProfile());
}
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software))
@ -150,7 +169,23 @@ internal class TiffEncoderEntriesCollector
}
}
private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, ExifProfile exifProfile, XmpProfile xmpProfile)
private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata)
{
this.ProcessExifProfile(skipMetadata, imageMetadata.ExifProfile);
this.ProcessIptcProfile(skipMetadata, imageMetadata.IptcProfile, imageMetadata.ExifProfile);
this.ProcessIccProfile(imageMetadata.IccProfile, imageMetadata.ExifProfile);
this.ProcessXmpProfile(skipMetadata, imageMetadata.XmpProfile, imageMetadata.ExifProfile);
}
private void ProcessProfiles(ImageFrameMetadata frameMetadata, bool skipMetadata)
{
this.ProcessExifProfile(skipMetadata, frameMetadata.ExifProfile);
this.ProcessIptcProfile(skipMetadata, frameMetadata.IptcProfile, frameMetadata.ExifProfile);
this.ProcessIccProfile(frameMetadata.IccProfile, frameMetadata.ExifProfile);
this.ProcessXmpProfile(skipMetadata, frameMetadata.XmpProfile, frameMetadata.ExifProfile);
}
private void ProcessExifProfile(bool skipMetadata, ExifProfile exifProfile)
{
if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None))
{
@ -170,13 +205,16 @@ internal class TiffEncoderEntriesCollector
{
exifProfile?.RemoveValue(ExifTag.SubIFDOffset);
}
}
if (!skipMetadata && imageMetadata.IptcProfile != null)
private void ProcessIptcProfile(bool skipMetadata, IptcProfile iptcProfile, ExifProfile exifProfile)
{
if (!skipMetadata && iptcProfile != null)
{
imageMetadata.IptcProfile.UpdateData();
iptcProfile.UpdateData();
ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte)
{
Value = imageMetadata.IptcProfile.Data
Value = iptcProfile.Data
};
this.Collector.AddOrReplace(iptc);
@ -185,12 +223,15 @@ internal class TiffEncoderEntriesCollector
{
exifProfile?.RemoveValue(ExifTag.IPTC);
}
}
if (imageMetadata.IccProfile != null)
private void ProcessIccProfile(IccProfile iccProfile, ExifProfile exifProfile)
{
if (iccProfile != null)
{
ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined)
{
Value = imageMetadata.IccProfile.ToByteArray()
Value = iccProfile.ToByteArray()
};
this.Collector.AddOrReplace(icc);
@ -199,7 +240,10 @@ internal class TiffEncoderEntriesCollector
{
exifProfile?.RemoveValue(ExifTag.IccProfile);
}
}
private void ProcessXmpProfile(bool skipMetadata, XmpProfile xmpProfile, ExifProfile exifProfile)
{
if (!skipMetadata && xmpProfile != null)
{
ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte)

10
src/ImageSharp/ImageFrame{TPixel}.cs

@ -21,6 +21,16 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
{
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="size">The <see cref="Size"/> of the frame.</param>
internal ImageFrame(Configuration configuration, Size size)
: this(configuration, size.Width, size.Height, new ImageFrameMetadata())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary>

11
src/ImageSharp/Memory/Buffer2D{T}.cs

@ -173,13 +173,15 @@ public sealed class Buffer2D<T> : IDisposable
/// Swaps the contents of 'destination' with 'source' if the buffers are owned (1),
/// copies the contents of 'source' to 'destination' otherwise (2). Buffers should be of same size in case 2!
/// </summary>
/// <param name="destination">The destination buffer.</param>
/// <param name="source">The source buffer.</param>
/// <exception cref="InvalidMemoryOperationException">Attempt to copy/swap incompatible buffers.</exception>
internal static bool SwapOrCopyContent(Buffer2D<T> destination, Buffer2D<T> source)
{
bool swapped = false;
if (MemoryGroup<T>.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup))
{
(destination.FastMemoryGroup, source.FastMemoryGroup) =
(source.FastMemoryGroup, destination.FastMemoryGroup);
(destination.FastMemoryGroup, source.FastMemoryGroup) = (source.FastMemoryGroup, destination.FastMemoryGroup);
destination.FastMemoryGroup.RecreateViewAfterSwap();
source.FastMemoryGroup.RecreateViewAfterSwap();
swapped = true;
@ -201,7 +203,6 @@ public sealed class Buffer2D<T> : IDisposable
}
[MethodImpl(InliningOptions.ColdPath)]
private void ThrowYOutOfRangeException(int y) =>
throw new ArgumentOutOfRangeException(
$"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
private void ThrowYOutOfRangeException(int y)
=> throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
}

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

@ -4,6 +4,7 @@
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -34,17 +35,25 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
int levels = Math.Clamp(this.definition.Levels, 1, 255);
int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height));
using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size());
source.CopyTo(targetPixels);
RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, this.definition.Levels);
ParallelRowIterator.IterateRowIntervals(
RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, levels);
try
{
ParallelRowIterator.IterateRowIntervals(
this.Configuration,
this.SourceRectangle,
in operation);
}
catch (Exception ex)
{
throw new ImageProcessingException("The OilPaintProcessor failed. The most likely reason is that a pixel component was outside of its' allowed range.", ex);
}
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}
@ -105,18 +114,18 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
Span<Vector4> targetRowVector4Span = targetRowBuffer.Memory.Span;
Span<Vector4> targetRowAreaVector4Span = targetRowVector4Span.Slice(this.bounds.X, this.bounds.Width);
ref float binsRef = ref bins.GetReference();
ref int intensityBinRef = ref Unsafe.As<float, int>(ref binsRef);
ref float redBinRef = ref Unsafe.Add(ref binsRef, (uint)this.levels);
ref float blueBinRef = ref Unsafe.Add(ref redBinRef, (uint)this.levels);
ref float greenBinRef = ref Unsafe.Add(ref blueBinRef, (uint)this.levels);
Span<float> binsSpan = bins.GetSpan();
Span<int> intensityBinsSpan = MemoryMarshal.Cast<float, int>(binsSpan);
Span<float> redBinSpan = binsSpan[this.levels..];
Span<float> blueBinSpan = redBinSpan[this.levels..];
Span<float> greenBinSpan = blueBinSpan[this.levels..];
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRowPixelSpan = this.source.DangerousGetRowSpan(y);
Span<TPixel> sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(this.bounds.X, this.bounds.Width);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span, PixelConversionModifiers.Scale);
for (int x = this.bounds.X; x < this.bounds.Right; x++)
{
@ -140,7 +149,7 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
int offsetX = x + fxr;
offsetX = Numerics.Clamp(offsetX, 0, maxX);
Vector4 vector = sourceOffsetRow[offsetX].ToVector4();
Vector4 vector = sourceOffsetRow[offsetX].ToScaledVector4();
float sourceRed = vector.X;
float sourceBlue = vector.Z;
@ -148,21 +157,21 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (this.levels - 1));
Unsafe.Add(ref intensityBinRef, (uint)currentIntensity)++;
Unsafe.Add(ref redBinRef, (uint)currentIntensity) += sourceRed;
Unsafe.Add(ref blueBinRef, (uint)currentIntensity) += sourceBlue;
Unsafe.Add(ref greenBinRef, (uint)currentIntensity) += sourceGreen;
intensityBinsSpan[currentIntensity]++;
redBinSpan[currentIntensity] += sourceRed;
blueBinSpan[currentIntensity] += sourceBlue;
greenBinSpan[currentIntensity] += sourceGreen;
if (Unsafe.Add(ref intensityBinRef, (uint)currentIntensity) > maxIntensity)
if (intensityBinsSpan[currentIntensity] > maxIntensity)
{
maxIntensity = Unsafe.Add(ref intensityBinRef, (uint)currentIntensity);
maxIntensity = intensityBinsSpan[currentIntensity];
maxIndex = currentIntensity;
}
}
float red = MathF.Abs(Unsafe.Add(ref redBinRef, (uint)maxIndex) / maxIntensity);
float blue = MathF.Abs(Unsafe.Add(ref blueBinRef, (uint)maxIndex) / maxIntensity);
float green = MathF.Abs(Unsafe.Add(ref greenBinRef, (uint)maxIndex) / maxIntensity);
float red = redBinSpan[maxIndex] / maxIntensity;
float blue = blueBinSpan[maxIndex] / maxIntensity;
float green = greenBinSpan[maxIndex] / maxIntensity;
float alpha = sourceRowVector4Span[x].W;
targetRowVector4Span[x] = new Vector4(red, green, blue, alpha);
@ -171,7 +180,7 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
Span<TPixel> targetRowAreaPixelSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan, PixelConversionModifiers.Scale);
}
}
}

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

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
@ -14,13 +15,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <para>
/// This class is not threadsafe and should not be accessed in parallel.
/// This class is not thread safe and should not be accessed in parallel.
/// Doing so will result in non-idempotent results.
/// </para>
internal sealed class EuclideanPixelMap<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private Rgba32[] rgbaPalette;
private int transparentIndex;
/// <summary>
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls.
@ -34,26 +36,33 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param>
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette)
: this(configuration, palette, -1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param>
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette, int transparentIndex = -1)
{
this.configuration = configuration;
this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length];
this.cache = new ColorDistanceCache(configuration.MemoryAllocator);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette);
// If the provided transparentIndex is outside of the palette, silently ignore it.
this.transparentIndex = transparentIndex < this.Palette.Length ? transparentIndex : -1;
}
/// <summary>
/// Gets the color palette of this <see cref="EuclideanPixelMap{TPixel}"/>.
/// The palette memory is owned by the palette source that created it.
/// </summary>
public ReadOnlyMemory<TPixel> Palette
{
[MethodImpl(InliningOptions.ShortMethod)]
get;
[MethodImpl(InliningOptions.ShortMethod)]
private set;
}
public ReadOnlyMemory<TPixel> Palette { get; private set; }
/// <summary>
/// Returns the closest color in the palette and the index of that pixel.
@ -91,16 +100,33 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
this.cache.Clear();
}
/// <summary>
/// Allows setting the transparent index after construction. If the provided transparentIndex is outside of the palette, silently ignore it.
/// </summary>
/// <param name="index">An explicit index at which to match transparent pixels.</param>
public void SetTransparentIndex(int index) => this.transparentIndex = index < this.Palette.Length ? index : -1;
[MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
{
// Loop through the palette and find the nearest match.
int index = 0;
float leastDistance = float.MaxValue;
if (this.transparentIndex >= 0 && rgba == default)
{
// We have explicit instructions. No need to search.
index = this.transparentIndex;
DebugGuard.MustBeLessThan(index, this.Palette.Length, nameof(index));
this.cache.Add(rgba, (byte)index);
match = Unsafe.Add(ref paletteRef, (uint)index);
return index;
}
for (int i = 0; i < this.rgbaPalette.Length; i++)
{
Rgba32 candidate = this.rgbaPalette[i];
int distance = DistanceSquared(rgba, candidate);
float distance = DistanceSquared(rgba, candidate);
// If it's an exact match, exit the loop
if (distance == 0)
@ -130,12 +156,12 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// <param name="b">The second point.</param>
/// <returns>The distance squared.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int DistanceSquared(Rgba32 a, Rgba32 b)
private static float DistanceSquared(Rgba32 a, Rgba32 b)
{
int deltaR = a.R - b.R;
int deltaG = a.G - b.G;
int deltaB = a.B - b.B;
int deltaA = a.A - b.A;
float deltaR = a.R - b.R;
float deltaG = a.G - b.G;
float deltaB = a.B - b.B;
float deltaA = a.A - b.A;
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
}

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

@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
public class PaletteQuantizer : IQuantizer
{
private readonly ReadOnlyMemory<Color> colorPalette;
private readonly int transparentIndex;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
@ -27,12 +28,24 @@ public class PaletteQuantizer : IQuantizer
/// <param name="palette">The color palette.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options)
: this(palette, options, -1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The color palette.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
internal PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options, int transparentIndex)
{
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options));
this.colorPalette = palette;
this.Options = options;
this.transparentIndex = transparentIndex;
}
/// <inheritdoc />
@ -52,6 +65,6 @@ public class PaletteQuantizer : IQuantizer
// Always use the palette length over options since the palette cannot be reduced.
TPixel[] palette = new TPixel[this.colorPalette.Length];
Color.ToPixel(this.colorPalette.Span, palette.AsSpan());
return new PaletteQuantizer<TPixel>(configuration, options, palette);
return new PaletteQuantizer<TPixel>(configuration, options, palette, this.transparentIndex);
}
}

17
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs

@ -25,18 +25,23 @@ internal readonly struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="palette">The palette to use.</param>
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory<TPixel> palette)
public PaletteQuantizer(
Configuration configuration,
QuantizerOptions options,
ReadOnlyMemory<TPixel> palette,
int transparentIndex)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
this.Configuration = configuration;
this.Options = options;
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette, transparentIndex);
}
/// <inheritdoc/>
@ -59,6 +64,12 @@ internal readonly struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
{
}
/// <summary>
/// Allows setting the transparent index after construction.
/// </summary>
/// <param name="index">An explicit index at which to match transparent pixels.</param>
public void SetTransparentIndex(int index) => this.pixelMap.SetTransparentIndex(index);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)

8
src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs

@ -25,8 +25,8 @@ public class QuantizerOptions
/// </summary>
public float DitherScale
{
get { return this.ditherScale; }
set { this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); }
get => this.ditherScale;
set => this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale);
}
/// <summary>
@ -35,7 +35,7 @@ public class QuantizerOptions
/// </summary>
public int MaxColors
{
get { return this.maxColors; }
set { this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors); }
get => this.maxColors;
set => this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
}
}

19
tests/ImageSharp.Benchmarks/Processing/OilPaint.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Benchmarks.Processing;
[Config(typeof(Config.MultiFramework))]
public class OilPaint
{
[Benchmark]
public void DoOilPaint()
{
using Image<RgbaVector> image = new Image<RgbaVector>(1920, 1200, new(127, 191, 255));
image.Mutate(ctx => ctx.OilPaint());
}
}

39
tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

@ -46,17 +46,20 @@ public class BmpEncoderTests
{ Bit32Rgb, BmpBitsPerPixel.Pixel32 }
};
[Fact]
public void BmpEncoderDefaultInstanceHasQuantizer() => Assert.NotNull(BmpEncoder.Quantizer);
[Theory]
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
input.Save(memStream, BmpEncoder);
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
@ -67,13 +70,13 @@ public class BmpEncoderTests
[MemberData(nameof(BmpBitsPerPixelFiles))]
public void Encode_PreserveBitsPerPixel(string imagePath, BmpBitsPerPixel bmpBitsPerPixel)
{
var testFile = TestFile.Create(imagePath);
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
input.Save(memStream, BmpEncoder);
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
BmpMetadata meta = output.Metadata.GetBmpMetadata();
Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel);
@ -196,8 +199,8 @@ public class BmpEncoderTests
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel };
using var memoryStream = new MemoryStream();
BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel };
using MemoryStream memoryStream = new();
using Image<TPixel> input = provider.GetImage(BmpDecoder.Instance);
// act
@ -205,7 +208,7 @@ public class BmpEncoderTests
memoryStream.Position = 0;
// assert
using var actual = Image.Load<TPixel>(memoryStream);
using Image<TPixel> actual = Image.Load<TPixel>(memoryStream);
ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual);
Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image");
}
@ -218,8 +221,8 @@ public class BmpEncoderTests
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel };
using var memoryStream = new MemoryStream();
BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel };
using MemoryStream memoryStream = new();
using Image<TPixel> input = provider.GetImage(BmpDecoder.Instance);
// act
@ -227,7 +230,7 @@ public class BmpEncoderTests
memoryStream.Position = 0;
// assert
using var actual = Image.Load<TPixel>(memoryStream);
using Image<TPixel> actual = Image.Load<TPixel>(memoryStream);
ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual);
Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image");
}
@ -266,7 +269,7 @@ public class BmpEncoderTests
}
using Image<TPixel> image = provider.GetImage();
var encoder = new BmpEncoder
BmpEncoder encoder = new()
{
BitsPerPixel = BmpBitsPerPixel.Pixel8,
Quantizer = new WuQuantizer()
@ -298,7 +301,7 @@ public class BmpEncoderTests
}
using Image<TPixel> image = provider.GetImage();
var encoder = new BmpEncoder
BmpEncoder encoder = new()
{
BitsPerPixel = BmpBitsPerPixel.Pixel8,
Quantizer = new OctreeQuantizer()
@ -333,11 +336,11 @@ public class BmpEncoderTests
ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile;
byte[] expectedProfileBytes = expectedProfile.ToByteArray();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
input.Save(memStream, new BmpEncoder());
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile;
byte[] actualProfileBytes = actualProfile.ToByteArray();
@ -353,7 +356,7 @@ public class BmpEncoderTests
Exception exception = Record.Exception(() =>
{
using Image image = new Image<Rgba32>(width, height);
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
image.Save(memStream, BmpEncoder);
});
@ -411,7 +414,7 @@ public class BmpEncoderTests
image.Mutate(c => c.MakeOpaque());
}
var encoder = new BmpEncoder
BmpEncoder encoder = new()
{
BitsPerPixel = bitsPerPixel,
SupportTransparency = supportTransparency,

14
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -34,6 +34,20 @@ public class GifDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
[Theory]
[WithFile(TestImages.Gif.Issues.Issue2450_A, PixelTypes.Rgba32)]
[WithFile(TestImages.Gif.Issues.Issue2450_B, PixelTypes.Rgba32)]
public void Decode_Issue2450<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
// Images have many frames, only compare a selection of them.
static bool Predicate(int i, int _) => i % 8 == 0;
using Image<TPixel> image = provider.GetImage();
image.DebugSaveMultiFrame(provider, predicate: Predicate);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact, predicate: Predicate);
}
[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void GifDecoder_Decode_Resize<TPixel>(TestImageProvider<TPixel> provider)

44
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -33,9 +33,12 @@ public class GifEncoderTests
}
}
[Fact]
public void GifEncoderDefaultInstanceHasNullQuantizer() => Assert.Null(new GifEncoder().Quantizer);
[Theory]
[WithTestPatternImages(100, 100, TestPixelTypes, false)]
[WithTestPatternImages(100, 100, TestPixelTypes, false)]
[WithTestPatternImages(100, 100, TestPixelTypes, true)]
public void EncodeGeneratedPatterns<TPixel>(TestImageProvider<TPixel> provider, bool limitAllocationBuffer)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -171,10 +174,21 @@ public class GifEncoderTests
GifMetadata metaData = image.Metadata.GetGifMetadata();
GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata();
GifColorTableMode colorMode = metaData.ColorTableMode;
int maxColors;
if (colorMode == GifColorTableMode.Global)
{
maxColors = metaData.GlobalColorTable.Value.Length;
}
else
{
maxColors = frameMetadata.LocalColorTable.Value.Length;
}
GifEncoder encoder = new()
{
ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength })
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = maxColors })
};
image.Save(outStream, encoder);
@ -187,15 +201,31 @@ public class GifEncoderTests
Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode);
// Gifiddle and Cyotek GifInfo say this image has 64 colors.
Assert.Equal(64, frameMetadata.ColorTableLength);
colorMode = cloneMetadata.ColorTableMode;
if (colorMode == GifColorTableMode.Global)
{
maxColors = metaData.GlobalColorTable.Value.Length;
}
else
{
maxColors = frameMetadata.LocalColorTable.Value.Length;
}
Assert.Equal(64, maxColors);
for (int i = 0; i < image.Frames.Count; i++)
{
GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata iMeta = image.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata cMeta = clone.Frames[i].Metadata.GetGifMetadata();
if (iMeta.ColorTableMode == GifColorTableMode.Local)
{
Assert.Equal(iMeta.LocalColorTable.Value.Length, cMeta.LocalColorTable.Value.Length);
}
Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength);
Assert.Equal(ifm.FrameDelay, cifm.FrameDelay);
Assert.Equal(iMeta.FrameDelay, cMeta.FrameDelay);
Assert.Equal(iMeta.HasTransparency, cMeta.HasTransparency);
Assert.Equal(iMeta.TransparencyIndex, cMeta.TransparencyIndex);
}
image.Dispose();

11
tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs

@ -11,21 +11,22 @@ public class GifFrameMetadataTests
[Fact]
public void CloneIsDeep()
{
var meta = new GifFrameMetadata
GifFrameMetadata meta = new()
{
FrameDelay = 1,
DisposalMethod = GifDisposalMethod.RestoreToBackground,
ColorTableLength = 2
LocalColorTable = new[] { Color.Black, Color.White }
};
var clone = (GifFrameMetadata)meta.DeepClone();
GifFrameMetadata clone = (GifFrameMetadata)meta.DeepClone();
clone.FrameDelay = 2;
clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious;
clone.ColorTableLength = 1;
clone.LocalColorTable = new[] { Color.Black };
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod));
Assert.False(meta.ColorTableLength.Equals(clone.ColorTableLength));
Assert.False(meta.LocalColorTable.Value.Length == clone.LocalColorTable.Value.Length);
Assert.Equal(1, clone.LocalColorTable.Value.Length);
}
}

15
tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using Microsoft.CodeAnalysis;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
@ -35,7 +34,7 @@ public class GifMetadataTests
{
RepeatCount = 1,
ColorTableMode = GifColorTableMode.Global,
GlobalColorTableLength = 2,
GlobalColorTable = new[] { Color.Black, Color.White },
Comments = new List<string> { "Foo" }
};
@ -43,11 +42,12 @@ public class GifMetadataTests
clone.RepeatCount = 2;
clone.ColorTableMode = GifColorTableMode.Local;
clone.GlobalColorTableLength = 1;
clone.GlobalColorTable = new[] { Color.Black };
Assert.False(meta.RepeatCount.Equals(clone.RepeatCount));
Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode));
Assert.False(meta.GlobalColorTableLength.Equals(clone.GlobalColorTableLength));
Assert.False(meta.GlobalColorTable.Value.Length == clone.GlobalColorTable.Value.Length);
Assert.Equal(1, clone.GlobalColorTable.Value.Length);
Assert.False(meta.Comments.Equals(clone.Comments));
Assert.True(meta.Comments.SequenceEqual(clone.Comments));
}
@ -205,7 +205,12 @@ public class GifMetadataTests
GifFrameMetadata gifFrameMetadata = imageInfo.FrameMetadataCollection[imageInfo.FrameMetadataCollection.Count - 1].GetGifMetadata();
Assert.Equal(colorTableMode, gifFrameMetadata.ColorTableMode);
Assert.Equal(globalColorTableLength, gifFrameMetadata.ColorTableLength);
if (colorTableMode == GifColorTableMode.Global)
{
Assert.Equal(globalColorTableLength, gifMetadata.GlobalColorTable.Value.Length);
}
Assert.Equal(frameDelay, gifFrameMetadata.FrameDelay);
Assert.Equal(disposalMethod, gifFrameMetadata.DisposalMethod);
}

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

@ -325,4 +325,21 @@ public partial class JpegDecoderTests
image.DebugSave(provider);
image.CompareToOriginal(provider);
}
[Theory]
[WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)]
public void DecodeHang<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.IsWindows &&
TestEnvironment.RunsOnCI)
{
// Windows CI runs consistently fail with OOM.
return;
}
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
Assert.Equal(65503, image.Width);
Assert.Equal(65503, image.Height);
}
}

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

@ -99,6 +99,9 @@ public partial class PngEncoderTests
{ TestImages.Png.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio }
};
[Fact]
public void PngEncoderDefaultInstanceHasNullQuantizer() => Assert.Null(PngEncoder.Quantizer);
[Theory]
[WithFile(TestImages.Png.Palette8Bpp, nameof(PngColorTypes), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(PngColorTypes), 48, 24, PixelTypes.Rgba32)]
@ -595,7 +598,7 @@ public partial class PngEncoderTests
string pngBitDepthInfo = appendPngBitDepth ? bitDepth.ToString() : string.Empty;
string pngInterlaceModeInfo = interlaceMode != PngInterlaceMode.None ? $"_{interlaceMode}" : string.Empty;
string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}{pngBitDepthInfo}{pngInterlaceModeInfo}";
string debugInfo = pngColorTypeInfo + pngFilterMethodInfo + compressionLevelInfo + paletteSizeInfo + pngBitDepthInfo + pngInterlaceModeInfo;
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType);

76
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -11,6 +11,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff;
[Trait("Format", "Tiff")]
public class TiffEncoderTests : TiffEncoderBaseTester
{
[Fact]
public void TiffEncoderDefaultInstanceHasQuantizer() => Assert.NotNull(new TiffEncoder().Quantizer);
[Theory]
[InlineData(null, TiffBitsPerPixel.Bit24)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)]
@ -28,18 +31,18 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffBitsPerPixel expectedBitsPerPixel)
{
// arrange
var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation };
TiffEncoder tiffEncoder = new() { PhotometricInterpretation = photometricInterpretation };
using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
? new Image<L16>(10, 10)
: new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
Assert.Equal(TiffCompression.None, frameMetaData.Compression);
@ -54,16 +57,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel)
{
// arrange
var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel };
TiffEncoder tiffEncoder = new()
{ BitsPerPixel = bitsPerPixel };
using Image input = new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(bitsPerPixel, frameMetaData.BitsPerPixel);
@ -81,16 +85,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPixel bitsPerPixel)
{
// arrange
var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel };
TiffEncoder tiffEncoder = new()
{ BitsPerPixel = bitsPerPixel };
using Image input = new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel);
@ -103,16 +108,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_WithInvalidCompressionAndPixelTypeCombination_DefaultsToRgb(TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression)
{
// arrange
var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression };
TiffEncoder tiffEncoder = new()
{ PhotometricInterpretation = photometricInterpretation, Compression = compression };
using Image input = new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel);
@ -149,18 +155,19 @@ public class TiffEncoderTests : TiffEncoderBaseTester
TiffCompression expectedCompression)
{
// arrange
var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression };
TiffEncoder tiffEncoder = new()
{ PhotometricInterpretation = photometricInterpretation, Compression = compression };
using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
? new Image<L16>(10, 10)
: new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata rootFrameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, rootFrameMetaData.BitsPerPixel);
Assert.Equal(expectedCompression, rootFrameMetaData.Compression);
@ -178,16 +185,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
var tiffEncoder = new TiffEncoder();
TiffEncoder tiffEncoder = new();
using Image<TPixel> input = provider.GetImage();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
}
@ -196,17 +203,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void TiffEncoder_PreservesBitsPerPixel_WhenInputIsL8()
{
// arrange
var tiffEncoder = new TiffEncoder();
TiffEncoder tiffEncoder = new();
using Image input = new Image<L8>(10, 10);
using var memStream = new MemoryStream();
TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8;
using MemoryStream memStream = new();
const TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8;
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
}
@ -220,16 +227,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
var tiffEncoder = new TiffEncoder();
TiffEncoder tiffEncoder = new();
using Image<TPixel> input = provider.GetImage();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
Assert.Equal(expectedCompression, output.Frames.RootFrame.Metadata.GetTiffMetadata().Compression);
}
@ -242,16 +249,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
var tiffEncoder = new TiffEncoder();
TiffEncoder tiffEncoder = new();
using Image<TPixel> input = provider.GetImage();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetadata = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedPredictor, frameMetadata.Predictor);
}
@ -261,8 +268,8 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void TiffEncoder_WritesIfdOffsetAtWordBoundary()
{
// arrange
var tiffEncoder = new TiffEncoder();
using var memStream = new MemoryStream();
TiffEncoder tiffEncoder = new();
using MemoryStream memStream = new();
using Image<Rgba32> image = new(1, 1);
byte[] expectedIfdOffsetBytes = { 12, 0 };
@ -286,16 +293,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
var encoder = new TiffEncoder() { Compression = compression, BitsPerPixel = TiffBitsPerPixel.Bit1 };
TiffEncoder encoder = new() { Compression = compression, BitsPerPixel = TiffBitsPerPixel.Bit1 };
using Image<TPixel> input = provider.GetImage();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, encoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit1, frameMetaData.BitsPerPixel);
Assert.Equal(expectedCompression, frameMetaData.Compression);
@ -545,7 +552,8 @@ public class TiffEncoderTests : TiffEncoderBaseTester
provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200);
using Image<TPixel> image = provider.GetImage();
var encoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation };
TiffEncoder encoder = new()
{ PhotometricInterpretation = photometricInterpretation };
image.DebugSave(provider, encoder);
}
}

91
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
@ -318,4 +319,94 @@ public class TiffMetadataTests
Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value);
Assert.Equal(exifProfileInput.Values.Count, encodedImageExifProfile.Values.Count);
}
[Theory]
[WithFile(SampleMetadata, PixelTypes.Rgba32)]
public void Encode_PreservesMetadata_IptcAndIcc<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
// Load Tiff image
DecoderOptions options = new() { SkipMetadata = false };
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance, options);
ImageMetadata inputMetaData = image.Metadata;
ImageFrame<TPixel> rootFrameInput = image.Frames.RootFrame;
IptcProfile iptcProfile = new();
iptcProfile.SetValue(IptcTag.Name, "Test name");
rootFrameInput.Metadata.IptcProfile = iptcProfile;
IccProfileHeader iccProfileHeader = new() { Class = IccProfileClass.ColorSpace };
IccProfile iccProfile = new();
rootFrameInput.Metadata.IccProfile = iccProfile;
TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata();
XmpProfile xmpProfileInput = rootFrameInput.Metadata.XmpProfile;
ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile;
IptcProfile iptcProfileInput = rootFrameInput.Metadata.IptcProfile;
IccProfile iccProfileInput = rootFrameInput.Metadata.IccProfile;
Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression);
Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaInput.BitsPerPixel);
// Save to Tiff
TiffEncoder tiffEncoder = new() { PhotometricInterpretation = TiffPhotometricInterpretation.Rgb };
using MemoryStream ms = new();
image.Save(ms, tiffEncoder);
// Assert
ms.Position = 0;
using Image<Rgba32> encodedImage = Image.Load<Rgba32>(ms);
ImageMetadata encodedImageMetaData = encodedImage.Metadata;
ImageFrame<Rgba32> rootFrameEncodedImage = encodedImage.Frames.RootFrame;
TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata();
ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile;
XmpProfile encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile;
IptcProfile encodedImageIptcProfile = rootFrameEncodedImage.Metadata.IptcProfile;
IccProfile encodedImageIccProfile = rootFrameEncodedImage.Metadata.IccProfile;
Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel);
Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression);
Assert.Equal(inputMetaData.HorizontalResolution, encodedImageMetaData.HorizontalResolution);
Assert.Equal(inputMetaData.VerticalResolution, encodedImageMetaData.VerticalResolution);
Assert.Equal(inputMetaData.ResolutionUnits, encodedImageMetaData.ResolutionUnits);
Assert.Equal(rootFrameInput.Width, rootFrameEncodedImage.Width);
Assert.Equal(rootFrameInput.Height, rootFrameEncodedImage.Height);
PixelResolutionUnit resolutionUnitInput = UnitConverter.ExifProfileToResolutionUnit(exifProfileInput);
PixelResolutionUnit resolutionUnitEncoded = UnitConverter.ExifProfileToResolutionUnit(encodedImageExifProfile);
Assert.Equal(resolutionUnitInput, resolutionUnitEncoded);
Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble());
Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble());
Assert.NotNull(xmpProfileInput);
Assert.NotNull(encodedImageXmpProfile);
Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data);
Assert.NotNull(iptcProfileInput);
Assert.NotNull(encodedImageIptcProfile);
Assert.Equal(iptcProfileInput.Data, encodedImageIptcProfile.Data);
Assert.Equal(iptcProfileInput.GetValues(IptcTag.Name)[0].Value, encodedImageIptcProfile.GetValues(IptcTag.Name)[0].Value);
Assert.NotNull(iccProfileInput);
Assert.NotNull(encodedImageIccProfile);
Assert.Equal(iccProfileInput.Entries.Length, encodedImageIccProfile.Entries.Length);
Assert.Equal(iccProfileInput.Header.Class, encodedImageIccProfile.Header.Class);
Assert.Equal(exifProfileInput.GetValue(ExifTag.Software).Value, encodedImageExifProfile.GetValue(ExifTag.Software).Value);
Assert.Equal(exifProfileInput.GetValue(ExifTag.ImageDescription).Value, encodedImageExifProfile.GetValue(ExifTag.ImageDescription).Value);
Assert.Equal(exifProfileInput.GetValue(ExifTag.Make).Value, encodedImageExifProfile.GetValue(ExifTag.Make).Value);
Assert.Equal(exifProfileInput.GetValue(ExifTag.Copyright).Value, encodedImageExifProfile.GetValue(ExifTag.Copyright).Value);
Assert.Equal(exifProfileInput.GetValue(ExifTag.Artist).Value, encodedImageExifProfile.GetValue(ExifTag.Artist).Value);
Assert.Equal(exifProfileInput.GetValue(ExifTag.Orientation).Value, encodedImageExifProfile.GetValue(ExifTag.Orientation).Value);
Assert.Equal(exifProfileInput.GetValue(ExifTag.Model).Value, encodedImageExifProfile.GetValue(ExifTag.Model).Value);
Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value);
// Adding the IPTC and ICC profiles dynamically increments the number of values in the original EXIF profile by 2
Assert.Equal(exifProfileInput.Values.Count + 2, encodedImageExifProfile.Values.Count);
}
}

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

@ -2,6 +2,8 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using Castle.Core.Configuration;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -406,6 +408,43 @@ public class ParallelRowIteratorTests
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
}
[Fact]
public void CanIterateWithoutIntOverflow()
{
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(Configuration.Default);
const int max = 100_000;
Rectangle rect = new(0, 0, max, max);
int intervalMaxY = 0;
void RowAction(RowInterval rows, Span<Rgba32> memory) => intervalMaxY = Math.Max(rows.Max, intervalMaxY);
TestRowOperation operation = new();
TestRowIntervalOperation<Rgba32> intervalOperation = new(RowAction);
ParallelRowIterator.IterateRows(Configuration.Default, rect, in operation);
Assert.Equal(max - 1, operation.MaxY.Value);
ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in intervalOperation);
Assert.Equal(max, intervalMaxY);
}
private readonly struct TestRowOperation : IRowOperation
{
public TestRowOperation()
{
}
public StrongBox<int> MaxY { get; } = new StrongBox<int>();
public void Invoke(int y)
{
lock (this.MaxY)
{
this.MaxY.Value = Math.Max(y, this.MaxY.Value);
}
}
}
private readonly struct TestRowIntervalOperation : IRowIntervalOperation
{
private readonly Action<RowInterval> action;

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

@ -279,6 +279,7 @@ public abstract partial class ImageFrameCollectionTests
{
using Image source = provider.GetImage();
using Image<TPixel> dest = new(source.Configuration, source.Width, source.Height);
// Giphy.gif has 5 frames
ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 0);
ImportFrameAs<Argb32>(source.Frames, dest.Frames, 1);
@ -289,7 +290,7 @@ public abstract partial class ImageFrameCollectionTests
// Drop the original empty root frame:
dest.Frames.RemoveFrame(0);
dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif");
dest.DebugSave(provider, extension: "gif", appendSourceFileOrDescription: false);
dest.CompareToOriginal(provider);
for (int i = 0; i < 5; i++)
@ -314,7 +315,11 @@ public abstract partial class ImageFrameCollectionTests
Assert.Equal(aData.DisposalMethod, bData.DisposalMethod);
Assert.Equal(aData.FrameDelay, bData.FrameDelay);
Assert.Equal(aData.ColorTableLength, bData.ColorTableLength);
if (aData.ColorTableMode == GifColorTableMode.Local && bData.ColorTableMode == GifColorTableMode.Local)
{
Assert.Equal(aData.LocalColorTable.Value.Length, bData.LocalColorTable.Value.Length);
}
}
}
}

19
tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs

@ -4,6 +4,7 @@
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile;
using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag;
@ -22,17 +23,17 @@ public class ImageFrameMetadataTests
const int colorTableLength = 128;
const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground;
var metaData = new ImageFrameMetadata();
ImageFrameMetadata metaData = new();
GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata();
gifFrameMetadata.FrameDelay = frameDelay;
gifFrameMetadata.ColorTableLength = colorTableLength;
gifFrameMetadata.LocalColorTable = Enumerable.Repeat(Color.HotPink, colorTableLength).ToArray();
gifFrameMetadata.DisposalMethod = disposalMethod;
var clone = new ImageFrameMetadata(metaData);
ImageFrameMetadata clone = new(metaData);
GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata();
Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay);
Assert.Equal(colorTableLength, cloneGifFrameMetadata.ColorTableLength);
Assert.Equal(colorTableLength, cloneGifFrameMetadata.LocalColorTable.Value.Length);
Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod);
}
@ -40,19 +41,19 @@ public class ImageFrameMetadataTests
public void CloneIsDeep()
{
// arrange
var exifProfile = new ExifProfile();
ExifProfile exifProfile = new();
exifProfile.SetValue(ExifTag.Software, "UnitTest");
exifProfile.SetValue(ExifTag.Artist, "UnitTest");
var xmpProfile = new XmpProfile(new byte[0]);
var iccProfile = new IccProfile()
XmpProfile xmpProfile = new(Array.Empty<byte>());
IccProfile iccProfile = new()
{
Header = new IccProfileHeader()
{
CmmType = "Unittest"
}
};
var iptcProfile = new ImageSharp.Metadata.Profiles.Iptc.IptcProfile();
var metaData = new ImageFrameMetadata()
IptcProfile iptcProfile = new();
ImageFrameMetadata metaData = new()
{
XmpProfile = xmpProfile,
ExifProfile = exifProfile,

13
tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs

@ -27,8 +27,7 @@ public class OilPaintTest
[WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
public void FullImage<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.RunValidatingProcessorTest(
=> provider.RunValidatingProcessorTest(
x =>
{
x.OilPaint(levels, brushSize);
@ -36,17 +35,21 @@ public class OilPaintTest
},
ImageComparer.TolerantPercentage(0.01F),
appendPixelTypeToFileName: false);
}
[Theory]
[WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(OilPaintValues), 100, 100, PixelTypes.Rgba32)]
public void InBox<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.RunRectangleConstrainedValidatingProcessorTest(
=> provider.RunRectangleConstrainedValidatingProcessorTest(
(x, rect) => x.OilPaint(levels, brushSize, rect),
$"{levels}-{brushSize}",
ImageComparer.TolerantPercentage(0.01F));
[Fact]
public void Issue2518_PixelComponentOutsideOfRange_ThrowsImageProcessingException()
{
using Image<RgbaVector> image = new(10, 10, new RgbaVector(1, 1, 100));
Assert.Throws<ImageProcessingException>(() => image.Mutate(ctx => ctx.OilPaint()));
}
}

4
tests/ImageSharp.Tests/TestImages.cs

@ -291,6 +291,7 @@ public static class TestImages
public const string Issue2334_NotEnoughBytesA = "Jpg/issues/issue-2334-a.jpg";
public const string Issue2334_NotEnoughBytesB = "Jpg/issues/issue-2334-b.jpg";
public const string Issue2478_JFXX = "Jpg/issues/issue-2478-jfxx.jpg";
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";
public static class Fuzz
{
@ -491,6 +492,9 @@ public static class TestImages
public const string Issue2288_B = "Gif/issues/issue_2288_2.gif";
public const string Issue2288_C = "Gif/issues/issue_2288_3.gif";
public const string Issue2288_D = "Gif/issues/issue_2288_4.gif";
public const string Issue2450_A = "Gif/issues/issue_2450.gif";
public const string Issue2450_B = "Gif/issues/issue_2450_2.gif";
public const string Issue2198 = "Gif/issues/issue_2198.gif";
}
public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 };

47
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs

@ -46,19 +46,38 @@ public static class ImageComparerExtensions
public static IEnumerable<ImageSimilarityReport<TPixelA, TPixelB>> CompareImages<TPixelA, TPixelB>(
this ImageComparer comparer,
Image<TPixelA> expected,
Image<TPixelB> actual)
Image<TPixelB> actual,
Func<int, int, bool> predicate = null)
where TPixelA : unmanaged, IPixel<TPixelA>
where TPixelB : unmanaged, IPixel<TPixelB>
{
var result = new List<ImageSimilarityReport<TPixelA, TPixelB>>();
List<ImageSimilarityReport<TPixelA, TPixelB>> result = new();
if (expected.Frames.Count != actual.Frames.Count)
int expectedFrameCount = actual.Frames.Count;
if (predicate != null)
{
expectedFrameCount = 0;
for (int i = 0; i < actual.Frames.Count; i++)
{
if (predicate(i, actual.Frames.Count))
{
expectedFrameCount++;
}
}
}
if (expected.Frames.Count != expectedFrameCount)
{
throw new Exception("Frame count does not match!");
throw new ImagesSimilarityException("Frame count does not match!");
}
for (int i = 0; i < expected.Frames.Count; i++)
{
if (predicate != null && !predicate(i, expected.Frames.Count))
{
continue;
}
ImageSimilarityReport<TPixelA, TPixelB> report = comparer.CompareImagesOrFrames(i, expected.Frames[i], actual.Frames[i]);
if (!report.IsEmpty)
{
@ -72,7 +91,8 @@ public static class ImageComparerExtensions
public static void VerifySimilarity<TPixelA, TPixelB>(
this ImageComparer comparer,
Image<TPixelA> expected,
Image<TPixelB> actual)
Image<TPixelB> actual,
Func<int, int, bool> predicate = null)
where TPixelA : unmanaged, IPixel<TPixelA>
where TPixelB : unmanaged, IPixel<TPixelB>
{
@ -81,12 +101,25 @@ public static class ImageComparerExtensions
throw new ImageDimensionsMismatchException(expected.Size, actual.Size);
}
if (expected.Frames.Count != actual.Frames.Count)
int expectedFrameCount = actual.Frames.Count;
if (predicate != null)
{
expectedFrameCount = 0;
for (int i = 0; i < actual.Frames.Count; i++)
{
if (predicate(i, actual.Frames.Count))
{
expectedFrameCount++;
}
}
}
if (expected.Frames.Count != expectedFrameCount)
{
throw new ImagesSimilarityException("Image frame count does not match!");
}
IEnumerable<ImageSimilarityReport> reports = comparer.CompareImages(expected, actual);
IEnumerable<ImageSimilarityReport> reports = comparer.CompareImages(expected, actual, predicate);
if (reports.Any())
{
throw new ImageDifferenceIsOverThresholdException(reports);

50
tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs

@ -184,7 +184,8 @@ public class ImagingTestCaseUtility
string extension = null,
object testOutputDetails = null,
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
bool appendSourceFileOrDescription = true,
Func<int, int, bool> predicate = null)
{
string baseDir = this.GetTestOutputFileName(string.Empty, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription);
@ -195,8 +196,12 @@ public class ImagingTestCaseUtility
for (int i = 0; i < frameCount; i++)
{
string filePath = $"{baseDir}/{i:D2}.{extension}";
yield return filePath;
if (predicate != null && !predicate(i, frameCount))
{
continue;
}
yield return $"{baseDir}/{i:D2}.{extension}";
}
}
@ -205,27 +210,35 @@ public class ImagingTestCaseUtility
string extension = "png",
IImageEncoder encoder = null,
object testOutputDetails = null,
bool appendPixelTypeToFileName = true)
bool appendPixelTypeToFileName = true,
Func<int, int, bool> predicate = null)
where TPixel : unmanaged, IPixel<TPixel>
{
encoder = encoder ?? TestEnvironment.GetReferenceEncoder($"foo.{extension}");
encoder ??= TestEnvironment.GetReferenceEncoder($"foo.{extension}");
string[] files = this.GetTestOutputFileNamesMultiFrame(
image.Frames.Count,
extension,
testOutputDetails,
appendPixelTypeToFileName).ToArray();
appendPixelTypeToFileName,
predicate: predicate).ToArray();
for (int i = 0; i < image.Frames.Count; i++)
{
using (Image<TPixel> frameImage = image.Frames.CloneFrame(i))
if (predicate != null && !predicate(i, image.Frames.Count))
{
string filePath = files[i];
using (FileStream stream = File.OpenWrite(filePath))
{
frameImage.Save(stream, encoder);
}
continue;
}
if (i >= files.Length)
{
break;
}
using Image<TPixel> frameImage = image.Frames.CloneFrame(i);
string filePath = files[i];
using FileStream stream = File.OpenWrite(filePath);
frameImage.Save(stream, encoder);
}
return files;
@ -236,20 +249,17 @@ public class ImagingTestCaseUtility
object testOutputDetails,
bool appendPixelTypeToFileName,
bool appendSourceFileOrDescription)
{
return TestEnvironment.GetReferenceOutputFileName(
=> TestEnvironment.GetReferenceOutputFileName(
this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription));
}
public string[] GetReferenceOutputFileNamesMultiFrame(
int frameCount,
string extension,
object testOutputDetails,
bool appendPixelTypeToFileName = true)
{
return this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails)
.Select(TestEnvironment.GetReferenceOutputFileName).ToArray();
}
bool appendPixelTypeToFileName = true,
Func<int, int, bool> predicate = null)
=> this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails, appendPixelTypeToFileName, predicate: predicate)
.Select(TestEnvironment.GetReferenceOutputFileName).ToArray();
internal void Init(string typeName, string methodName, string outputSubfolderName)
{

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

@ -67,10 +67,10 @@ public static class TestImageExtensions
provider.Utility.SaveTestOutputFile(
image,
extension,
encoder: encoder,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName,
appendSourceFileOrDescription: appendSourceFileOrDescription,
encoder: encoder);
appendSourceFileOrDescription: appendSourceFileOrDescription);
return image;
}
@ -107,7 +107,8 @@ public static class TestImageExtensions
ITestImageProvider provider,
object testOutputDetails = null,
string extension = "png",
bool appendPixelTypeToFileName = true)
bool appendPixelTypeToFileName = true,
Func<int, int, bool> predicate = null)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.RunsWithCodeCoverage)
@ -119,7 +120,8 @@ public static class TestImageExtensions
image,
extension,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName);
appendPixelTypeToFileName: appendPixelTypeToFileName,
predicate: predicate);
return image;
}
@ -237,7 +239,6 @@ public static class TestImageExtensions
ITestImageProvider provider,
FormattableString testOutputDetails,
string extension = "png",
bool grayscale = false,
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel>
@ -246,7 +247,6 @@ public static class TestImageExtensions
provider,
(object)testOutputDetails,
extension,
grayscale,
appendPixelTypeToFileName,
appendSourceFileOrDescription);
@ -256,12 +256,11 @@ public static class TestImageExtensions
ITestImageProvider provider,
object testOutputDetails = null,
string extension = "png",
bool grayscale = false,
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel>
{
using (var firstFrameOnlyImage = new Image<TPixel>(image.Width, image.Height))
using (Image<TPixel> firstFrameOnlyImage = new(image.Width, image.Height))
using (Image<TPixel> referenceImage = GetReferenceOutputImage<TPixel>(
provider,
testOutputDetails,
@ -284,8 +283,8 @@ public static class TestImageExtensions
ImageComparer comparer,
object testOutputDetails = null,
string extension = "png",
bool grayscale = false,
bool appendPixelTypeToFileName = true)
bool appendPixelTypeToFileName = true,
Func<int, int, bool> predicate = null)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> referenceImage = GetReferenceOutputImageMultiFrame<TPixel>(
@ -293,9 +292,10 @@ public static class TestImageExtensions
image.Frames.Count,
testOutputDetails,
extension,
appendPixelTypeToFileName))
appendPixelTypeToFileName,
predicate: predicate))
{
comparer.VerifySimilarity(referenceImage, image);
comparer.VerifySimilarity(referenceImage, image, predicate);
}
return image;
@ -332,16 +332,18 @@ public static class TestImageExtensions
int frameCount,
object testOutputDetails = null,
string extension = "png",
bool appendPixelTypeToFileName = true)
bool appendPixelTypeToFileName = true,
Func<int, int, bool> predicate = null)
where TPixel : unmanaged, IPixel<TPixel>
{
string[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame(
frameCount,
extension,
testOutputDetails,
appendPixelTypeToFileName);
appendPixelTypeToFileName,
predicate);
var temporaryFrameImages = new List<Image<TPixel>>();
List<Image<TPixel>> temporaryFrameImages = new();
IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]);
@ -359,7 +361,7 @@ public static class TestImageExtensions
Image<TPixel> firstTemp = temporaryFrameImages[0];
var result = new Image<TPixel>(firstTemp.Width, firstTemp.Height);
Image<TPixel> result = new(firstTemp.Width, firstTemp.Height);
foreach (Image<TPixel> fi in temporaryFrameImages)
{

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb615374f4c680ed4b7e4922e6a0404446c520e254365a1c2406c3dcdad8d02f
size 2574

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/08.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7ac936ace1ea78c3aa7fb099853e32140278f0ce1b5f27cc1ac68aa9d256d5d6
size 161248

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/104.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5cc1406b0b5c7fd60f539414249007112224388b2cc27785833cf229e1078c81
size 181703

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/112.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0fa21bee072c1e2563770759c6fb95f7dc16e467e9aa9e29c5ab482acdbee170
size 182851

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/120.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:da813a5f5bbbf95f7f5c8464bdab10d1a7cb7b5f60169b64910f650b98056b3a
size 183582

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/128.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e12217fb78a91a18b0d2110ce1c38159534647e49e9f8390ae8b33eda1bf1046
size 183390

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/136.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f62ad66be6a04c50b47e1a047e54a177bbaf97ff8a3e4a170e114c3dcc2386c7
size 183231

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/144.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f91b0f28197e2dc9e2e010c32ae2c2cc79568c2e9158b40e383e88eb8d299f8
size 183209

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/152.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b17a8715a14e63e7b68f77a41eb15ce07f11fc4e652b27b1c071fda9182aa4e7
size 183214

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/16.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed78b0a881154b7867c749f4375a1341611d155aa100821211d76c70cacf70ae
size 166536

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/24.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f9c5ac7c97d903588ecd73205e85c732b72a708c35f1e88b3402f01e1a996222
size 172363

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/32.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f56c8daa27477f2e20702176f01a1e35f40a250d461fc3d5c3f4ded436b81dd9
size 173335

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/40.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41d36b364522adf170aa87f331ce8e1243ef24f0a0d730d8d62116d451380069
size 174487

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/48.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f80e8fc0f32f5eebc24066e2dca4dc193cc253561aa2d34a80055c17c9911741
size 174931

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/56.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:98ab9e6879e35841ed91a3c55d3daf26bed01f4b411cdac100caf21737e197e6
size 176282

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/64.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf1fba5a468f8944dec62b0ccf723a4843b46f0e1718c2b37deca00dc048cb20
size 179139

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/72.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dea9bf39eb210bfcfeb573cc50f3a9676b3d1da729b3ae2fc5af72dbc687668f
size 181197

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/80.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aa7aa1f601d12d20059bb51e8d642f72976e25f5e116a3f85b1741f0f557d8e9
size 179779

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/88.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:654fd1df2dec9c8694e60041a1fb8ebb3e213223038742da4b3f89173a3cf0c4
size 180044

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/96.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8102e88f544bd06317e52b485a7aaf81bb46ed82e4b617af29b4c9823d46dcfc
size 181874

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2cf3a4141ca32ab8f60060140f00fd79765b2950a542a146d3587596ad6770b
size 4489

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/08.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3195912d89a03928926ba56e6a7845d2ea7b0f9d0efc4854d5b36d99541eb01a
size 4596

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/16.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb10d95b54c4c2c3b589db0fe420a79f572752e27682666fc20eada3d001e281
size 4654

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/24.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ff9524242c8ad0fa5e87f32aa3a1365fe8062fee14d594c4f66a4aecf0bde05
size 4642

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/32.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a0d6b4b72c5ec38f36679a38d9c0e95f1aaf5a8dbe016174593a05ae6fa28f2b
size 4317

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/40.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:70be2794b20cec8ea558b9902b04dee6b1790bf5d867c8b8531ad71f238d8b73
size 4417

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/48.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3abc1beaefbc9c95a9ca828bbd06de8d1bed504b7e1877b66e3f881bbed2dbf4
size 4716

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/56.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d279d361a77bd0d95204853adf7d575a93118688625f6ec2dad3979fadfb456
size 4697

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/64.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de60756ff2501e88c83e2732c38456b8fc66780bb2302452cdd21f8b7bd82108
size 4936

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/72.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:606235a70e3b167192c0783c6eda9f2f5867cf14d5521a83af3441cfe1adc66f
size 4917

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/80.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1931befb45c7eedfa44518d62cf2fc8ecfc64e5505c1639d0b6187d988fa06c5
size 4951

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/88.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:785ed19db48a60886bebe90223e9f48f9d6df45b1e1c7e5ac467f6a9211db1f0
size 7528

3
tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/96.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:511d2e3ffec299188a715389b7a17f35bc152e3830a8ecc34ce93c044d1c3962
size 4897

3
tests/Images/Input/Gif/issues/issue_2198.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:48bd8a2992c3aeda920250effb53d4e9aef09c76dc5d0c5fade545ec5ba522a4
size 1863378

3
tests/Images/Input/Gif/issues/issue_2450.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de38adf0b7347862db03ef10f17df231e2985e6f0bfa2eb824d9bbca007ff04e
size 4107068

3
tests/Images/Input/Gif/issues/issue_2450_2.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af7c04d8a5db464be782aba904ad1fc6168d5ab196fef84314b1e2f6d703e923
size 29995

3
tests/Images/Input/Jpg/issues/Hang_C438A851.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:580760756f2e7e3ed0752a4ec53d6b6786a4f005606f3a50878f732b3b2a1bcb
size 413
Loading…
Cancel
Save