Browse Source

Merge branch 'main' into bp/tiff-jpeg-cmyk

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

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

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

2
.github/workflows/code-coverage.yml

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

1
ImageSharp.sln

@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1
Directory.Build.targets = Directory.Build.targets Directory.Build.targets = Directory.Build.targets
LICENSE = LICENSE LICENSE = LICENSE
README.md = README.md README.md = README.md
SixLabors.ImageSharp.props = SixLabors.ImageSharp.props
EndProjectSection EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1799C43E-5C54-4A8F-8D64-B1475241DB0D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1799C43E-5C54-4A8F-8D64-B1475241DB0D}"

2
SixLabors.ImageSharp.props

@ -2,7 +2,7 @@
<Project> <Project>
<!--Add common namespaces to implicit global usings if enabled.--> <!--Add common namespaces to implicit global usings if enabled.-->
<ItemGroup Condition="'$(ImplicitUsings)'=='enable' OR '$(ImplicitUsings)'=='true'"> <ItemGroup Condition="'$(UseImageSharp)'=='enable' OR '$(UseImageSharp)'=='true'">
<Using Include="SixLabors.ImageSharp" /> <Using Include="SixLabors.ImageSharp" />
<Using Include="SixLabors.ImageSharp.PixelFormats" /> <Using Include="SixLabors.ImageSharp.PixelFormats" />
<Using Include="SixLabors.ImageSharp.Processing" /> <Using Include="SixLabors.ImageSharp.Processing" />

10
src/ImageSharp/Advanced/ParallelRowIterator.cs

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

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

@ -139,7 +139,7 @@ public readonly partial struct Color
/// </summary> /// </summary>
/// <param name="color">The <see cref="Color"/>.</param> /// <param name="color">The <see cref="Color"/>.</param>
/// <returns>The <see cref="Vector4"/>.</returns> /// <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> /// <summary>
/// Converts an <see cref="Vector4"/> to <see cref="Color"/>. /// Converts an <see cref="Vector4"/> to <see cref="Color"/>.
@ -228,7 +228,7 @@ public readonly partial struct Color
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
internal Vector4 ToVector4() internal Vector4 ToScaledVector4()
{ {
if (this.boxedHighPrecisionPixel is null) 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)); 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> /// <summary>
/// <see cref="ByteToNormalizedFloat"/> as many elements as possible, slicing them down (keeping the remainder). /// <see cref="ByteToNormalizedFloat"/> as many elements as possible, slicing them down (keeping the remainder).
/// </summary> /// </summary>

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

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

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

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp; namespace SixLabors.ImageSharp.Formats.Bmp;
@ -100,7 +101,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel; this.bitsPerPixel = encoder.BitsPerPixel;
this.quantizer = encoder.Quantizer; this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; 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> /// </summary>
private IMemoryOwner<byte>? globalColorTable; 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> /// <summary>
/// The area to restore. /// The area to restore.
/// </summary> /// </summary>
@ -159,6 +169,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
finally finally
{ {
this.globalColorTable?.Dispose(); this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
} }
if (image is null) if (image is null)
@ -229,6 +240,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
finally finally
{ {
this.globalColorTable?.Dispose(); this.globalColorTable?.Dispose();
this.currentLocalColorTable?.Dispose();
} }
if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0) if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
@ -332,7 +344,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{ {
stream.Read(this.buffer.Span, 0, 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. stream.Skip(1); // Skip the terminator.
return; return;
} }
@ -415,25 +427,27 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
this.ReadImageDescriptor(stream); this.ReadImageDescriptor(stream);
IMemoryOwner<byte>? localColorTable = null;
Buffer2D<byte>? indices = null; Buffer2D<byte>? indices = null;
try try
{ {
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. // 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; // Read and store the local color table. We allocate the maximum possible size and slice to match.
localColorTable = this.configuration.MemoryAllocator.Allocate<byte>(length, AllocationOptions.Clean); int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
stream.Read(localColorTable.GetSpan()); 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); indices = this.configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(stream, indices); this.ReadFrameIndices(stream, indices);
Span<byte> rawColorTable = default; Span<byte> rawColorTable = default;
if (localColorTable != null) if (hasLocalColorTable)
{ {
rawColorTable = localColorTable.GetSpan(); rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize];
} }
else if (this.globalColorTable != null) else if (this.globalColorTable != null)
{ {
@ -448,7 +462,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
} }
finally finally
{ {
localColorTable?.Dispose();
indices?.Dispose(); indices?.Dispose();
} }
} }
@ -509,7 +522,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
prevFrame = previousFrame; 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); this.SetFrameMetadata(currentFrame.Metadata);
@ -631,7 +647,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
// Skip the color table for this frame if local. // Skip the color table for this frame if local.
if (this.imageDescriptor.LocalColorTableFlag) 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. // Skip the frame indices. Pixels length + mincode size.
@ -682,7 +701,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
GifFrameMetadata gifMeta = metadata.GetGifMetadata(); GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global; gifMeta.ColorTableMode = GifColorTableMode.Global;
gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
} }
if (this.imageDescriptor.LocalColorTableFlag if (this.imageDescriptor.LocalColorTableFlag
@ -690,13 +708,23 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
GifFrameMetadata gifMeta = metadata.GetGifMetadata(); GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local; 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. // Graphics control extensions is optional.
if (this.graphicsControlExtension != default) if (this.graphicsControlExtension != default)
{ {
GifFrameMetadata gifMeta = metadata.GetGifMetadata(); GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag;
gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex;
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime; gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
} }
@ -751,14 +779,22 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
if (this.logicalScreenDescriptor.GlobalColorTableFlag) if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{ {
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
this.gifMetadata.GlobalColorTableLength = globalColorTableLength;
if (globalColorTableLength > 0) if (globalColorTableLength > 0)
{ {
this.globalColorTable = this.memoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean); this.globalColorTable = this.memoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean);
// Read the global color table data from the stream // Read the global color table data from the stream and preserve it in the gif metadata
stream.Read(this.globalColorTable.GetSpan()); 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. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Formats.Gif;
@ -36,17 +40,17 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// The quantizer used to generate the color palette. /// The quantizer used to generate the color palette.
/// </summary> /// </summary>
private readonly IQuantizer quantizer; private IQuantizer? quantizer;
/// <summary> /// <summary>
/// The color table mode: Global or local. /// Whether the quantizer was supplied via options.
/// </summary> /// </summary>
private GifColorTableMode? colorTableMode; private readonly bool hasQuantizer;
/// <summary> /// <summary>
/// The number of bits requires to store the color palette. /// The color table mode: Global or local.
/// </summary> /// </summary>
private int bitDepth; private GifColorTableMode? colorTableMode;
/// <summary> /// <summary>
/// The pixel sampling strategy for global quantization. /// The pixel sampling strategy for global quantization.
@ -56,7 +60,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class. /// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param> /// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="encoder">The encoder with options.</param> /// <param name="encoder">The encoder with options.</param>
public GifEncoderCore(Configuration configuration, GifEncoder encoder) public GifEncoderCore(Configuration configuration, GifEncoder encoder)
{ {
@ -64,6 +68,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.memoryAllocator = configuration.MemoryAllocator; this.memoryAllocator = configuration.MemoryAllocator;
this.skipMetadata = encoder.SkipMetadata; this.skipMetadata = encoder.SkipMetadata;
this.quantizer = encoder.Quantizer; this.quantizer = encoder.Quantizer;
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode; this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
} }
@ -86,8 +91,28 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.colorTableMode ??= gifMetadata.ColorTableMode; this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette. // Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized; 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)) using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{ {
if (useGlobalTable) 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. // Write the header.
WriteHeader(stream); WriteHeader(stream);
// Write the LSD. // Write the LSD.
int index = GetTransparentIndex(quantized); transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, index, useGlobalTable, stream); 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) if (useGlobalTable)
{ {
this.WriteColorTable(quantized, stream); this.WriteColorTable(quantized, bitDepth, stream);
} }
if (!this.skipMetadata) if (!this.skipMetadata)
@ -127,41 +157,68 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); 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); stream.WriteByte(GifConstants.EndIntroducer);
} }
private void EncodeFrames<TPixel>( private void EncodeAdditionalFrames<TPixel>(
Stream stream, Stream stream,
Image<TPixel> image, Image<TPixel> image,
IndexedImageFrame<TPixel> quantized, ReadOnlyMemory<TPixel> globalPalette)
ReadOnlyMemory<TPixel> palette)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (image.Frames.Count == 1)
{
return;
}
PaletteQuantizer<TPixel> paletteQuantizer = default; PaletteQuantizer<TPixel> paletteQuantizer = default;
bool hasPaletteQuantizer = false; 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. // Gather the metadata for this frame.
ImageFrame<TPixel> frame = image.Frames[i]; ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata; ImageFrameMetadata metadata = currentFrame.Metadata;
bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata); metadata.TryGetGifMetadata(out GifFrameMetadata? gifMetadata);
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata!.ColorTableMode == GifColorTableMode.Local); bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0) if (!useLocal && !hasPaletteQuantizer && i > 0)
{ {
// The palette quantizer can reuse the same pixel map across multiple frames // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// since the palette is unchanging. This allows a reduction of memory usage across // This allows a reduction of memory usage across multi-frame gifs using a global palette
// 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; 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. previousFrame = currentFrame;
quantized.Dispose();
} }
if (hasPaletteQuantizer) if (hasPaletteQuantizer)
@ -170,88 +227,419 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
} }
} }
private void EncodeFrame<TPixel>( private void EncodeFirstFrame<TPixel>(
Stream stream, Stream stream,
ImageFrame<TPixel> frame, GifFrameMetadata? metadata,
int frameIndex, 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, bool useLocal,
GifFrameMetadata? metadata, GifFrameMetadata? metadata,
ref IndexedImageFrame<TPixel> quantized, PaletteQuantizer<TPixel> globalPaletteQuantizer)
ref PaletteQuantizer<TPixel> paletteQuantizer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// The first frame has already been quantized so we do not need to do so again. // Capture any explicit transparency index from the metadata.
if (frameIndex > 0) // 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) if (useLocal)
{ {
// Reassign using the current frame and details. if (metadata?.LocalColorTable?.Length > 0)
QuantizerOptions? options = null;
int colorTableLength = metadata?.ColorTableLength ?? 0;
if (colorTableLength > 0)
{ {
options = new() ReadOnlySpan<Color> palette = metadata.LocalColorTable.Value.Span;
if (transparencyIndex < palette.Length)
{ {
Dither = this.quantizer.Options.Dither, replacement = palette[transparencyIndex].ToScaledVector4();
DitherScale = this.quantizer.Options.DitherScale, }
MaxColors = colorTableLength }
}; }
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); IndexedImageFrame<TPixel> quantized;
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); 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 else
{ {
// Quantize the image using the global palette. // We must quantize the frame to generate a local color table.
quantized = paletteQuantizer.QuantizeFrame(frame, frame.Bounds()); 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? this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex);
int index = GetTransparentIndex(quantized); }
if (metadata != null || index > -1)
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> /// <summary>
/// Returns the index of the most transparent color in the palette. /// Returns the index of the most transparent color in the palette.
/// </summary> /// </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> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns> /// <returns>
/// The <see cref="int"/>. /// The <see cref="int"/>.
/// </returns> /// </returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized) private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Transparent pixels are much more likely to be found at the end of a palette. if (metadata?.HasTransparency == true)
int index = -1; {
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span; return metadata.TransparencyIndex;
}
using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteSpan.Length);
Span<Rgba32> rgbaSpan = rgbaOwner.GetSpan();
PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
for (int i = 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="metadata">The image metadata.</param>
/// <param name="width">The image width.</param> /// <param name="width">The image width.</param>
/// <param name="height">The image height.</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="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> /// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor( private void WriteLogicalScreenDescriptor(
ImageMetadata metadata, ImageMetadata metadata,
int width, int width,
int height, int height,
int transparencyIndex, byte backgroundIndex,
bool useGlobalTable, bool useGlobalTable,
int bitDepth,
Stream stream) 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 // 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 // width over its height. The value range in this field allows
@ -316,7 +706,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
width: (ushort)width, width: (ushort)width,
height: (ushort)height, height: (ushort)height,
packed: packedValue, packed: packedValue,
backgroundColorIndex: unchecked((byte)transparencyIndex), backgroundColorIndex: backgroundIndex,
ratio); ratio);
Span<byte> buffer = stackalloc byte[20]; 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="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="transparencyIndex">The index of the color in the color palette to make transparent.</param>
/// <param name="stream">The stream to write to.</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( byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metadata.DisposalMethod, disposalMethod: data!.DisposalMethod,
transparencyFlag: transparencyIndex > -1); transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new( GifGraphicControlExtension extension = new(
packed: packedValue, packed: packedValue,
delayTime: (ushort)metadata.FrameDelay, delayTime: (ushort)data.FrameDelay,
transparencyIndex: unchecked((byte)transparencyIndex)); transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue);
this.WriteExtension(extension, stream); this.WriteExtension(extension, stream);
} }
@ -443,7 +845,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
} }
IMemoryOwner<byte>? owner = null; 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) if (extensionSize > 128)
{ {
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3); owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
@ -466,26 +868,25 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
} }
/// <summary> /// <summary>
/// Writes the image descriptor to the stream. /// Writes the image frame descriptor to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="rectangle">The frame location and size.</param>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
/// <param name="hasColorTable">Whether to use the global color table.</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> /// <param name="stream">The stream to write to.</param>
private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, bool hasColorTable, Stream stream) private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, int bitDepth, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{ {
byte packedValue = GifImageDescriptor.GetPackedValue( byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: hasColorTable, localColorTableFlag: hasColorTable,
interfaceFlag: false, interfaceFlag: false,
sortFlag: false, sortFlag: false,
localColorTableSize: this.bitDepth - 1); localColorTableSize: bitDepth - 1);
GifImageDescriptor descriptor = new( GifImageDescriptor descriptor = new(
left: 0, left: (ushort)rectangle.X,
top: 0, top: (ushort)rectangle.Y,
width: (ushort)image.Width, width: (ushort)rectangle.Width,
height: (ushort)image.Height, height: (ushort)rectangle.Height,
packed: packedValue); packed: packedValue);
Span<byte> buffer = stackalloc byte[20]; Span<byte> buffer = stackalloc byte[20];
@ -499,12 +900,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param> /// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
// The maximum number of colors for the bit depth // The maximum number of colors for the bit depth
int colorTableLength = 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); using IMemoryOwner<byte> colorTable = this.memoryAllocator.Allocate<byte>(colorTableLength, AllocationOptions.Clean);
Span<byte> colorTableSpan = colorTable.GetSpan(); Span<byte> colorTableSpan = colorTable.GetSpan();
@ -521,13 +923,23 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Writes the image pixel data to the stream. /// Writes the image pixel data to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="indices">The <see cref="Buffer2DRegion{Byte}"/> containing indexed pixels.</param>
/// <param name="image">The <see cref="IndexedImageFrame{TPixel}"/> containing indexed pixels.</param> /// <param name="interest">The region of interest.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteImageData<TPixel>(IndexedImageFrame<TPixel> image, Stream stream) /// <param name="paletteLength">The length of the frame color palette.</param>
where TPixel : unmanaged, IPixel<TPixel> /// <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); Buffer2DRegion<byte> region = indices.GetRegion(interest);
encoder.Encode(((IPixelSource)image).PixelBuffer, stream);
// 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. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary> /// <summary>
@ -22,9 +24,16 @@ public class GifFrameMetadata : IDeepCloneable
private GifFrameMetadata(GifFrameMetadata other) private GifFrameMetadata(GifFrameMetadata other)
{ {
this.ColorTableMode = other.ColorTableMode; this.ColorTableMode = other.ColorTableMode;
this.ColorTableLength = other.ColorTableLength;
this.FrameDelay = other.FrameDelay; this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod; this.DisposalMethod = other.DisposalMethod;
if (other.LocalColorTable?.Length > 0)
{
this.LocalColorTable = other.LocalColorTable.Value.ToArray();
}
this.HasTransparency = other.HasTransparency;
this.TransparencyIndex = other.TransparencyIndex;
} }
/// <summary> /// <summary>
@ -33,11 +42,22 @@ public class GifFrameMetadata : IDeepCloneable
public GifColorTableMode ColorTableMode { get; set; } public GifColorTableMode ColorTableMode { get; set; }
/// <summary> /// <summary>
/// Gets or sets the length of the color table. /// Gets or sets the local color table, if any.
/// If not 0, then this field indicates the maximum number of colors to use when quantizing the /// The underlying pixel format is represented by <see cref="Rgb24"/>.
/// image frame. /// </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> /// </summary>
public int ColorTableLength { get; set; } public byte TransparencyIndex { get; set; }
/// <summary> /// <summary>
/// Gets or sets the frame delay for animated images. /// Gets or sets the frame delay for animated images.

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

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary> /// <summary>
@ -23,7 +25,12 @@ public class GifMetadata : IDeepCloneable
{ {
this.RepeatCount = other.RepeatCount; this.RepeatCount = other.RepeatCount;
this.ColorTableMode = other.ColorTableMode; 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++) for (int i = 0; i < other.Comments.Count; i++)
{ {
@ -45,9 +52,16 @@ public class GifMetadata : IDeepCloneable
public GifColorTableMode ColorTableMode { get; set; } public GifColorTableMode ColorTableMode { get; set; }
/// <summary> /// <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> /// </summary>
public int GlobalColorTableLength { get; set; } public byte BackgroundColorIndex { get; set; }
/// <summary> /// <summary>
/// Gets or sets the collection of comments about the graphics, credits, descriptions or any /// 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> /// </summary>
/// <param name="indexedPixels">The 2D buffer of indexed pixels.</param> /// <param name="indexedPixels">The 2D buffer of indexed pixels.</param>
/// <param name="stream">The stream to write to.</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 // Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize); 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="indexedPixels">The 2D buffer of indexed pixels.</param>
/// <param name="initialBits">The initial bits.</param> /// <param name="initialBits">The initial bits.</param>
/// <param name="stream">The stream to write to.</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 // Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = initialBits; this.globalInitialBits = initialBits;

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

@ -17,14 +17,16 @@ public static partial class MetadataExtensions
/// </summary> /// </summary>
/// <param name="source">The metadata this method extends.</param> /// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifMetadata"/>.</returns> /// <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> /// <summary>
/// Gets the gif format specific metadata for the image frame. /// Gets the gif format specific metadata for the image frame.
/// </summary> /// </summary>
/// <param name="source">The metadata this method extends.</param> /// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="GifFrameMetadata"/>.</returns> /// <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> /// <summary>
/// Gets the gif format specific metadata for the image frame. /// Gets the gif format specific metadata for the image frame.
@ -38,5 +40,6 @@ public static partial class MetadataExtensions
/// <returns> /// <returns>
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>. /// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
/// </returns> /// </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() private int ReadStream()
{ {
int value = this.badData ? 0 : this.stream.ReadByte(); 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 // 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. // in the image or the SOS marker has the wrong dimensions set.

2
src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs

@ -28,7 +28,7 @@ internal static class BufferedReadStreamExtensions
{ {
innerValue = stream.ReadByte(); innerValue = stream.ReadByte();
} }
while (innerValue != 0x0a); while (innerValue is not 0x0a and not -0x1);
// Continue searching for whitespace. // Continue searching for whitespace.
val = innerValue; val = innerValue;

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

@ -35,9 +35,9 @@ internal static class PaethFilter
// row: a d // row: a d
// The Paeth function predicts d to be whichever of a, b, or c is nearest to // The Paeth function predicts d to be whichever of a, b, or c is nearest to
// p = a + b - c. // 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) else if (AdvSimd.Arm64.IsSupported && bytesPerPixel is 4)
{ {
@ -50,7 +50,7 @@ internal static class PaethFilter
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); 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)); Vector128<short> smallest = Sse2.Min(pc, Sse2.Min(pa, pb));
// Paeth breaks ties favoring a over b over c. // Paeth breaks ties favoring a over b over c.
Vector128<byte> mask = Sse41.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte()); Vector128<byte> mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = Sse41.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte()); Vector128<byte> nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte());
// Note `_epi8`: we need addition to wrap modulo 255. // Note `_epi8`: we need addition to wrap modulo 255.
d = Sse2.Add(d, nearest); d = Sse2.Add(d, nearest);
@ -143,8 +143,8 @@ internal static class PaethFilter
Vector128<short> smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb)); Vector128<short> smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb));
// Paeth breaks ties favoring a over b over c. // Paeth breaks ties favoring a over b over c.
Vector128<byte> mask = BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte()); Vector128<byte> mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte()); Vector128<byte> nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte());
d = AdvSimd.Add(d, nearest); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, uint bytesPerPixel) private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, uint bytesPerPixel)
{ {

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

@ -11,15 +11,6 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary> /// </summary>
public class PngEncoder : QuantizingImageEncoder 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> /// <summary>
/// Gets the number of bits per sample or per palette index (not per pixel). /// 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. /// 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. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
@ -14,7 +13,7 @@ public abstract class QuantizingImageEncoder : ImageEncoder
/// <summary> /// <summary>
/// Gets the quantizer used to generate the color palette. /// Gets the quantizer used to generate the color palette.
/// </summary> /// </summary>
public IQuantizer Quantizer { get; init; } = KnownQuantizers.Octree; public IQuantizer? Quantizer { get; init; }
/// <summary> /// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes. /// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.

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

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

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

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
@ -85,7 +86,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation; this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = options.PixelSamplingStrategy; this.pixelSamplingStrategy = options.PixelSamplingStrategy;
this.BitsPerPixel = options.BitsPerPixel; this.BitsPerPixel = options.BitsPerPixel;
this.HorizontalPredictor = options.HorizontalPredictor; this.HorizontalPredictor = options.HorizontalPredictor;
@ -157,6 +158,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
long ifdMarker = WriteHeader(writer, buffer); long ifdMarker = WriteHeader(writer, buffer);
Image<TPixel> metadataImage = image; Image<TPixel> metadataImage = image;
foreach (ImageFrame<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -235,9 +237,13 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
if (image != null) if (image != null)
{ {
// Write the metadata for the root image
entriesCollector.ProcessMetadata(image, this.skipMetadata); entriesCollector.ProcessMetadata(image, this.skipMetadata);
} }
// Write the metadata for the frame
entriesCollector.ProcessMetadata(frame, this.skipMetadata);
entriesCollector.ProcessFrameInfo(frame, imageMetadata); entriesCollector.ProcessFrameInfo(frame, imageMetadata);
entriesCollector.ProcessImageFormat(this); entriesCollector.ProcessImageFormat(this);
@ -320,7 +326,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
{ {
int sz = ExifWriter.WriteValue(entry, buffer, 0); int sz = ExifWriter.WriteValue(entry, buffer, 0);
DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written"); DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written");
writer.WritePadded(buffer.Slice(0, sz)); writer.WritePadded(buffer[..sz]);
} }
else 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.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; 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.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
@ -19,6 +21,9 @@ internal class TiffEncoderEntriesCollector
public void ProcessMetadata(Image image, bool skipMetadata) public void ProcessMetadata(Image image, bool skipMetadata)
=> new MetadataProcessor(this).Process(image, 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) public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata)
=> new FrameInfoProcessor(this).Process(frame, imageMetadata); => new FrameInfoProcessor(this).Process(frame, imageMetadata);
@ -56,15 +61,29 @@ internal class TiffEncoderEntriesCollector
public void Process(Image image, bool skipMetadata) public void Process(Image image, bool skipMetadata)
{ {
ImageFrame rootFrame = image.Frames.RootFrame; this.ProcessProfiles(image.Metadata, skipMetadata);
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile;
XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile;
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) if (!skipMetadata)
{ {
this.ProcessMetadata(rootFrameExifProfile ?? new ExifProfile()); this.ProcessMetadata(frame.Metadata.ExifProfile ?? new ExifProfile());
} }
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) 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)) if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None))
{ {
@ -170,13 +205,16 @@ internal class TiffEncoderEntriesCollector
{ {
exifProfile?.RemoveValue(ExifTag.SubIFDOffset); 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) ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte)
{ {
Value = imageMetadata.IptcProfile.Data Value = iptcProfile.Data
}; };
this.Collector.AddOrReplace(iptc); this.Collector.AddOrReplace(iptc);
@ -185,12 +223,15 @@ internal class TiffEncoderEntriesCollector
{ {
exifProfile?.RemoveValue(ExifTag.IPTC); 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) ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined)
{ {
Value = imageMetadata.IccProfile.ToByteArray() Value = iccProfile.ToByteArray()
}; };
this.Collector.AddOrReplace(icc); this.Collector.AddOrReplace(icc);
@ -199,7 +240,10 @@ internal class TiffEncoderEntriesCollector
{ {
exifProfile?.RemoveValue(ExifTag.IccProfile); exifProfile?.RemoveValue(ExifTag.IccProfile);
} }
}
private void ProcessXmpProfile(bool skipMetadata, XmpProfile xmpProfile, ExifProfile exifProfile)
{
if (!skipMetadata && xmpProfile != null) if (!skipMetadata && xmpProfile != null)
{ {
ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte) 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; 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> /// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class. /// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary> /// </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), /// 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! /// copies the contents of 'source' to 'destination' otherwise (2). Buffers should be of same size in case 2!
/// </summary> /// </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) internal static bool SwapOrCopyContent(Buffer2D<T> destination, Buffer2D<T> source)
{ {
bool swapped = false; bool swapped = false;
if (MemoryGroup<T>.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup)) if (MemoryGroup<T>.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup))
{ {
(destination.FastMemoryGroup, source.FastMemoryGroup) = (destination.FastMemoryGroup, source.FastMemoryGroup) = (source.FastMemoryGroup, destination.FastMemoryGroup);
(source.FastMemoryGroup, destination.FastMemoryGroup);
destination.FastMemoryGroup.RecreateViewAfterSwap(); destination.FastMemoryGroup.RecreateViewAfterSwap();
source.FastMemoryGroup.RecreateViewAfterSwap(); source.FastMemoryGroup.RecreateViewAfterSwap();
swapped = true; swapped = true;
@ -201,7 +203,6 @@ public sealed class Buffer2D<T> : IDisposable
} }
[MethodImpl(InliningOptions.ColdPath)] [MethodImpl(InliningOptions.ColdPath)]
private void ThrowYOutOfRangeException(int y) => private void ThrowYOutOfRangeException(int y)
throw new ArgumentOutOfRangeException( => throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
$"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.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -34,17 +35,25 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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)); 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()); using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size());
source.CopyTo(targetPixels); source.CopyTo(targetPixels);
RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, this.definition.Levels); RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, levels);
ParallelRowIterator.IterateRowIntervals( try
{
ParallelRowIterator.IterateRowIntervals(
this.Configuration, this.Configuration,
this.SourceRectangle, this.SourceRectangle,
in operation); 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); Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
} }
@ -105,18 +114,18 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
Span<Vector4> targetRowVector4Span = targetRowBuffer.Memory.Span; Span<Vector4> targetRowVector4Span = targetRowBuffer.Memory.Span;
Span<Vector4> targetRowAreaVector4Span = targetRowVector4Span.Slice(this.bounds.X, this.bounds.Width); Span<Vector4> targetRowAreaVector4Span = targetRowVector4Span.Slice(this.bounds.X, this.bounds.Width);
ref float binsRef = ref bins.GetReference(); Span<float> binsSpan = bins.GetSpan();
ref int intensityBinRef = ref Unsafe.As<float, int>(ref binsRef); Span<int> intensityBinsSpan = MemoryMarshal.Cast<float, int>(binsSpan);
ref float redBinRef = ref Unsafe.Add(ref binsRef, (uint)this.levels); Span<float> redBinSpan = binsSpan[this.levels..];
ref float blueBinRef = ref Unsafe.Add(ref redBinRef, (uint)this.levels); Span<float> blueBinSpan = redBinSpan[this.levels..];
ref float greenBinRef = ref Unsafe.Add(ref blueBinRef, (uint)this.levels); Span<float> greenBinSpan = blueBinSpan[this.levels..];
for (int y = rows.Min; y < rows.Max; y++) for (int y = rows.Min; y < rows.Max; y++)
{ {
Span<TPixel> sourceRowPixelSpan = this.source.DangerousGetRowSpan(y); Span<TPixel> sourceRowPixelSpan = this.source.DangerousGetRowSpan(y);
Span<TPixel> sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(this.bounds.X, this.bounds.Width); 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++) 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; int offsetX = x + fxr;
offsetX = Numerics.Clamp(offsetX, 0, maxX); offsetX = Numerics.Clamp(offsetX, 0, maxX);
Vector4 vector = sourceOffsetRow[offsetX].ToVector4(); Vector4 vector = sourceOffsetRow[offsetX].ToScaledVector4();
float sourceRed = vector.X; float sourceRed = vector.X;
float sourceBlue = vector.Z; 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)); int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (this.levels - 1));
Unsafe.Add(ref intensityBinRef, (uint)currentIntensity)++; intensityBinsSpan[currentIntensity]++;
Unsafe.Add(ref redBinRef, (uint)currentIntensity) += sourceRed; redBinSpan[currentIntensity] += sourceRed;
Unsafe.Add(ref blueBinRef, (uint)currentIntensity) += sourceBlue; blueBinSpan[currentIntensity] += sourceBlue;
Unsafe.Add(ref greenBinRef, (uint)currentIntensity) += sourceGreen; 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; maxIndex = currentIntensity;
} }
} }
float red = MathF.Abs(Unsafe.Add(ref redBinRef, (uint)maxIndex) / maxIntensity); float red = redBinSpan[maxIndex] / maxIntensity;
float blue = MathF.Abs(Unsafe.Add(ref blueBinRef, (uint)maxIndex) / maxIntensity); float blue = blueBinSpan[maxIndex] / maxIntensity;
float green = MathF.Abs(Unsafe.Add(ref greenBinRef, (uint)maxIndex) / maxIntensity); float green = greenBinSpan[maxIndex] / maxIntensity;
float alpha = sourceRowVector4Span[x].W; float alpha = sourceRowVector4Span[x].W;
targetRowVector4Span[x] = new Vector4(red, green, blue, alpha); 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); 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. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -14,13 +15,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <para> /// <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. /// Doing so will result in non-idempotent results.
/// </para> /// </para>
internal sealed class EuclideanPixelMap<TPixel> : IDisposable internal sealed class EuclideanPixelMap<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private Rgba32[] rgbaPalette; private Rgba32[] rgbaPalette;
private int transparentIndex;
/// <summary> /// <summary>
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls. /// 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="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param> /// <param name="palette">The color palette to map from.</param>
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette) 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.configuration = configuration;
this.Palette = palette; this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length]; this.rgbaPalette = new Rgba32[palette.Length];
this.cache = new ColorDistanceCache(configuration.MemoryAllocator); this.cache = new ColorDistanceCache(configuration.MemoryAllocator);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); 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> /// <summary>
/// Gets the color palette of this <see cref="EuclideanPixelMap{TPixel}"/>. /// Gets the color palette of this <see cref="EuclideanPixelMap{TPixel}"/>.
/// The palette memory is owned by the palette source that created it. /// The palette memory is owned by the palette source that created it.
/// </summary> /// </summary>
public ReadOnlyMemory<TPixel> Palette public ReadOnlyMemory<TPixel> Palette { get; private set; }
{
[MethodImpl(InliningOptions.ShortMethod)]
get;
[MethodImpl(InliningOptions.ShortMethod)]
private set;
}
/// <summary> /// <summary>
/// Returns the closest color in the palette and the index of that pixel. /// 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(); 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)] [MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
{ {
// Loop through the palette and find the nearest match. // Loop through the palette and find the nearest match.
int index = 0; int index = 0;
float leastDistance = float.MaxValue; float leastDistance = float.MaxValue;
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++) for (int i = 0; i < this.rgbaPalette.Length; i++)
{ {
Rgba32 candidate = this.rgbaPalette[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 it's an exact match, exit the loop
if (distance == 0) if (distance == 0)
@ -130,12 +156,12 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// <param name="b">The second point.</param> /// <param name="b">The second point.</param>
/// <returns>The distance squared.</returns> /// <returns>The distance squared.</returns>
[MethodImpl(InliningOptions.ShortMethod)] [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; float deltaR = a.R - b.R;
int deltaG = a.G - b.G; float deltaG = a.G - b.G;
int deltaB = a.B - b.B; float deltaB = a.B - b.B;
int deltaA = a.A - b.A; float deltaA = a.A - b.A;
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); 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 public class PaletteQuantizer : IQuantizer
{ {
private readonly ReadOnlyMemory<Color> colorPalette; private readonly ReadOnlyMemory<Color> colorPalette;
private readonly int transparentIndex;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class. /// 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="palette">The color palette.</param>
/// <param name="options">The quantizer options defining quantization rules.</param> /// <param name="options">The quantizer options defining quantization rules.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options) 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.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
this.colorPalette = palette; this.colorPalette = palette;
this.Options = options; this.Options = options;
this.transparentIndex = transparentIndex;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -52,6 +65,6 @@ public class PaletteQuantizer : IQuantizer
// Always use the palette length over options since the palette cannot be reduced. // Always use the palette length over options since the palette cannot be reduced.
TPixel[] palette = new TPixel[this.colorPalette.Length]; TPixel[] palette = new TPixel[this.colorPalette.Length];
Color.ToPixel(this.colorPalette.Span, palette.AsSpan()); 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> /// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct. /// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct.
/// </summary> /// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param> /// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="options">The quantizer options defining quantization rules.</param> /// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="palette">The palette to use.</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)] [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(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
this.Configuration = configuration; this.Configuration = configuration;
this.Options = options; this.Options = options;
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette); this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette, transparentIndex);
} }
/// <inheritdoc/> /// <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/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, out TPixel match) 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> /// </summary>
public float DitherScale public float DitherScale
{ {
get { return this.ditherScale; } get => this.ditherScale;
set { this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); } set => this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale);
} }
/// <summary> /// <summary>
@ -35,7 +35,7 @@ public class QuantizerOptions
/// </summary> /// </summary>
public int MaxColors public int MaxColors
{ {
get { return this.maxColors; } get => this.maxColors;
set { this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.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 } { Bit32Rgb, BmpBitsPerPixel.Pixel32 }
}; };
[Fact]
public void BmpEncoderDefaultInstanceHasQuantizer() => Assert.NotNull(BmpEncoder.Quantizer);
[Theory] [Theory]
[MemberData(nameof(RatioFiles))] [MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) 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 Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
input.Save(memStream, BmpEncoder); input.Save(memStream, BmpEncoder);
memStream.Position = 0; memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream); using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageMetadata meta = output.Metadata; ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution); Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution); Assert.Equal(yResolution, meta.VerticalResolution);
@ -67,13 +70,13 @@ public class BmpEncoderTests
[MemberData(nameof(BmpBitsPerPixelFiles))] [MemberData(nameof(BmpBitsPerPixelFiles))]
public void Encode_PreserveBitsPerPixel(string imagePath, BmpBitsPerPixel bmpBitsPerPixel) 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 Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
input.Save(memStream, BmpEncoder); input.Save(memStream, BmpEncoder);
memStream.Position = 0; memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream); using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
BmpMetadata meta = output.Metadata.GetBmpMetadata(); BmpMetadata meta = output.Metadata.GetBmpMetadata();
Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel);
@ -196,8 +199,8 @@ public class BmpEncoderTests
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// arrange // arrange
var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel }; BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel };
using var memoryStream = new MemoryStream(); using MemoryStream memoryStream = new();
using Image<TPixel> input = provider.GetImage(BmpDecoder.Instance); using Image<TPixel> input = provider.GetImage(BmpDecoder.Instance);
// act // act
@ -205,7 +208,7 @@ public class BmpEncoderTests
memoryStream.Position = 0; memoryStream.Position = 0;
// assert // assert
using var actual = Image.Load<TPixel>(memoryStream); using Image<TPixel> actual = Image.Load<TPixel>(memoryStream);
ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual); ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual);
Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image"); Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image");
} }
@ -218,8 +221,8 @@ public class BmpEncoderTests
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// arrange // arrange
var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel }; BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel };
using var memoryStream = new MemoryStream(); using MemoryStream memoryStream = new();
using Image<TPixel> input = provider.GetImage(BmpDecoder.Instance); using Image<TPixel> input = provider.GetImage(BmpDecoder.Instance);
// act // act
@ -227,7 +230,7 @@ public class BmpEncoderTests
memoryStream.Position = 0; memoryStream.Position = 0;
// assert // assert
using var actual = Image.Load<TPixel>(memoryStream); using Image<TPixel> actual = Image.Load<TPixel>(memoryStream);
ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual); ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual);
Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image"); Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image");
} }
@ -266,7 +269,7 @@ public class BmpEncoderTests
} }
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
var encoder = new BmpEncoder BmpEncoder encoder = new()
{ {
BitsPerPixel = BmpBitsPerPixel.Pixel8, BitsPerPixel = BmpBitsPerPixel.Pixel8,
Quantizer = new WuQuantizer() Quantizer = new WuQuantizer()
@ -298,7 +301,7 @@ public class BmpEncoderTests
} }
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
var encoder = new BmpEncoder BmpEncoder encoder = new()
{ {
BitsPerPixel = BmpBitsPerPixel.Pixel8, BitsPerPixel = BmpBitsPerPixel.Pixel8,
Quantizer = new OctreeQuantizer() Quantizer = new OctreeQuantizer()
@ -333,11 +336,11 @@ public class BmpEncoderTests
ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile;
byte[] expectedProfileBytes = expectedProfile.ToByteArray(); byte[] expectedProfileBytes = expectedProfile.ToByteArray();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
input.Save(memStream, new BmpEncoder()); input.Save(memStream, new BmpEncoder());
memStream.Position = 0; 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; ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile;
byte[] actualProfileBytes = actualProfile.ToByteArray(); byte[] actualProfileBytes = actualProfile.ToByteArray();
@ -353,7 +356,7 @@ public class BmpEncoderTests
Exception exception = Record.Exception(() => Exception exception = Record.Exception(() =>
{ {
using Image image = new Image<Rgba32>(width, height); using Image image = new Image<Rgba32>(width, height);
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
image.Save(memStream, BmpEncoder); image.Save(memStream, BmpEncoder);
}); });
@ -411,7 +414,7 @@ public class BmpEncoderTests
image.Mutate(c => c.MakeOpaque()); image.Mutate(c => c.MakeOpaque());
} }
var encoder = new BmpEncoder BmpEncoder encoder = new()
{ {
BitsPerPixel = bitsPerPixel, BitsPerPixel = bitsPerPixel,
SupportTransparency = supportTransparency, SupportTransparency = supportTransparency,

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

@ -34,6 +34,20 @@ public class GifDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); 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] [Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void GifDecoder_Decode_Resize<TPixel>(TestImageProvider<TPixel> provider) 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] [Theory]
[WithTestPatternImages(100, 100, TestPixelTypes, false)] [WithTestPatternImages(100, 100, TestPixelTypes, false)]
[WithTestPatternImages(100, 100, TestPixelTypes, false)] [WithTestPatternImages(100, 100, TestPixelTypes, true)]
public void EncodeGeneratedPatterns<TPixel>(TestImageProvider<TPixel> provider, bool limitAllocationBuffer) public void EncodeGeneratedPatterns<TPixel>(TestImageProvider<TPixel> provider, bool limitAllocationBuffer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -171,10 +174,21 @@ public class GifEncoderTests
GifMetadata metaData = image.Metadata.GetGifMetadata(); GifMetadata metaData = image.Metadata.GetGifMetadata();
GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata();
GifColorTableMode colorMode = metaData.ColorTableMode; GifColorTableMode colorMode = metaData.ColorTableMode;
int maxColors;
if (colorMode == GifColorTableMode.Global)
{
maxColors = metaData.GlobalColorTable.Value.Length;
}
else
{
maxColors = frameMetadata.LocalColorTable.Value.Length;
}
GifEncoder encoder = new() GifEncoder encoder = new()
{ {
ColorTableMode = colorMode, ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength }) Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = maxColors })
}; };
image.Save(outStream, encoder); image.Save(outStream, encoder);
@ -187,15 +201,31 @@ public class GifEncoderTests
Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode); Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode);
// Gifiddle and Cyotek GifInfo say this image has 64 colors. // 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++) for (int i = 0; i < image.Frames.Count; i++)
{ {
GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata(); GifFrameMetadata iMeta = image.Frames[i].Metadata.GetGifMetadata();
GifFrameMetadata cifm = clone.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(iMeta.FrameDelay, cMeta.FrameDelay);
Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); Assert.Equal(iMeta.HasTransparency, cMeta.HasTransparency);
Assert.Equal(iMeta.TransparencyIndex, cMeta.TransparencyIndex);
} }
image.Dispose(); image.Dispose();

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

@ -11,21 +11,22 @@ public class GifFrameMetadataTests
[Fact] [Fact]
public void CloneIsDeep() public void CloneIsDeep()
{ {
var meta = new GifFrameMetadata GifFrameMetadata meta = new()
{ {
FrameDelay = 1, FrameDelay = 1,
DisposalMethod = GifDisposalMethod.RestoreToBackground, 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.FrameDelay = 2;
clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious; clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious;
clone.ColorTableLength = 1; clone.LocalColorTable = new[] { Color.Black };
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay)); Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod)); 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. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using Microsoft.CodeAnalysis;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
@ -35,7 +34,7 @@ public class GifMetadataTests
{ {
RepeatCount = 1, RepeatCount = 1,
ColorTableMode = GifColorTableMode.Global, ColorTableMode = GifColorTableMode.Global,
GlobalColorTableLength = 2, GlobalColorTable = new[] { Color.Black, Color.White },
Comments = new List<string> { "Foo" } Comments = new List<string> { "Foo" }
}; };
@ -43,11 +42,12 @@ public class GifMetadataTests
clone.RepeatCount = 2; clone.RepeatCount = 2;
clone.ColorTableMode = GifColorTableMode.Local; clone.ColorTableMode = GifColorTableMode.Local;
clone.GlobalColorTableLength = 1; clone.GlobalColorTable = new[] { Color.Black };
Assert.False(meta.RepeatCount.Equals(clone.RepeatCount)); Assert.False(meta.RepeatCount.Equals(clone.RepeatCount));
Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode)); 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.False(meta.Comments.Equals(clone.Comments));
Assert.True(meta.Comments.SequenceEqual(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(); GifFrameMetadata gifFrameMetadata = imageInfo.FrameMetadataCollection[imageInfo.FrameMetadataCollection.Count - 1].GetGifMetadata();
Assert.Equal(colorTableMode, gifFrameMetadata.ColorTableMode); 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(frameDelay, gifFrameMetadata.FrameDelay);
Assert.Equal(disposalMethod, gifFrameMetadata.DisposalMethod); 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.DebugSave(provider);
image.CompareToOriginal(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);
}
} }

11
tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Pbm;
using static SixLabors.ImageSharp.Tests.TestImages.Pbm; using static SixLabors.ImageSharp.Tests.TestImages.Pbm;
@ -80,4 +81,14 @@ public class PbmMetadataTests
Assert.NotNull(bitmapMetadata); Assert.NotNull(bitmapMetadata);
Assert.Equal(expectedComponentType, bitmapMetadata.ComponentType); Assert.Equal(expectedComponentType, bitmapMetadata.ComponentType);
} }
[Fact]
public void Identify_HandlesCraftedDenialOfServiceString()
{
byte[] bytes = Convert.FromBase64String("UDEjWAAACQAAAAA=");
ImageInfo info = Image.Identify(bytes);
Assert.Equal(default, info.Size);
Configuration.Default.ImageFormatsManager.TryFindFormatByFileExtension("pbm", out IImageFormat format);
Assert.Equal(format!, info.Metadata.DecodedImageFormat);
}
} }

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

@ -99,6 +99,9 @@ public partial class PngEncoderTests
{ TestImages.Png.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio } { TestImages.Png.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio }
}; };
[Fact]
public void PngEncoderDefaultInstanceHasNullQuantizer() => Assert.Null(PngEncoder.Quantizer);
[Theory] [Theory]
[WithFile(TestImages.Png.Palette8Bpp, nameof(PngColorTypes), PixelTypes.Rgba32)] [WithFile(TestImages.Png.Palette8Bpp, nameof(PngColorTypes), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(PngColorTypes), 48, 24, 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 pngBitDepthInfo = appendPngBitDepth ? bitDepth.ToString() : string.Empty;
string pngInterlaceModeInfo = interlaceMode != PngInterlaceMode.None ? $"_{interlaceMode}" : 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); 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")] [Trait("Format", "Tiff")]
public class TiffEncoderTests : TiffEncoderBaseTester public class TiffEncoderTests : TiffEncoderBaseTester
{ {
[Fact]
public void TiffEncoderDefaultInstanceHasQuantizer() => Assert.NotNull(new TiffEncoder().Quantizer);
[Theory] [Theory]
[InlineData(null, TiffBitsPerPixel.Bit24)] [InlineData(null, TiffBitsPerPixel.Bit24)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)]
@ -28,18 +31,18 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffBitsPerPixel expectedBitsPerPixel) public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffBitsPerPixel expectedBitsPerPixel)
{ {
// arrange // arrange
var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation }; TiffEncoder tiffEncoder = new() { PhotometricInterpretation = photometricInterpretation };
using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16 using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
? new Image<L16>(10, 10) ? new Image<L16>(10, 10)
: new Image<Rgb24>(10, 10); : new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);
// assert // assert
memStream.Position = 0; 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(); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
Assert.Equal(TiffCompression.None, frameMetaData.Compression); Assert.Equal(TiffCompression.None, frameMetaData.Compression);
@ -54,16 +57,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel) public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel)
{ {
// arrange // arrange
var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel }; TiffEncoder tiffEncoder = new()
{ BitsPerPixel = bitsPerPixel };
using Image input = new Image<Rgb24>(10, 10); using Image input = new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);
// assert // assert
memStream.Position = 0; 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(); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(bitsPerPixel, frameMetaData.BitsPerPixel); Assert.Equal(bitsPerPixel, frameMetaData.BitsPerPixel);
@ -81,16 +85,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPixel bitsPerPixel) public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPixel bitsPerPixel)
{ {
// arrange // arrange
var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel }; TiffEncoder tiffEncoder = new()
{ BitsPerPixel = bitsPerPixel };
using Image input = new Image<Rgb24>(10, 10); using Image input = new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);
// assert // assert
memStream.Position = 0; 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(); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel); Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel);
@ -103,16 +108,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_WithInvalidCompressionAndPixelTypeCombination_DefaultsToRgb(TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression) public void EncoderOptions_WithInvalidCompressionAndPixelTypeCombination_DefaultsToRgb(TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression)
{ {
// arrange // 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 Image input = new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);
// assert // assert
memStream.Position = 0; 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(); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel); Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel);
@ -149,18 +155,19 @@ public class TiffEncoderTests : TiffEncoderBaseTester
TiffCompression expectedCompression) TiffCompression expectedCompression)
{ {
// arrange // arrange
var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression }; TiffEncoder tiffEncoder = new()
{ PhotometricInterpretation = photometricInterpretation, Compression = compression };
using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16 using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
? new Image<L16>(10, 10) ? new Image<L16>(10, 10)
: new Image<Rgb24>(10, 10); : new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);
// assert // assert
memStream.Position = 0; 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(); TiffFrameMetadata rootFrameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, rootFrameMetaData.BitsPerPixel); Assert.Equal(expectedBitsPerPixel, rootFrameMetaData.BitsPerPixel);
Assert.Equal(expectedCompression, rootFrameMetaData.Compression); Assert.Equal(expectedCompression, rootFrameMetaData.Compression);
@ -178,16 +185,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// arrange // arrange
var tiffEncoder = new TiffEncoder(); TiffEncoder tiffEncoder = new();
using Image<TPixel> input = provider.GetImage(); using Image<TPixel> input = provider.GetImage();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);
// assert // assert
memStream.Position = 0; 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(); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
} }
@ -196,17 +203,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void TiffEncoder_PreservesBitsPerPixel_WhenInputIsL8() public void TiffEncoder_PreservesBitsPerPixel_WhenInputIsL8()
{ {
// arrange // arrange
var tiffEncoder = new TiffEncoder(); TiffEncoder tiffEncoder = new();
using Image input = new Image<L8>(10, 10); using Image input = new Image<L8>(10, 10);
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8; const TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8;
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);
// assert // assert
memStream.Position = 0; 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(); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
} }
@ -220,16 +227,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// arrange // arrange
var tiffEncoder = new TiffEncoder(); TiffEncoder tiffEncoder = new();
using Image<TPixel> input = provider.GetImage(); using Image<TPixel> input = provider.GetImage();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);
// assert // assert
memStream.Position = 0; 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); Assert.Equal(expectedCompression, output.Frames.RootFrame.Metadata.GetTiffMetadata().Compression);
} }
@ -242,16 +249,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// arrange // arrange
var tiffEncoder = new TiffEncoder(); TiffEncoder tiffEncoder = new();
using Image<TPixel> input = provider.GetImage(); using Image<TPixel> input = provider.GetImage();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, tiffEncoder); input.Save(memStream, tiffEncoder);
// assert // assert
memStream.Position = 0; 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(); TiffFrameMetadata frameMetadata = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedPredictor, frameMetadata.Predictor); Assert.Equal(expectedPredictor, frameMetadata.Predictor);
} }
@ -261,8 +268,8 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void TiffEncoder_WritesIfdOffsetAtWordBoundary() public void TiffEncoder_WritesIfdOffsetAtWordBoundary()
{ {
// arrange // arrange
var tiffEncoder = new TiffEncoder(); TiffEncoder tiffEncoder = new();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
using Image<Rgba32> image = new(1, 1); using Image<Rgba32> image = new(1, 1);
byte[] expectedIfdOffsetBytes = { 12, 0 }; byte[] expectedIfdOffsetBytes = { 12, 0 };
@ -286,16 +293,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// arrange // 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 Image<TPixel> input = provider.GetImage();
using var memStream = new MemoryStream(); using MemoryStream memStream = new();
// act // act
input.Save(memStream, encoder); input.Save(memStream, encoder);
// assert // assert
memStream.Position = 0; 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(); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit1, frameMetaData.BitsPerPixel); Assert.Equal(TiffBitsPerPixel.Bit1, frameMetaData.BitsPerPixel);
Assert.Equal(expectedCompression, frameMetaData.Compression); Assert.Equal(expectedCompression, frameMetaData.Compression);
@ -545,7 +552,8 @@ public class TiffEncoderTests : TiffEncoderBaseTester
provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200); provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200);
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
var encoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation }; TiffEncoder encoder = new()
{ PhotometricInterpretation = photometricInterpretation };
image.DebugSave(provider, encoder); 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.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -318,4 +319,94 @@ public class TiffMetadataTests
Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value); Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value);
Assert.Equal(exifProfileInput.Values.Count, encodedImageExifProfile.Values.Count); 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. // Licensed under the Six Labors Split License.
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using Castle.Core.Configuration;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -406,6 +408,43 @@ public class ParallelRowIteratorTests
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message); 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 struct TestRowIntervalOperation : IRowIntervalOperation
{ {
private readonly Action<RowInterval> action; 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 source = provider.GetImage();
using Image<TPixel> dest = new(source.GetConfiguration(), source.Width, source.Height); using Image<TPixel> dest = new(source.GetConfiguration(), source.Width, source.Height);
// Giphy.gif has 5 frames // Giphy.gif has 5 frames
ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 0); ImportFrameAs<Bgra32>(source.Frames, dest.Frames, 0);
ImportFrameAs<Argb32>(source.Frames, dest.Frames, 1); ImportFrameAs<Argb32>(source.Frames, dest.Frames, 1);
@ -289,7 +290,7 @@ public abstract partial class ImageFrameCollectionTests
// Drop the original empty root frame: // Drop the original empty root frame:
dest.Frames.RemoveFrame(0); dest.Frames.RemoveFrame(0);
dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif"); dest.DebugSave(provider, extension: "gif", appendSourceFileOrDescription: false);
dest.CompareToOriginal(provider); dest.CompareToOriginal(provider);
for (int i = 0; i < 5; i++) 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.DisposalMethod, bData.DisposalMethod);
Assert.Equal(aData.FrameDelay, bData.FrameDelay); 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.Formats.Gif;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile; using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile;
using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag; using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag;
@ -22,17 +23,17 @@ public class ImageFrameMetadataTests
const int colorTableLength = 128; const int colorTableLength = 128;
const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground; const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground;
var metaData = new ImageFrameMetadata(); ImageFrameMetadata metaData = new();
GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata(); GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata();
gifFrameMetadata.FrameDelay = frameDelay; gifFrameMetadata.FrameDelay = frameDelay;
gifFrameMetadata.ColorTableLength = colorTableLength; gifFrameMetadata.LocalColorTable = Enumerable.Repeat(Color.HotPink, colorTableLength).ToArray();
gifFrameMetadata.DisposalMethod = disposalMethod; gifFrameMetadata.DisposalMethod = disposalMethod;
var clone = new ImageFrameMetadata(metaData); ImageFrameMetadata clone = new(metaData);
GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata(); GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata();
Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay); Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay);
Assert.Equal(colorTableLength, cloneGifFrameMetadata.ColorTableLength); Assert.Equal(colorTableLength, cloneGifFrameMetadata.LocalColorTable.Value.Length);
Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod); Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod);
} }
@ -40,19 +41,19 @@ public class ImageFrameMetadataTests
public void CloneIsDeep() public void CloneIsDeep()
{ {
// arrange // arrange
var exifProfile = new ExifProfile(); ExifProfile exifProfile = new();
exifProfile.SetValue(ExifTag.Software, "UnitTest"); exifProfile.SetValue(ExifTag.Software, "UnitTest");
exifProfile.SetValue(ExifTag.Artist, "UnitTest"); exifProfile.SetValue(ExifTag.Artist, "UnitTest");
var xmpProfile = new XmpProfile(new byte[0]); XmpProfile xmpProfile = new(Array.Empty<byte>());
var iccProfile = new IccProfile() IccProfile iccProfile = new()
{ {
Header = new IccProfileHeader() Header = new IccProfileHeader()
{ {
CmmType = "Unittest" CmmType = "Unittest"
} }
}; };
var iptcProfile = new ImageSharp.Metadata.Profiles.Iptc.IptcProfile(); IptcProfile iptcProfile = new();
var metaData = new ImageFrameMetadata() ImageFrameMetadata metaData = new()
{ {
XmpProfile = xmpProfile, XmpProfile = xmpProfile,
ExifProfile = exifProfile, 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)] [WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
public void FullImage<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize) public void FullImage<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ => provider.RunValidatingProcessorTest(
provider.RunValidatingProcessorTest(
x => x =>
{ {
x.OilPaint(levels, brushSize); x.OilPaint(levels, brushSize);
@ -36,17 +35,21 @@ public class OilPaintTest
}, },
ImageComparer.TolerantPercentage(0.01F), ImageComparer.TolerantPercentage(0.01F),
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);
}
[Theory] [Theory]
[WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)] [WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(OilPaintValues), 100, 100, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(OilPaintValues), 100, 100, PixelTypes.Rgba32)]
public void InBox<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize) public void InBox<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ => provider.RunRectangleConstrainedValidatingProcessorTest(
provider.RunRectangleConstrainedValidatingProcessorTest(
(x, rect) => x.OilPaint(levels, brushSize, rect), (x, rect) => x.OilPaint(levels, brushSize, rect),
$"{levels}-{brushSize}", $"{levels}-{brushSize}",
ImageComparer.TolerantPercentage(0.01F)); 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_NotEnoughBytesA = "Jpg/issues/issue-2334-a.jpg";
public const string Issue2334_NotEnoughBytesB = "Jpg/issues/issue-2334-b.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 Issue2478_JFXX = "Jpg/issues/issue-2478-jfxx.jpg";
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";
public static class Fuzz 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_B = "Gif/issues/issue_2288_2.gif";
public const string Issue2288_C = "Gif/issues/issue_2288_3.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 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 }; 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>( public static IEnumerable<ImageSimilarityReport<TPixelA, TPixelB>> CompareImages<TPixelA, TPixelB>(
this ImageComparer comparer, this ImageComparer comparer,
Image<TPixelA> expected, Image<TPixelA> expected,
Image<TPixelB> actual) Image<TPixelB> actual,
Func<int, int, bool> predicate = null)
where TPixelA : unmanaged, IPixel<TPixelA> where TPixelA : unmanaged, IPixel<TPixelA>
where TPixelB : unmanaged, IPixel<TPixelB> 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++) 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]); ImageSimilarityReport<TPixelA, TPixelB> report = comparer.CompareImagesOrFrames(i, expected.Frames[i], actual.Frames[i]);
if (!report.IsEmpty) if (!report.IsEmpty)
{ {
@ -72,7 +91,8 @@ public static class ImageComparerExtensions
public static void VerifySimilarity<TPixelA, TPixelB>( public static void VerifySimilarity<TPixelA, TPixelB>(
this ImageComparer comparer, this ImageComparer comparer,
Image<TPixelA> expected, Image<TPixelA> expected,
Image<TPixelB> actual) Image<TPixelB> actual,
Func<int, int, bool> predicate = null)
where TPixelA : unmanaged, IPixel<TPixelA> where TPixelA : unmanaged, IPixel<TPixelA>
where TPixelB : unmanaged, IPixel<TPixelB> where TPixelB : unmanaged, IPixel<TPixelB>
{ {
@ -81,12 +101,25 @@ public static class ImageComparerExtensions
throw new ImageDimensionsMismatchException(expected.Size, actual.Size); 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!"); 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()) if (reports.Any())
{ {
throw new ImageDifferenceIsOverThresholdException(reports); throw new ImageDifferenceIsOverThresholdException(reports);

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

@ -184,7 +184,8 @@ public class ImagingTestCaseUtility
string extension = null, string extension = null,
object testOutputDetails = null, object testOutputDetails = null,
bool appendPixelTypeToFileName = true, bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true) bool appendSourceFileOrDescription = true,
Func<int, int, bool> predicate = null)
{ {
string baseDir = this.GetTestOutputFileName(string.Empty, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription); string baseDir = this.GetTestOutputFileName(string.Empty, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription);
@ -195,8 +196,12 @@ public class ImagingTestCaseUtility
for (int i = 0; i < frameCount; i++) for (int i = 0; i < frameCount; i++)
{ {
string filePath = $"{baseDir}/{i:D2}.{extension}"; if (predicate != null && !predicate(i, frameCount))
yield return filePath; {
continue;
}
yield return $"{baseDir}/{i:D2}.{extension}";
} }
} }
@ -205,27 +210,35 @@ public class ImagingTestCaseUtility
string extension = "png", string extension = "png",
IImageEncoder encoder = null, IImageEncoder encoder = null,
object testOutputDetails = null, object testOutputDetails = null,
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true,
Func<int, int, bool> predicate = null)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
encoder = encoder ?? TestEnvironment.GetReferenceEncoder($"foo.{extension}"); encoder ??= TestEnvironment.GetReferenceEncoder($"foo.{extension}");
string[] files = this.GetTestOutputFileNamesMultiFrame( string[] files = this.GetTestOutputFileNamesMultiFrame(
image.Frames.Count, image.Frames.Count,
extension, extension,
testOutputDetails, testOutputDetails,
appendPixelTypeToFileName).ToArray(); appendPixelTypeToFileName,
predicate: predicate).ToArray();
for (int i = 0; i < image.Frames.Count; i++) 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]; continue;
using (FileStream stream = File.OpenWrite(filePath))
{
frameImage.Save(stream, encoder);
}
} }
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; return files;
@ -236,20 +249,17 @@ public class ImagingTestCaseUtility
object testOutputDetails, object testOutputDetails,
bool appendPixelTypeToFileName, bool appendPixelTypeToFileName,
bool appendSourceFileOrDescription) bool appendSourceFileOrDescription)
{ => TestEnvironment.GetReferenceOutputFileName(
return TestEnvironment.GetReferenceOutputFileName(
this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription)); this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription));
}
public string[] GetReferenceOutputFileNamesMultiFrame( public string[] GetReferenceOutputFileNamesMultiFrame(
int frameCount, int frameCount,
string extension, string extension,
object testOutputDetails, object testOutputDetails,
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true,
{ Func<int, int, bool> predicate = null)
return this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails) => this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails, appendPixelTypeToFileName, predicate: predicate)
.Select(TestEnvironment.GetReferenceOutputFileName).ToArray(); .Select(TestEnvironment.GetReferenceOutputFileName).ToArray();
}
internal void Init(string typeName, string methodName, string outputSubfolderName) 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( provider.Utility.SaveTestOutputFile(
image, image,
extension, extension,
encoder: encoder,
testOutputDetails: testOutputDetails, testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName, appendPixelTypeToFileName: appendPixelTypeToFileName,
appendSourceFileOrDescription: appendSourceFileOrDescription, appendSourceFileOrDescription: appendSourceFileOrDescription);
encoder: encoder);
return image; return image;
} }
@ -107,7 +107,8 @@ public static class TestImageExtensions
ITestImageProvider provider, ITestImageProvider provider,
object testOutputDetails = null, object testOutputDetails = null,
string extension = "png", string extension = "png",
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true,
Func<int, int, bool> predicate = null)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (TestEnvironment.RunsWithCodeCoverage) if (TestEnvironment.RunsWithCodeCoverage)
@ -119,7 +120,8 @@ public static class TestImageExtensions
image, image,
extension, extension,
testOutputDetails: testOutputDetails, testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName); appendPixelTypeToFileName: appendPixelTypeToFileName,
predicate: predicate);
return image; return image;
} }
@ -237,7 +239,6 @@ public static class TestImageExtensions
ITestImageProvider provider, ITestImageProvider provider,
FormattableString testOutputDetails, FormattableString testOutputDetails,
string extension = "png", string extension = "png",
bool grayscale = false,
bool appendPixelTypeToFileName = true, bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true) bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -246,7 +247,6 @@ public static class TestImageExtensions
provider, provider,
(object)testOutputDetails, (object)testOutputDetails,
extension, extension,
grayscale,
appendPixelTypeToFileName, appendPixelTypeToFileName,
appendSourceFileOrDescription); appendSourceFileOrDescription);
@ -256,12 +256,11 @@ public static class TestImageExtensions
ITestImageProvider provider, ITestImageProvider provider,
object testOutputDetails = null, object testOutputDetails = null,
string extension = "png", string extension = "png",
bool grayscale = false,
bool appendPixelTypeToFileName = true, bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true) bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel> 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>( using (Image<TPixel> referenceImage = GetReferenceOutputImage<TPixel>(
provider, provider,
testOutputDetails, testOutputDetails,
@ -284,8 +283,8 @@ public static class TestImageExtensions
ImageComparer comparer, ImageComparer comparer,
object testOutputDetails = null, object testOutputDetails = null,
string extension = "png", string extension = "png",
bool grayscale = false, bool appendPixelTypeToFileName = true,
bool appendPixelTypeToFileName = true) Func<int, int, bool> predicate = null)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image<TPixel> referenceImage = GetReferenceOutputImageMultiFrame<TPixel>( using (Image<TPixel> referenceImage = GetReferenceOutputImageMultiFrame<TPixel>(
@ -293,9 +292,10 @@ public static class TestImageExtensions
image.Frames.Count, image.Frames.Count,
testOutputDetails, testOutputDetails,
extension, extension,
appendPixelTypeToFileName)) appendPixelTypeToFileName,
predicate: predicate))
{ {
comparer.VerifySimilarity(referenceImage, image); comparer.VerifySimilarity(referenceImage, image, predicate);
} }
return image; return image;
@ -332,16 +332,18 @@ public static class TestImageExtensions
int frameCount, int frameCount,
object testOutputDetails = null, object testOutputDetails = null,
string extension = "png", string extension = "png",
bool appendPixelTypeToFileName = true) bool appendPixelTypeToFileName = true,
Func<int, int, bool> predicate = null)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
string[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame( string[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame(
frameCount, frameCount,
extension, extension,
testOutputDetails, testOutputDetails,
appendPixelTypeToFileName); appendPixelTypeToFileName,
predicate);
var temporaryFrameImages = new List<Image<TPixel>>(); List<Image<TPixel>> temporaryFrameImages = new();
IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]); IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]);
@ -359,7 +361,7 @@ public static class TestImageExtensions
Image<TPixel> firstTemp = temporaryFrameImages[0]; 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) 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