Browse Source

Merge remote-tracking branch 'upstream/main'

pull/2511/head
James Jackson-South 3 years ago
parent
commit
5a711a8ea9
  1. 4
      .github/workflows/build-and-test.yml
  2. 2
      .github/workflows/code-coverage.yml
  3. 40
      src/ImageSharp/Advanced/AdvancedImageExtensions.cs
  4. 2
      src/ImageSharp/Advanced/IConfigurationProvider.cs
  5. 4
      src/ImageSharp/Color/Color.Conversions.cs
  6. 12
      src/ImageSharp/Color/Color.cs
  7. 27
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  8. 45
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  9. 8
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  10. 5
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  11. 70
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  12. 2
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  13. 612
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  14. 30
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  15. 20
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  16. 4
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  17. 9
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  18. 10
      src/ImageSharp/Formats/ImageDecoderUtilities.cs
  19. 4
      src/ImageSharp/Formats/ImageEncoder.cs
  20. 72
      src/ImageSharp/Formats/ImageExtensions.Save.cs
  21. 8
      src/ImageSharp/Formats/ImageExtensions.Save.tt
  22. 2
      src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs
  23. 2
      src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs
  24. 29
      src/ImageSharp/Formats/Pbm/BinaryDecoder.cs
  25. 41
      src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs
  26. 25
      src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
  27. 2
      src/ImageSharp/Formats/Pbm/PbmEncoder.cs
  28. 2
      src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
  29. 109
      src/ImageSharp/Formats/Pbm/PlainDecoder.cs
  30. 35
      src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
  31. 8
      src/ImageSharp/Formats/Png/PngDecoder.cs
  32. 103
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  33. 13
      src/ImageSharp/Formats/Png/PngEncoder.cs
  34. 89
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  35. 40
      src/ImageSharp/Formats/Png/PngMetadata.cs
  36. 64
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  37. 4
      src/ImageSharp/Formats/Qoi/QoiEncoder.cs
  38. 5
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  39. 3
      src/ImageSharp/Formats/QuantizingImageEncoder.cs
  40. 6
      src/ImageSharp/Formats/Tga/TgaEncoder.cs
  41. 2
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  42. 2
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
  43. 1
      src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
  44. 22
      src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
  45. 9
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  46. 12
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  47. 66
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  48. 21
      src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs
  49. 2
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  50. 14
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  51. 18
      src/ImageSharp/Formats/Webp/WebpDecoder.cs
  52. 18
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  53. 21
      src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs
  54. 2
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  55. 18
      src/ImageSharp/IO/BufferedReadStream.cs
  56. 13
      src/ImageSharp/Image.cs
  57. 12
      src/ImageSharp/ImageExtensions.cs
  58. 14
      src/ImageSharp/ImageFrame.cs
  59. 20
      src/ImageSharp/ImageFrameCollection{TPixel}.cs
  60. 22
      src/ImageSharp/ImageFrame{TPixel}.cs
  61. 4
      src/ImageSharp/ImageSharp.csproj
  62. 8
      src/ImageSharp/Image{TPixel}.cs
  63. 5
      src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
  64. 14
      src/ImageSharp/Memory/Buffer2D{T}.cs
  65. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
  66. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs
  67. 13
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs
  68. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs
  69. 5
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs
  70. 7
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs
  71. 4
      src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
  72. 2
      src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
  73. 12
      src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs
  74. 56
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  75. 15
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  76. 17
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
  77. 8
      src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs
  78. 12
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
  79. 2
      src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs
  80. 40
      tests/ImageSharp.Tests/Color/ColorTests.cs
  81. 39
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  82. 14
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  83. 44
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  84. 11
      tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs
  85. 15
      tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
  86. 21
      tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
  87. 7
      tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs
  88. 6
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  89. 42
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  90. 28
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  91. 76
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  92. 91
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  93. 19
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  94. 4
      tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs
  95. 15
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
  96. 2
      tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
  97. 6
      tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs
  98. 13
      tests/ImageSharp.Tests/Image/ImageTests.cs
  99. 7
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  100. 19
      tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs

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

@ -76,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
@ -172,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

40
src/ImageSharp/Advanced/AdvancedImageExtensions.cs

@ -27,11 +27,11 @@ public static class AdvancedImageExtensions
Guard.NotNull(filePath, nameof(filePath)); Guard.NotNull(filePath, nameof(filePath));
string ext = Path.GetExtension(filePath); string ext = Path.GetExtension(filePath);
if (!source.GetConfiguration().ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat? format)) if (!source.Configuration.ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat? format))
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}'. Registered encoders include:"); sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}'. Registered encoders include:");
foreach (IImageFormat fmt in source.GetConfiguration().ImageFormats) foreach (IImageFormat fmt in source.Configuration.ImageFormats)
{ {
sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", fmt.Name, string.Join(", ", fmt.FileExtensions), Environment.NewLine); sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", fmt.Name, string.Join(", ", fmt.FileExtensions), Environment.NewLine);
} }
@ -39,13 +39,13 @@ public static class AdvancedImageExtensions
throw new UnknownImageFormatException(sb.ToString()); throw new UnknownImageFormatException(sb.ToString());
} }
IImageEncoder? encoder = source.GetConfiguration().ImageFormatsManager.GetEncoder(format); IImageEncoder? encoder = source.Configuration.ImageFormatsManager.GetEncoder(format);
if (encoder is null) if (encoder is null)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:"); sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:");
foreach (KeyValuePair<IImageFormat, IImageEncoder> enc in source.GetConfiguration().ImageFormatsManager.ImageEncoders) foreach (KeyValuePair<IImageFormat, IImageEncoder> enc in source.Configuration.ImageFormatsManager.ImageEncoders)
{ {
sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine); sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine);
} }
@ -76,30 +76,6 @@ public static class AdvancedImageExtensions
public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default) public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default)
=> source.AcceptAsync(visitor, cancellationToken); => source.AcceptAsync(visitor, cancellationToken);
/// <summary>
/// Gets the configuration for the image.
/// </summary>
/// <param name="source">The source image.</param>
/// <returns>Returns the configuration.</returns>
public static Configuration GetConfiguration(this Image source)
=> GetConfiguration((IConfigurationProvider)source);
/// <summary>
/// Gets the configuration for the image frame.
/// </summary>
/// <param name="source">The source image.</param>
/// <returns>Returns the configuration.</returns>
public static Configuration GetConfiguration(this ImageFrame source)
=> GetConfiguration((IConfigurationProvider)source);
/// <summary>
/// Gets the configuration.
/// </summary>
/// <param name="source">The source image</param>
/// <returns>Returns the bounds of the image</returns>
private static Configuration GetConfiguration(IConfigurationProvider source)
=> source?.Configuration ?? Configuration.Default;
/// <summary> /// <summary>
/// Gets the representation of the pixels as a <see cref="IMemoryGroup{T}"/> containing the backing pixel data of the image /// Gets the representation of the pixels as a <see cref="IMemoryGroup{T}"/> containing the backing pixel data of the image
/// stored in row major order, as a list of contiguous <see cref="Memory{T}"/> blocks in the source image's pixel format. /// stored in row major order, as a list of contiguous <see cref="Memory{T}"/> blocks in the source image's pixel format.
@ -167,12 +143,4 @@ public static class AdvancedImageExtensions
return source.Frames.RootFrame.PixelBuffer.GetSafeRowMemory(rowIndex); return source.Frames.RootFrame.PixelBuffer.GetSafeRowMemory(rowIndex);
} }
/// <summary>
/// Gets the <see cref="MemoryAllocator"/> assigned to 'source'.
/// </summary>
/// <param name="source">The source image.</param>
/// <returns>Returns the configuration.</returns>
internal static MemoryAllocator GetMemoryAllocator(this IConfigurationProvider source)
=> GetConfiguration(source).MemoryAllocator;
} }

2
src/ImageSharp/Advanced/IConfigurationProvider.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Advanced;
/// <summary> /// <summary>
/// Defines the contract for objects that can provide access to configuration. /// Defines the contract for objects that can provide access to configuration.
/// </summary> /// </summary>
internal interface IConfigurationProvider public interface IConfigurationProvider
{ {
/// <summary> /// <summary>
/// Gets the configuration which allows altering default behaviour or extending the library. /// Gets the configuration which allows altering default behaviour or extending the library.

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)
{ {

12
src/ImageSharp/Color/Color.cs

@ -251,7 +251,17 @@ public readonly partial struct Color : IEquatable<Color>
/// </summary> /// </summary>
/// <returns>A hexadecimal string representation of the value.</returns> /// <returns>A hexadecimal string representation of the value.</returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public string ToHex() => this.data.ToRgba32().ToHex(); public string ToHex()
{
if (this.boxedHighPrecisionPixel is not null)
{
Rgba32 rgba = default;
this.boxedHighPrecisionPixel.ToRgba32(ref rgba);
return rgba.ToHex();
}
return this.data.ToRgba32().ToHex();
}
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() => this.ToHex(); public override string ToString() => this.ToHex();

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>

45
src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

@ -161,6 +161,11 @@ internal sealed class ZlibInflateStream : Stream
bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining); bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead; this.currentDataRemaining -= bytesToRead;
bytesRead = this.innerStream.Read(buffer, offset, bytesToRead); bytesRead = this.innerStream.Read(buffer, offset, bytesToRead);
if (bytesRead == 0)
{
return totalBytesRead;
}
totalBytesRead += bytesRead; totalBytesRead += bytesRead;
} }
@ -168,22 +173,13 @@ internal sealed class ZlibInflateStream : Stream
} }
/// <inheritdoc/> /// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
{
throw new NotSupportedException();
}
/// <inheritdoc/> /// <inheritdoc/>
public override void SetLength(long value) public override void SetLength(long value) => throw new NotSupportedException();
{
throw new NotSupportedException();
}
/// <inheritdoc/> /// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
{
throw new NotSupportedException();
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@ -246,22 +242,17 @@ internal sealed class ZlibInflateStream : Stream
// CINFO is not defined in this specification for CM not equal to 8. // CINFO is not defined in this specification for CM not equal to 8.
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}");
} }
else
{ return false;
return false;
}
} }
} }
else if (isCriticalChunk)
{
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}");
}
else else
{ {
if (isCriticalChunk) return false;
{
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}");
}
else
{
return false;
}
} }
// The preset dictionary. // The preset dictionary.
@ -270,7 +261,11 @@ internal sealed class ZlibInflateStream : Stream
{ {
// We don't need this for inflate so simply skip by the next four bytes. // We don't need this for inflate so simply skip by the next four bytes.
// https://tools.ietf.org/html/rfc1950#page-6 // https://tools.ietf.org/html/rfc1950#page-6
this.innerStream.Read(ChecksumBuffer, 0, 4); if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4)
{
return false;
}
this.currentDataRemaining -= 4; this.currentDataRemaining -= 4;
} }

8
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>
@ -26,7 +32,7 @@ public sealed class BmpEncoder : QuantizingImageEncoder
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
BmpEncoderCore encoder = new(this, image.GetMemoryAllocator()); BmpEncoderCore encoder = new(this, image.Configuration.MemoryAllocator);
encoder.Encode(image, stream, cancellationToken); encoder.Encode(image, stream, cancellationToken);
} }
} }

5
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;
} }
@ -118,7 +119,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
Configuration configuration = image.GetConfiguration(); Configuration configuration = image.Configuration;
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
BmpMetadata bmpMetadata = metadata.GetBmpMetadata(); BmpMetadata bmpMetadata = metadata.GetBmpMetadata();
this.bitsPerPixel ??= bmpMetadata.BitsPerPixel; this.bitsPerPixel ??= bmpMetadata.BitsPerPixel;

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;
} }
} }
} }

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

@ -18,7 +18,7 @@ public sealed class GifEncoder : QuantizingImageEncoder
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
GifEncoderCore encoder = new(image.GetConfiguration(), this); GifEncoderCore encoder = new(image.Configuration, this);
encoder.Encode(image, stream, cancellationToken); encoder.Encode(image, stream, cancellationToken);
} }
} }

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.Configuration, 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);
} }

10
src/ImageSharp/Formats/ImageDecoderUtilities.cs

@ -50,7 +50,8 @@ internal static class ImageDecoderUtilities
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken); // Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance.
BufferedReadStream bufferedReadStream = stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken);
try try
{ {
@ -64,6 +65,13 @@ internal static class ImageDecoderUtilities
{ {
throw; throw;
} }
finally
{
if (bufferedReadStream != stream)
{
bufferedReadStream.Dispose();
}
}
} }
private static InvalidImageContentException DefaultLargeImageExceptionFactory( private static InvalidImageContentException DefaultLargeImageExceptionFactory(

4
src/ImageSharp/Formats/ImageEncoder.cs

@ -42,7 +42,7 @@ public abstract class ImageEncoder : IImageEncoder
private void EncodeWithSeekableStream<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) private void EncodeWithSeekableStream<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Configuration configuration = image.GetConfiguration(); Configuration configuration = image.Configuration;
if (stream.CanSeek) if (stream.CanSeek)
{ {
this.Encode(image, stream, cancellationToken); this.Encode(image, stream, cancellationToken);
@ -59,7 +59,7 @@ public abstract class ImageEncoder : IImageEncoder
private async Task EncodeWithSeekableStreamAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) private async Task EncodeWithSeekableStreamAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Configuration configuration = image.GetConfiguration(); Configuration configuration = image.Configuration;
if (stream.CanSeek) if (stream.CanSeek)
{ {
await DoEncodeAsync(stream).ConfigureAwait(false); await DoEncodeAsync(stream).ConfigureAwait(false);

72
src/ImageSharp/Formats/ImageExtensions.Save.cs

@ -59,7 +59,7 @@ public static partial class ImageExtensions
public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) => public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Bmp format. /// Saves the image to the given stream with the Bmp format.
@ -73,7 +73,7 @@ public static partial class ImageExtensions
public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -106,7 +106,7 @@ public static partial class ImageExtensions
public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Bmp format. /// Saves the image to the given stream with the Bmp format.
@ -120,7 +120,7 @@ public static partial class ImageExtensions
public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -161,7 +161,7 @@ public static partial class ImageExtensions
public static void SaveAsGif(this Image source, string path, GifEncoder encoder) => public static void SaveAsGif(this Image source, string path, GifEncoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Gif format. /// Saves the image to the given stream with the Gif format.
@ -175,7 +175,7 @@ public static partial class ImageExtensions
public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -208,7 +208,7 @@ public static partial class ImageExtensions
public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Gif format. /// Saves the image to the given stream with the Gif format.
@ -222,7 +222,7 @@ public static partial class ImageExtensions
public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -263,7 +263,7 @@ public static partial class ImageExtensions
public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) => public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Jpeg format. /// Saves the image to the given stream with the Jpeg format.
@ -277,7 +277,7 @@ public static partial class ImageExtensions
public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -310,7 +310,7 @@ public static partial class ImageExtensions
public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Jpeg format. /// Saves the image to the given stream with the Jpeg format.
@ -324,7 +324,7 @@ public static partial class ImageExtensions
public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -365,7 +365,7 @@ public static partial class ImageExtensions
public static void SaveAsPbm(this Image source, string path, PbmEncoder encoder) => public static void SaveAsPbm(this Image source, string path, PbmEncoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Pbm format. /// Saves the image to the given stream with the Pbm format.
@ -379,7 +379,7 @@ public static partial class ImageExtensions
public static Task SaveAsPbmAsync(this Image source, string path, PbmEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsPbmAsync(this Image source, string path, PbmEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -412,7 +412,7 @@ public static partial class ImageExtensions
public static void SaveAsPbm(this Image source, Stream stream, PbmEncoder encoder) public static void SaveAsPbm(this Image source, Stream stream, PbmEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Pbm format. /// Saves the image to the given stream with the Pbm format.
@ -426,7 +426,7 @@ public static partial class ImageExtensions
public static Task SaveAsPbmAsync(this Image source, Stream stream, PbmEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsPbmAsync(this Image source, Stream stream, PbmEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -467,7 +467,7 @@ public static partial class ImageExtensions
public static void SaveAsPng(this Image source, string path, PngEncoder encoder) => public static void SaveAsPng(this Image source, string path, PngEncoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Png format. /// Saves the image to the given stream with the Png format.
@ -481,7 +481,7 @@ public static partial class ImageExtensions
public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -514,7 +514,7 @@ public static partial class ImageExtensions
public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Png format. /// Saves the image to the given stream with the Png format.
@ -528,7 +528,7 @@ public static partial class ImageExtensions
public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -569,7 +569,7 @@ public static partial class ImageExtensions
public static void SaveAsQoi(this Image source, string path, QoiEncoder encoder) => public static void SaveAsQoi(this Image source, string path, QoiEncoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Qoi format. /// Saves the image to the given stream with the Qoi format.
@ -583,7 +583,7 @@ public static partial class ImageExtensions
public static Task SaveAsQoiAsync(this Image source, string path, QoiEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsQoiAsync(this Image source, string path, QoiEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -616,7 +616,7 @@ public static partial class ImageExtensions
public static void SaveAsQoi(this Image source, Stream stream, QoiEncoder encoder) public static void SaveAsQoi(this Image source, Stream stream, QoiEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Qoi format. /// Saves the image to the given stream with the Qoi format.
@ -630,7 +630,7 @@ public static partial class ImageExtensions
public static Task SaveAsQoiAsync(this Image source, Stream stream, QoiEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsQoiAsync(this Image source, Stream stream, QoiEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -671,7 +671,7 @@ public static partial class ImageExtensions
public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) => public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Tga format. /// Saves the image to the given stream with the Tga format.
@ -685,7 +685,7 @@ public static partial class ImageExtensions
public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -718,7 +718,7 @@ public static partial class ImageExtensions
public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Tga format. /// Saves the image to the given stream with the Tga format.
@ -732,7 +732,7 @@ public static partial class ImageExtensions
public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -773,7 +773,7 @@ public static partial class ImageExtensions
public static void SaveAsTiff(this Image source, string path, TiffEncoder encoder) => public static void SaveAsTiff(this Image source, string path, TiffEncoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Tiff format. /// Saves the image to the given stream with the Tiff format.
@ -787,7 +787,7 @@ public static partial class ImageExtensions
public static Task SaveAsTiffAsync(this Image source, string path, TiffEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsTiffAsync(this Image source, string path, TiffEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -820,7 +820,7 @@ public static partial class ImageExtensions
public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder) public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Tiff format. /// Saves the image to the given stream with the Tiff format.
@ -834,7 +834,7 @@ public static partial class ImageExtensions
public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -875,7 +875,7 @@ public static partial class ImageExtensions
public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) => public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Webp format. /// Saves the image to the given stream with the Webp format.
@ -889,7 +889,7 @@ public static partial class ImageExtensions
public static Task SaveAsWebpAsync(this Image source, string path, WebpEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsWebpAsync(this Image source, string path, WebpEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -922,7 +922,7 @@ public static partial class ImageExtensions
public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder) public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the Webp format. /// Saves the image to the given stream with the Webp format.
@ -936,7 +936,7 @@ public static partial class ImageExtensions
public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder, CancellationToken cancellationToken = default) public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance),
cancellationToken); cancellationToken);
} }

8
src/ImageSharp/Formats/ImageExtensions.Save.tt

@ -78,7 +78,7 @@ public static partial class ImageExtensions
public static void SaveAs<#= fmt #>(this Image source, string path, <#= fmt #>Encoder encoder) => public static void SaveAs<#= fmt #>(this Image source, string path, <#= fmt #>Encoder encoder) =>
source.Save( source.Save(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the <#= fmt #> format. /// Saves the image to the given stream with the <#= fmt #> format.
@ -92,7 +92,7 @@ public static partial class ImageExtensions
public static Task SaveAs<#= fmt #>Async(this Image source, string path, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) public static Task SaveAs<#= fmt #>Async(this Image source, string path, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
path, path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance),
cancellationToken); cancellationToken);
/// <summary> /// <summary>
@ -125,7 +125,7 @@ public static partial class ImageExtensions
public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder) public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder)
=> source.Save( => source.Save(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance));
/// <summary> /// <summary>
/// Saves the image to the given stream with the <#= fmt #> format. /// Saves the image to the given stream with the <#= fmt #> format.
@ -139,7 +139,7 @@ public static partial class ImageExtensions
public static Task SaveAs<#= fmt #>Async(this Image source, Stream stream, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) public static Task SaveAs<#= fmt #>Async(this Image source, Stream stream, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync( => source.SaveAsync(
stream, stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance),
cancellationToken); cancellationToken);
<# <#

2
src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs

@ -20,7 +20,7 @@ internal sealed class JpegFrame : IDisposable
this.PixelWidth = image.Width; this.PixelWidth = image.Width;
this.PixelHeight = image.Height; this.PixelHeight = image.Height;
MemoryAllocator allocator = image.GetConfiguration().MemoryAllocator; MemoryAllocator allocator = image.Configuration.MemoryAllocator;
JpegComponentConfig[] componentConfigs = frameConfig.Components; JpegComponentConfig[] componentConfigs = frameConfig.Components;
this.Components = new Component[componentConfigs.Length]; this.Components = new Component[componentConfigs.Length];

2
src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs

@ -32,7 +32,7 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
public SpectralConverter(JpegFrame frame, Image<TPixel> image, Block8x8F[] dequantTables) public SpectralConverter(JpegFrame frame, Image<TPixel> image, Block8x8F[] dequantTables)
{ {
MemoryAllocator allocator = image.GetConfiguration().MemoryAllocator; MemoryAllocator allocator = image.Configuration.MemoryAllocator;
// iteration data // iteration data
int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width); int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width);

29
src/ImageSharp/Formats/Pbm/BinaryDecoder.cs

@ -71,7 +71,11 @@ internal class BinaryDecoder
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
stream.Read(rowSpan); if (stream.Read(rowSpan) < rowSpan.Length)
{
return;
}
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.FromL8Bytes( PixelOperations<TPixel>.Instance.FromL8Bytes(
configuration, configuration,
@ -93,7 +97,11 @@ internal class BinaryDecoder
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
stream.Read(rowSpan); if (stream.Read(rowSpan) < rowSpan.Length)
{
return;
}
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.FromL16Bytes( PixelOperations<TPixel>.Instance.FromL16Bytes(
configuration, configuration,
@ -115,7 +123,11 @@ internal class BinaryDecoder
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
stream.Read(rowSpan); if (stream.Read(rowSpan) < rowSpan.Length)
{
return;
}
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.FromRgb24Bytes( PixelOperations<TPixel>.Instance.FromRgb24Bytes(
configuration, configuration,
@ -137,7 +149,11 @@ internal class BinaryDecoder
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
stream.Read(rowSpan); if (stream.Read(rowSpan) < rowSpan.Length)
{
return;
}
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.FromRgb48Bytes( PixelOperations<TPixel>.Instance.FromRgb48Bytes(
configuration, configuration,
@ -161,6 +177,11 @@ internal class BinaryDecoder
for (int x = 0; x < width;) for (int x = 0; x < width;)
{ {
int raw = stream.ReadByte(); int raw = stream.ReadByte();
if (raw < 0)
{
return;
}
int stopBit = Math.Min(8, width - x); int stopBit = Math.Min(8, width - x);
for (int bit = 0; bit < stopBit; bit++) for (int bit = 0; bit < stopBit; bit++)
{ {

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

@ -11,14 +11,20 @@ namespace SixLabors.ImageSharp.Formats.Pbm;
internal static class BufferedReadStreamExtensions internal static class BufferedReadStreamExtensions
{ {
/// <summary> /// <summary>
/// Skip over any whitespace or any comments. /// Skip over any whitespace or any comments and signal if EOF has been reached.
/// </summary> /// </summary>
public static void SkipWhitespaceAndComments(this BufferedReadStream stream) /// <param name="stream">The buffered read stream.</param>
/// <returns><see langword="false"/> if EOF has been reached while reading the stream; see langword="true"/> otherwise.</returns>
public static bool SkipWhitespaceAndComments(this BufferedReadStream stream)
{ {
bool isWhitespace; bool isWhitespace;
do do
{ {
int val = stream.ReadByte(); int val = stream.ReadByte();
if (val < 0)
{
return false;
}
// Comments start with '#' and end at the next new-line. // Comments start with '#' and end at the next new-line.
if (val == 0x23) if (val == 0x23)
@ -27,8 +33,12 @@ internal static class BufferedReadStreamExtensions
do do
{ {
innerValue = stream.ReadByte(); innerValue = stream.ReadByte();
if (innerValue < 0)
{
return false;
}
} }
while (innerValue is not 0x0a and not -0x1); while (innerValue is not 0x0a);
// Continue searching for whitespace. // Continue searching for whitespace.
val = innerValue; val = innerValue;
@ -38,18 +48,31 @@ internal static class BufferedReadStreamExtensions
} }
while (isWhitespace); while (isWhitespace);
stream.Seek(-1, SeekOrigin.Current); stream.Seek(-1, SeekOrigin.Current);
return true;
} }
/// <summary> /// <summary>
/// Read a decimal text value. /// Read a decimal text value and signal if EOF has been reached.
/// </summary> /// </summary>
/// <returns>The integer value of the decimal.</returns> /// <param name="stream">The buffered read stream.</param>
public static int ReadDecimal(this BufferedReadStream stream) /// <param name="value">The read value.</param>
/// <returns><see langword="false"/> if EOF has been reached while reading the stream; <see langword="true"/> otherwise.</returns>
/// <remarks>
/// A 'false' return value doesn't mean that the parsing has been failed, since it's possible to reach EOF while reading the last decimal in the file.
/// It's up to the call site to handle such a situation.
/// </remarks>
public static bool ReadDecimal(this BufferedReadStream stream, out int value)
{ {
int value = 0; value = 0;
while (true) while (true)
{ {
int current = stream.ReadByte() - 0x30; int current = stream.ReadByte();
if (current < 0)
{
return false;
}
current -= 0x30;
if ((uint)current > 9) if ((uint)current > 9)
{ {
break; break;
@ -58,6 +81,6 @@ internal static class BufferedReadStreamExtensions
value = (value * 10) + current; value = (value * 10) + current;
} }
return value; return true;
} }
} }

25
src/ImageSharp/Formats/Pbm/PbmDecoderCore.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 System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
@ -95,6 +96,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
/// Processes the ppm header. /// Processes the ppm header.
/// </summary> /// </summary>
/// <param name="stream">The input stream.</param> /// <param name="stream">The input stream.</param>
/// <exception cref="InvalidImageContentException">An EOF marker has been read before the image has been decoded.</exception>
private void ProcessHeader(BufferedReadStream stream) private void ProcessHeader(BufferedReadStream stream)
{ {
Span<byte> buffer = stackalloc byte[2]; Span<byte> buffer = stackalloc byte[2];
@ -144,14 +146,22 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
throw new InvalidImageContentException("Unknown of not implemented image type encountered."); throw new InvalidImageContentException("Unknown of not implemented image type encountered.");
} }
stream.SkipWhitespaceAndComments(); if (!stream.SkipWhitespaceAndComments() ||
int width = stream.ReadDecimal(); !stream.ReadDecimal(out int width) ||
stream.SkipWhitespaceAndComments(); !stream.SkipWhitespaceAndComments() ||
int height = stream.ReadDecimal(); !stream.ReadDecimal(out int height) ||
stream.SkipWhitespaceAndComments(); !stream.SkipWhitespaceAndComments())
{
ThrowPrematureEof();
}
if (this.colorType != PbmColorType.BlackAndWhite) if (this.colorType != PbmColorType.BlackAndWhite)
{ {
this.maxPixelValue = stream.ReadDecimal(); if (!stream.ReadDecimal(out this.maxPixelValue))
{
ThrowPrematureEof();
}
if (this.maxPixelValue > 255) if (this.maxPixelValue > 255)
{ {
this.componentType = PbmComponentType.Short; this.componentType = PbmComponentType.Short;
@ -174,6 +184,9 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
meta.Encoding = this.encoding; meta.Encoding = this.encoding;
meta.ColorType = this.colorType; meta.ColorType = this.colorType;
meta.ComponentType = this.componentType; meta.ComponentType = this.componentType;
[DoesNotReturn]
static void ThrowPrematureEof() => throw new InvalidImageContentException("Reached EOF while reading the header.");
} }
private void ProcessPixels<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels) private void ProcessPixels<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels)

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

@ -49,7 +49,7 @@ public sealed class PbmEncoder : ImageEncoder
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
PbmEncoderCore encoder = new(image.GetConfiguration(), this); PbmEncoderCore encoder = new(image.Configuration, this);
encoder.Encode(image, stream, cancellationToken); encoder.Encode(image, stream, cancellationToken);
} }
} }

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

@ -78,7 +78,7 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals
private void SanitizeAndSetEncoderOptions<TPixel>(Image<TPixel> image) private void SanitizeAndSetEncoderOptions<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.configuration = image.GetConfiguration(); this.configuration = image.Configuration;
PbmMetadata metadata = image.Metadata.GetPbmMetadata(); PbmMetadata metadata = image.Metadata.GetPbmMetadata();
this.encoding = this.encoder.Encoding ?? metadata.Encoding; this.encoding = this.encoder.Encoding ?? metadata.Encoding;
this.colorType = this.encoder.ColorType ?? metadata.ColorType; this.colorType = this.encoder.ColorType ?? metadata.ColorType;

109
src/ImageSharp/Formats/Pbm/PlainDecoder.cs

@ -65,13 +65,18 @@ internal class PlainDecoder
using IMemoryOwner<L8> row = allocator.Allocate<L8>(width); using IMemoryOwner<L8> row = allocator.Allocate<L8>(width);
Span<L8> rowSpan = row.GetSpan(); Span<L8> rowSpan = row.GetSpan();
bool eofReached = false;
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
byte value = (byte)stream.ReadDecimal(); stream.ReadDecimal(out int value);
stream.SkipWhitespaceAndComments(); rowSpan[x] = new L8((byte)value);
rowSpan[x] = new L8(value); eofReached = !stream.SkipWhitespaceAndComments();
if (eofReached)
{
break;
}
} }
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
@ -79,6 +84,11 @@ internal class PlainDecoder
configuration, configuration,
rowSpan, rowSpan,
pixelSpan); pixelSpan);
if (eofReached)
{
return;
}
} }
} }
@ -91,13 +101,18 @@ internal class PlainDecoder
using IMemoryOwner<L16> row = allocator.Allocate<L16>(width); using IMemoryOwner<L16> row = allocator.Allocate<L16>(width);
Span<L16> rowSpan = row.GetSpan(); Span<L16> rowSpan = row.GetSpan();
bool eofReached = false;
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
ushort value = (ushort)stream.ReadDecimal(); stream.ReadDecimal(out int value);
stream.SkipWhitespaceAndComments(); rowSpan[x] = new L16((ushort)value);
rowSpan[x] = new L16(value); eofReached = !stream.SkipWhitespaceAndComments();
if (eofReached)
{
break;
}
} }
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
@ -105,6 +120,11 @@ internal class PlainDecoder
configuration, configuration,
rowSpan, rowSpan,
pixelSpan); pixelSpan);
if (eofReached)
{
return;
}
} }
} }
@ -117,17 +137,29 @@ internal class PlainDecoder
using IMemoryOwner<Rgb24> row = allocator.Allocate<Rgb24>(width); using IMemoryOwner<Rgb24> row = allocator.Allocate<Rgb24>(width);
Span<Rgb24> rowSpan = row.GetSpan(); Span<Rgb24> rowSpan = row.GetSpan();
bool eofReached = false;
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
byte red = (byte)stream.ReadDecimal(); if (!stream.ReadDecimal(out int red) ||
stream.SkipWhitespaceAndComments(); !stream.SkipWhitespaceAndComments() ||
byte green = (byte)stream.ReadDecimal(); !stream.ReadDecimal(out int green) ||
stream.SkipWhitespaceAndComments(); !stream.SkipWhitespaceAndComments())
byte blue = (byte)stream.ReadDecimal(); {
stream.SkipWhitespaceAndComments(); // Reached EOF before reading a full RGB value
rowSpan[x] = new Rgb24(red, green, blue); eofReached = true;
break;
}
stream.ReadDecimal(out int blue);
rowSpan[x] = new Rgb24((byte)red, (byte)green, (byte)blue);
eofReached = !stream.SkipWhitespaceAndComments();
if (eofReached)
{
break;
}
} }
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
@ -135,6 +167,11 @@ internal class PlainDecoder
configuration, configuration,
rowSpan, rowSpan,
pixelSpan); pixelSpan);
if (eofReached)
{
return;
}
} }
} }
@ -147,17 +184,29 @@ internal class PlainDecoder
using IMemoryOwner<Rgb48> row = allocator.Allocate<Rgb48>(width); using IMemoryOwner<Rgb48> row = allocator.Allocate<Rgb48>(width);
Span<Rgb48> rowSpan = row.GetSpan(); Span<Rgb48> rowSpan = row.GetSpan();
bool eofReached = false;
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
ushort red = (ushort)stream.ReadDecimal(); if (!stream.ReadDecimal(out int red) ||
stream.SkipWhitespaceAndComments(); !stream.SkipWhitespaceAndComments() ||
ushort green = (ushort)stream.ReadDecimal(); !stream.ReadDecimal(out int green) ||
stream.SkipWhitespaceAndComments(); !stream.SkipWhitespaceAndComments())
ushort blue = (ushort)stream.ReadDecimal(); {
stream.SkipWhitespaceAndComments(); // Reached EOF before reading a full RGB value
rowSpan[x] = new Rgb48(red, green, blue); eofReached = true;
break;
}
stream.ReadDecimal(out int blue);
rowSpan[x] = new Rgb48((ushort)red, (ushort)green, (ushort)blue);
eofReached = !stream.SkipWhitespaceAndComments();
if (eofReached)
{
break;
}
} }
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
@ -165,6 +214,11 @@ internal class PlainDecoder
configuration, configuration,
rowSpan, rowSpan,
pixelSpan); pixelSpan);
if (eofReached)
{
return;
}
} }
} }
@ -177,13 +231,19 @@ internal class PlainDecoder
using IMemoryOwner<L8> row = allocator.Allocate<L8>(width); using IMemoryOwner<L8> row = allocator.Allocate<L8>(width);
Span<L8> rowSpan = row.GetSpan(); Span<L8> rowSpan = row.GetSpan();
bool eofReached = false;
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
int value = stream.ReadDecimal(); stream.ReadDecimal(out int value);
stream.SkipWhitespaceAndComments();
rowSpan[x] = value == 0 ? White : Black; rowSpan[x] = value == 0 ? White : Black;
eofReached = !stream.SkipWhitespaceAndComments();
if (eofReached)
{
break;
}
} }
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
@ -191,6 +251,11 @@ internal class PlainDecoder
configuration, configuration,
rowSpan, rowSpan,
pixelSpan); pixelSpan);
if (eofReached)
{
return;
}
} }
} }
} }

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)
{ {

8
src/ImageSharp/Formats/Png/PngDecoder.cs

@ -61,24 +61,24 @@ public sealed class PngDecoder : ImageDecoder
case PngColorType.Grayscale: case PngColorType.Grayscale:
if (bits == PngBitDepth.Bit16) if (bits == PngBitDepth.Bit16)
{ {
return !meta.HasTransparency return !meta.TransparentColor.HasValue
? this.Decode<L16>(options, stream, cancellationToken) ? this.Decode<L16>(options, stream, cancellationToken)
: this.Decode<La32>(options, stream, cancellationToken); : this.Decode<La32>(options, stream, cancellationToken);
} }
return !meta.HasTransparency return !meta.TransparentColor.HasValue
? this.Decode<L8>(options, stream, cancellationToken) ? this.Decode<L8>(options, stream, cancellationToken)
: this.Decode<La16>(options, stream, cancellationToken); : this.Decode<La16>(options, stream, cancellationToken);
case PngColorType.Rgb: case PngColorType.Rgb:
if (bits == PngBitDepth.Bit16) if (bits == PngBitDepth.Bit16)
{ {
return !meta.HasTransparency return !meta.TransparentColor.HasValue
? this.Decode<Rgb48>(options, stream, cancellationToken) ? this.Decode<Rgb48>(options, stream, cancellationToken)
: this.Decode<Rgba64>(options, stream, cancellationToken); : this.Decode<Rgba64>(options, stream, cancellationToken);
} }
return !meta.HasTransparency return !meta.TransparentColor.HasValue
? this.Decode<Rgb24>(options, stream, cancellationToken) ? this.Decode<Rgb24>(options, stream, cancellationToken)
: this.Decode<Rgba32>(options, stream, cancellationToken); : this.Decode<Rgba32>(options, stream, cancellationToken);

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

@ -220,6 +220,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
if (image is null) if (image is null)
{ {
this.InitializeImage(metadata, lastFrameControl, out image); this.InitializeImage(metadata, lastFrameControl, out image);
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
} }
FrameControl frameControl = lastFrameControl ?? new(0, this.header.Width, this.header.Height, 0, 0, 0, 0, default, default); FrameControl frameControl = lastFrameControl ?? new(0, this.header.Width, this.header.Height, 0, 0, 0, 0, default, default);
@ -228,15 +231,11 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
lastFrameControl = null; lastFrameControl = null;
break; break;
case PngChunkType.Palette: case PngChunkType.Palette:
byte[] pal = new byte[chunk.Length]; this.palette = chunk.Data.GetSpan().ToArray();
chunk.Data.GetSpan().CopyTo(pal);
this.palette = pal;
break; break;
case PngChunkType.Transparency: case PngChunkType.Transparency:
byte[] alpha = new byte[chunk.Length]; this.paletteAlpha = chunk.Data.GetSpan().ToArray();
chunk.Data.GetSpan().CopyTo(alpha); this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata);
this.paletteAlpha = alpha;
this.AssignTransparentMarkers(alpha, pngMetadata);
break; break;
case PngChunkType.Text: case PngChunkType.Text:
this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan());
@ -375,12 +374,15 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.SkipChunkDataAndCrc(chunk); this.SkipChunkDataAndCrc(chunk);
break; break;
case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray();
break;
case PngChunkType.Transparency: case PngChunkType.Transparency:
byte[] alpha = new byte[chunk.Length]; this.paletteAlpha = chunk.Data.GetSpan().ToArray();
chunk.Data.GetSpan().CopyTo(alpha); this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata);
this.paletteAlpha = alpha;
this.AssignTransparentMarkers(alpha, pngMetadata);
// Spec says tRNS must be after PLTE so safe to exit.
if (this.colorMetadataOnly) if (this.colorMetadataOnly)
{ {
goto EOF; goto EOF;
@ -453,6 +455,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
PngThrowHelper.ThrowInvalidHeader(); PngThrowHelper.ThrowInvalidHeader();
} }
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata); return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata);
} }
finally finally
@ -892,9 +897,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
frameControl, frameControl,
scanlineSpan, scanlineSpan,
rowSpan, rowSpan,
pngMetadata.HasTransparency, pngMetadata.TransparentColor);
pngMetadata.TransparentL16.GetValueOrDefault(),
pngMetadata.TransparentL8.GetValueOrDefault());
break; break;
@ -914,8 +917,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
frameControl, frameControl,
scanlineSpan, scanlineSpan,
rowSpan, rowSpan,
this.palette, pngMetadata.ColorTable);
this.paletteAlpha);
break; break;
@ -927,9 +929,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
rowSpan, rowSpan,
this.bytesPerPixel, this.bytesPerPixel,
this.bytesPerSample, this.bytesPerSample,
pngMetadata.HasTransparency, pngMetadata.TransparentColor);
pngMetadata.TransparentRgb48.GetValueOrDefault(),
pngMetadata.TransparentRgb24.GetValueOrDefault());
break; break;
@ -989,9 +989,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
rowSpan, rowSpan,
(uint)pixelOffset, (uint)pixelOffset,
(uint)increment, (uint)increment,
pngMetadata.HasTransparency, pngMetadata.TransparentColor);
pngMetadata.TransparentL16.GetValueOrDefault(),
pngMetadata.TransparentL8.GetValueOrDefault());
break; break;
@ -1015,8 +1013,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
rowSpan, rowSpan,
(uint)pixelOffset, (uint)pixelOffset,
(uint)increment, (uint)increment,
this.palette, pngMetadata.ColorTable);
this.paletteAlpha);
break; break;
@ -1030,9 +1027,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
(uint)increment, (uint)increment,
this.bytesPerPixel, this.bytesPerPixel,
this.bytesPerSample, this.bytesPerSample,
pngMetadata.HasTransparency, pngMetadata.TransparentColor);
pngMetadata.TransparentRgb48.GetValueOrDefault(),
pngMetadata.TransparentRgb24.GetValueOrDefault());
break; break;
@ -1056,10 +1051,44 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
} }
} }
/// <summary>
/// Decodes and assigns the color palette to the metadata
/// </summary>
/// <param name="palette">The palette buffer.</param>
/// <param name="alpha">The alpha palette buffer.</param>
/// <param name="pngMetadata">The png metadata.</param>
private static void AssignColorPalette(ReadOnlySpan<byte> palette, ReadOnlySpan<byte> alpha, PngMetadata pngMetadata)
{
if (palette.Length == 0)
{
return;
}
Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf<Rgb24>()];
ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(palette);
for (int i = 0; i < colorTable.Length; i++)
{
colorTable[i] = new Color(rgbTable[i]);
}
if (alpha.Length > 0)
{
// The alpha chunk may contain as many transparency entries as there are palette entries
// (more than that would not make any sense) or as few as one.
for (int i = 0; i < alpha.Length; i++)
{
ref Color color = ref colorTable[i];
color = color.WithAlpha(alpha[i] / 255F);
}
}
pngMetadata.ColorTable = colorTable;
}
/// <summary> /// <summary>
/// Decodes and assigns marker colors that identify transparent pixels in non indexed images. /// Decodes and assigns marker colors that identify transparent pixels in non indexed images.
/// </summary> /// </summary>
/// <param name="alpha">The alpha tRNS array.</param> /// <param name="alpha">The alpha tRNS buffer.</param>
/// <param name="pngMetadata">The png metadata.</param> /// <param name="pngMetadata">The png metadata.</param>
private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha, PngMetadata pngMetadata) private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha, PngMetadata pngMetadata)
{ {
@ -1073,16 +1102,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2)); ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2));
ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2));
pngMetadata.TransparentRgb48 = new Rgb48(rc, gc, bc); pngMetadata.TransparentColor = new(new Rgb48(rc, gc, bc));
pngMetadata.HasTransparency = true;
return; return;
} }
byte r = ReadByteLittleEndian(alpha, 0); byte r = ReadByteLittleEndian(alpha, 0);
byte g = ReadByteLittleEndian(alpha, 2); byte g = ReadByteLittleEndian(alpha, 2);
byte b = ReadByteLittleEndian(alpha, 4); byte b = ReadByteLittleEndian(alpha, 4);
pngMetadata.TransparentRgb24 = new Rgb24(r, g, b); pngMetadata.TransparentColor = new(new Rgb24(r, g, b));
pngMetadata.HasTransparency = true;
} }
} }
else if (this.pngColorType == PngColorType.Grayscale) else if (this.pngColorType == PngColorType.Grayscale)
@ -1091,20 +1118,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{ {
if (this.header.BitDepth == 16) if (this.header.BitDepth == 16)
{ {
pngMetadata.TransparentL16 = new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])); pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])));
} }
else else
{ {
pngMetadata.TransparentL8 = new L8(ReadByteLittleEndian(alpha, 0)); pngMetadata.TransparentColor = Color.FromPixel(new L8(ReadByteLittleEndian(alpha, 0)));
} }
pngMetadata.HasTransparency = true;
} }
} }
else if (this.pngColorType == PngColorType.Palette && alpha.Length > 0)
{
pngMetadata.HasTransparency = true;
}
} }
/// <summary> /// <summary>
@ -1634,7 +1655,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
// If we're reading color metadata only we're only interested in the IHDR and tRNS chunks. // If we're reading color metadata only we're only interested in the IHDR and tRNS chunks.
// We can skip all other chunk data in the stream for better performance. // We can skip all other chunk data in the stream for better performance.
if (this.colorMetadataOnly && type is not PngChunkType.Header and not PngChunkType.Transparency) if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency && type != PngChunkType.Palette)
{ {
chunk = new PngChunk(length, type); chunk = new PngChunk(length, type);

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

@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable #nullable disable
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
@ -14,9 +14,12 @@ public class PngEncoder : QuantizingImageEncoder
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class. /// Initializes a new instance of the <see cref="PngEncoder"/> class.
/// </summary> /// </summary>
// We set the quantizer to null here to allow the underlying encoder to create a public PngEncoder()
// quantizer with options appropriate to the encoding bit depth.
public PngEncoder() => this.Quantizer = null; // Hack. TODO: Investigate means to fix/optimize the Wu quantizer.
// The Wu quantizer does not handle the default sampling strategy well for some larger images.
// It's expensive and the results are not better than the extensive strategy.
=> this.PixelSamplingStrategy = new ExtensivePixelSamplingStrategy();
/// <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).
@ -75,7 +78,7 @@ public class PngEncoder : QuantizingImageEncoder
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this); using PngEncoderCore encoder = new(image.Configuration, this);
encoder.Encode(image, stream, cancellationToken); encoder.Encode(image, stream, cancellationToken);
} }
} }

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

@ -5,7 +5,6 @@ using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
@ -115,13 +114,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PngEncoderCore" /> class. /// Initializes a new instance of the <see cref="PngEncoderCore" /> class.
/// </summary> /// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator" /> to use for buffer allocations.</param>
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
/// <param name="encoder">The encoder with options.</param> /// <param name="encoder">The encoder with options.</param>
public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoder encoder) public PngEncoderCore(Configuration configuration, PngEncoder encoder)
{ {
this.memoryAllocator = memoryAllocator;
this.configuration = configuration; this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder; this.encoder = encoder;
} }
@ -910,7 +908,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="pngMetadata">The image metadata.</param> /// <param name="pngMetadata">The image metadata.</param>
private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
{ {
if (!pngMetadata.HasTransparency) if (pngMetadata.TransparentColor is null)
{ {
return; return;
} }
@ -918,39 +916,40 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span<byte> alpha = this.chunkDataBuffer.Span; Span<byte> alpha = this.chunkDataBuffer.Span;
switch (pngMetadata.ColorType) switch (pngMetadata.ColorType)
{ {
case PngColorType.Rgb when pngMetadata.TransparentRgb48.HasValue && this.use16Bit: if (this.use16Bit)
Rgb48 rgb48 = pngMetadata.TransparentRgb48.Value; {
BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb48.R); Rgb48 rgb = pngMetadata.TransparentColor.Value.ToPixel<Rgb48>();
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb48.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb48.B); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
break; }
case PngColorType.Rgb: else
if (pngMetadata.TransparentRgb24.HasValue) {
{ alpha.Clear();
alpha.Clear(); Rgb24 rgb = pngMetadata.TransparentColor.Value.ToRgb24();
Rgb24 rgb24 = pngMetadata.TransparentRgb24.Value; alpha[1] = rgb.R;
alpha[1] = rgb24.R; alpha[3] = rgb.G;
alpha[3] = rgb24.G; alpha[5] = rgb.B;
alpha[5] = rgb24.B; this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); }
} }
else if (pngMetadata.ColorType == PngColorType.Grayscale)
break; {
case PngColorType.Grayscale when pngMetadata.TransparentL16.HasValue && this.use16Bit: if (this.use16Bit)
BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue); {
L16 l16 = pngMetadata.TransparentColor.Value.ToPixel<L16>();
BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
break; }
case PngColorType.Grayscale: else
if (pngMetadata.TransparentL8.HasValue) {
{ L8 l8 = pngMetadata.TransparentColor.Value.ToPixel<L8>();
alpha.Clear(); alpha.Clear();
alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; alpha[1] = l8.PackedValue;
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
} }
break;
} }
} }
@ -1350,12 +1349,24 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
} }
// Use the metadata to determine what quantization depth to use if no quantizer has been set. // Use the metadata to determine what quantization depth to use if no quantizer has been set.
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract IQuantizer quantizer = encoder.Quantizer;
IQuantizer quantizer = encoder.Quantizer if (quantizer is null)
?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); {
// TODO: Can APNG have per-frame color tables?
PngMetadata metadata = image.Metadata.GetPngMetadata();
if (metadata.ColorTable is not null)
{
// Use the provided palette in total. The caller is responsible for setting values.
quantizer = new PaletteQuantizer(metadata.ColorTable.Value);
}
else
{
quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
}
}
// Create quantized frame returning the palette and set the bit depth. // Create quantized frame returning the palette and set the bit depth.
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(frame.GetConfiguration()); using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(frame.Configuration);
frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); return frameQuantizer.QuantizeFrame(frame, frame.Bounds());

40
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -28,13 +28,14 @@ public class PngMetadata : IDeepCloneable
this.ColorType = other.ColorType; this.ColorType = other.ColorType;
this.Gamma = other.Gamma; this.Gamma = other.Gamma;
this.InterlaceMethod = other.InterlaceMethod; this.InterlaceMethod = other.InterlaceMethod;
this.HasTransparency = other.HasTransparency; this.TransparentColor = other.TransparentColor;
this.TransparentL8 = other.TransparentL8;
this.TransparentL16 = other.TransparentL16;
this.TransparentRgb24 = other.TransparentRgb24;
this.TransparentRgb48 = other.TransparentRgb48;
this.NumberPlays = other.NumberPlays; this.NumberPlays = other.NumberPlays;
if (other.ColorTable?.Length > 0)
{
this.ColorTable = other.ColorTable.Value.ToArray();
}
for (int i = 0; i < other.TextData.Count; i++) for (int i = 0; i < other.TextData.Count; i++)
{ {
this.TextData.Add(other.TextData[i]); this.TextData.Add(other.TextData[i]);
@ -63,33 +64,14 @@ public class PngMetadata : IDeepCloneable
public float Gamma { get; set; } public float Gamma { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Rgb24 transparent color. /// Gets or sets the color table, if any.
/// This represents any color in an 8 bit Rgb24 encoded png that should be transparent.
/// </summary>
public Rgb24? TransparentRgb24 { get; set; }
/// <summary>
/// Gets or sets the Rgb48 transparent color.
/// This represents any color in a 16 bit Rgb24 encoded png that should be transparent.
/// </summary>
public Rgb48? TransparentRgb48 { get; set; }
/// <summary>
/// Gets or sets the 8 bit grayscale transparent color.
/// This represents any color in an 8 bit grayscale encoded png that should be transparent.
/// </summary>
public L8? TransparentL8 { get; set; }
/// <summary>
/// Gets or sets the 16 bit grayscale transparent color.
/// This represents any color in a 16 bit grayscale encoded png that should be transparent.
/// </summary> /// </summary>
public L16? TransparentL16 { get; set; } public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the image contains a transparency chunk and markers were decoded. /// Gets or sets the transparent color used with non palette based images, if a transparency chunk and markers were decoded.
/// </summary> /// </summary>
public bool HasTransparency { get; set; } public Color? TransparentColor { get; set; }
/// <summary> /// <summary>
/// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks. /// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks.
@ -98,7 +80,7 @@ public class PngMetadata : IDeepCloneable
public IList<PngTextData> TextData { get; set; } = new List<PngTextData>(); public IList<PngTextData> TextData { get; set; } = new List<PngTextData>();
/// <summary> /// <summary>
/// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. TODO: RepeatCount!!
/// </summary> /// </summary>
public int NumberPlays { get; set; } public int NumberPlays { get; set; }

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

@ -42,9 +42,7 @@ internal static class PngScanlineProcessor
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
uint pixelOffset, uint pixelOffset,
uint increment, uint increment,
bool hasTrans, Color? transparentColor)
L16 luminance16Trans,
L8 luminanceTrans)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
uint offset = pixelOffset + (uint)frameControl.XOffset; uint offset = pixelOffset + (uint)frameControl.XOffset;
@ -53,7 +51,7 @@ internal static class PngScanlineProcessor
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(bitDepth) - 1); int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(bitDepth) - 1);
if (!hasTrans) if (transparentColor is null)
{ {
if (bitDepth == 16) if (bitDepth == 16)
{ {
@ -80,13 +78,14 @@ internal static class PngScanlineProcessor
if (bitDepth == 16) if (bitDepth == 16)
{ {
L16 transparent = transparentColor.Value.ToPixel<L16>();
La32 source = default; La32 source = default;
int o = 0; int o = 0;
for (nuint x = offset; x < frameControl.XLimit; x += increment, o += 2) for (nuint x = offset; x < frameControl.XLimit; x += increment, o += 2)
{ {
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.L = luminance; source.L = luminance;
source.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue; source.A = luminance.Equals(transparent.PackedValue) ? ushort.MinValue : ushort.MaxValue;
pixel.FromLa32(source); pixel.FromLa32(source);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -94,13 +93,14 @@ internal static class PngScanlineProcessor
} }
else else
{ {
byte transparent = (byte)(transparentColor.Value.ToPixel<L8>().PackedValue * scaleFactor);
La16 source = default; La16 source = default;
byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor);
for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++)
{ {
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
source.L = luminance; source.L = luminance;
source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue; source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue;
pixel.FromLa16(source); pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -197,7 +197,7 @@ internal static class PngScanlineProcessor
byte[] paletteAlpha) byte[] paletteAlpha)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (palette.IsEmpty) if (palette is null)
{ {
PngThrowHelper.ThrowMissingPalette(); PngThrowHelper.ThrowMissingPalette();
} }
@ -206,10 +206,9 @@ internal static class PngScanlineProcessor
TPixel pixel = default; TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(palette); ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
if (paletteAlpha?.Length > 0) for (nuint x = 0; x < (uint)header.Width; x++)
{ {
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it. // channel and we should try to read it.
@ -271,15 +270,14 @@ internal static class PngScanlineProcessor
uint increment, uint increment,
int bytesPerPixel, int bytesPerPixel,
int bytesPerSample, int bytesPerSample,
bool hasTrans, Color? transparentColor)
Rgb48 rgb48Trans,
Rgb24 rgb24Trans)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
uint offset = pixelOffset + (uint)frameControl.XOffset; uint offset = pixelOffset + (uint)frameControl.XOffset;
TPixel pixel = default; TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
bool hasTransparency = transparentColor is not null;
if (bitDepth == 16) if (bitDepth == 16)
{ {
@ -315,12 +313,48 @@ internal static class PngScanlineProcessor
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
} }
} }
else
{
Rgb24 rgb = default;
int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
{
rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample)));
pixel.FromRgb24(rgb);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return; return;
} }
if (hasTrans) if (header.BitDepth == 16)
{ {
Rgb48 transparent = transparentColor.Value.ToPixel<Rgb48>();
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue;
pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgb24 transparent = transparentColor.Value.ToPixel<Rgb24>();
Rgba32 rgba = default; Rgba32 rgba = default;
int o = 0; int o = 0;
for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel)
@ -328,7 +362,7 @@ internal static class PngScanlineProcessor
rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample)));
rgba.A = rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue; rgba.A = transparent.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue;
pixel.FromRgba32(rgba); pixel.FromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;

4
src/ImageSharp/Formats/Qoi/QoiEncoder.cs

@ -1,8 +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.Advanced;
namespace SixLabors.ImageSharp.Formats.Qoi; namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary> /// <summary>
@ -27,7 +25,7 @@ public class QoiEncoder : ImageEncoder
/// <inheritdoc /> /// <inheritdoc />
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
QoiEncoderCore encoder = new(this, image.GetMemoryAllocator(), image.GetConfiguration()); QoiEncoderCore encoder = new(this, image.Configuration);
encoder.Encode(image, stream, cancellationToken); encoder.Encode(image, stream, cancellationToken);
} }
} }

5
src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs

@ -33,13 +33,12 @@ internal class QoiEncoderCore : IImageEncoderInternals
/// Initializes a new instance of the <see cref="QoiEncoderCore"/> class. /// Initializes a new instance of the <see cref="QoiEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="encoder">The encoder with options.</param> /// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator" /> to use for buffer allocations.</param>
/// <param name="configuration">The configuration of the Encoder.</param> /// <param name="configuration">The configuration of the Encoder.</param>
public QoiEncoderCore(QoiEncoder encoder, MemoryAllocator memoryAllocator, Configuration configuration) public QoiEncoderCore(QoiEncoder encoder, Configuration configuration)
{ {
this.encoder = encoder; this.encoder = encoder;
this.memoryAllocator = memoryAllocator;
this.configuration = configuration; this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
} }
/// <inheritdoc /> /// <inheritdoc />

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/Tga/TgaEncoder.cs

@ -1,12 +1,10 @@
// 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.Advanced;
namespace SixLabors.ImageSharp.Formats.Tga; namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a targa truevision image. /// Image encoder for writing an image to a stream as a Targa true-vision image.
/// </summary> /// </summary>
public sealed class TgaEncoder : ImageEncoder public sealed class TgaEncoder : ImageEncoder
{ {
@ -23,7 +21,7 @@ public sealed class TgaEncoder : ImageEncoder
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
TgaEncoderCore encoder = new(this, image.GetMemoryAllocator()); TgaEncoderCore encoder = new(this, image.Configuration.MemoryAllocator);
encoder.Encode(image, stream, cancellationToken); encoder.Encode(image, stream, cancellationToken);
} }
} }

2
src/ImageSharp/Formats/Tga/TgaEncoderCore.cs

@ -112,7 +112,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
} }
else else
{ {
this.WriteImage(image.GetConfiguration(), stream, image.Frames.RootFrame); this.WriteImage(image.Configuration, stream, image.Frames.RootFrame);
} }
stream.Flush(); stream.Flush();

2
src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs

@ -32,7 +32,7 @@ internal class WebpTiffCompression : TiffBaseDecompressor
/// <inheritdoc/> /// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer, CancellationToken cancellationToken) protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer, CancellationToken cancellationToken)
{ {
using WebpDecoderCore decoder = new(this.options); using WebpDecoderCore decoder = new(new WebpDecoderOptions());
using Image<Rgb24> image = decoder.Decode<Rgb24>(stream, cancellationToken); using Image<Rgb24> image = decoder.Decode<Rgb24>(stream, cancellationToken);
CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer);
} }

1
src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs

@ -4,6 +4,7 @@
using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression; namespace SixLabors.ImageSharp.Formats.Tiff.Compression;

22
src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs

@ -40,7 +40,7 @@ internal class DirectoryReader
public IList<ExifProfile> Read() public IList<ExifProfile> Read()
{ {
this.ByteOrder = ReadByteOrder(this.stream); this.ByteOrder = ReadByteOrder(this.stream);
var headerReader = new HeaderReader(this.stream, this.ByteOrder); HeaderReader headerReader = new(this.stream, this.ByteOrder);
headerReader.ReadFileHeader(); headerReader.ReadFileHeader();
this.nextIfdOffset = headerReader.FirstIfdOffset; this.nextIfdOffset = headerReader.FirstIfdOffset;
@ -52,7 +52,12 @@ internal class DirectoryReader
private static ByteOrder ReadByteOrder(Stream stream) private static ByteOrder ReadByteOrder(Stream stream)
{ {
Span<byte> headerBytes = stackalloc byte[2]; Span<byte> headerBytes = stackalloc byte[2];
stream.Read(headerBytes);
if (stream.Read(headerBytes) != 2)
{
throw TiffThrowHelper.ThrowInvalidHeader();
}
if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian)
{ {
return ByteOrder.LittleEndian; return ByteOrder.LittleEndian;
@ -68,10 +73,10 @@ internal class DirectoryReader
private IList<ExifProfile> ReadIfds(bool isBigTiff) private IList<ExifProfile> ReadIfds(bool isBigTiff)
{ {
var readers = new List<EntryReader>(); List<EntryReader> readers = new();
while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length) while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length)
{ {
var reader = new EntryReader(this.stream, this.ByteOrder, this.allocator); EntryReader reader = new(this.stream, this.ByteOrder, this.allocator);
reader.ReadTags(isBigTiff, this.nextIfdOffset); reader.ReadTags(isBigTiff, this.nextIfdOffset);
if (reader.BigValues.Count > 0) if (reader.BigValues.Count > 0)
@ -85,6 +90,11 @@ internal class DirectoryReader
} }
} }
if (this.nextIfdOffset >= reader.NextIfdOffset && reader.NextIfdOffset != 0)
{
TiffThrowHelper.ThrowImageFormatException("TIFF image contains circular directory offsets");
}
this.nextIfdOffset = reader.NextIfdOffset; this.nextIfdOffset = reader.NextIfdOffset;
readers.Add(reader); readers.Add(reader);
@ -94,11 +104,11 @@ internal class DirectoryReader
} }
} }
var list = new List<ExifProfile>(readers.Count); List<ExifProfile> list = new(readers.Count);
foreach (EntryReader reader in readers) foreach (EntryReader reader in readers)
{ {
reader.ReadBigValues(); reader.ReadBigValues();
var profile = new ExifProfile(reader.Values, reader.InvalidTags); ExifProfile profile = new(reader.Values, reader.InvalidTags);
list.Add(profile); list.Add(profile);
} }

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

@ -1,9 +1,9 @@
// 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.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 +12,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>
@ -42,7 +47,7 @@ public class TiffEncoder : QuantizingImageEncoder
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
TiffEncoderCore encode = new(this, image.GetMemoryAllocator()); TiffEncoderCore encode = new(this, image.Configuration.MemoryAllocator);
encode.Encode(image, stream, cancellationToken); encode.Encode(image, stream, cancellationToken);
} }
} }

12
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;
@ -127,7 +128,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
this.configuration = image.GetConfiguration(); this.configuration = image.Configuration;
ImageFrameMetadata rootFrameMetaData = image.Frames.RootFrame.Metadata; ImageFrameMetadata rootFrameMetaData = image.Frames.RootFrame.Metadata;
TiffFrameMetadata rootFrameTiffMetaData = rootFrameMetaData.GetTiffMetadata(); TiffFrameMetadata rootFrameTiffMetaData = rootFrameMetaData.GetTiffMetadata();
@ -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)

21
src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Enum to decide how to handle the background color of the Animation chunk during decoding.
/// </summary>
public enum BackgroundColorHandling
{
/// <summary>
/// The background color of the ANIM chunk will be used to initialize the canvas to fill the unused space on the canvas around the frame.
/// Also, if AnimationDisposalMethod.Dispose is used, this color will be used to restore the canvas background.
/// </summary>
Standard = 0,
/// <summary>
/// The background color of the ANIM chunk is ignored and instead the canvas is initialized with transparent, BGRA(0, 0, 0, 0).
/// </summary>
Ignore = 1
}

2
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -502,7 +502,7 @@ internal class Vp8LEncoder : IDisposable
doNotCache = true; doNotCache = true;
// Go brute force on all transforms. // Go brute force on all transforms.
foreach (EntropyIx entropyIx in Enum.GetValues(typeof(EntropyIx)).Cast<EntropyIx>()) foreach (EntropyIx entropyIx in Enum.GetValues<EntropyIx>())
{ {
// We can only apply kPalette or kPaletteAndSpatial if we can indeed use a palette. // We can only apply kPalette or kPaletteAndSpatial if we can indeed use a palette.
if ((entropyIx != EntropyIx.Palette && entropyIx != EntropyIx.PaletteAndSpatial) || usePalette) if ((entropyIx != EntropyIx.Palette && entropyIx != EntropyIx.PaletteAndSpatial) || usePalette)

14
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -52,17 +52,24 @@ internal class WebpAnimationDecoder : IDisposable
/// </summary> /// </summary>
private IMemoryOwner<byte>? alphaData; private IMemoryOwner<byte>? alphaData;
/// <summary>
/// The flag to decide how to handle the background color in the Animation Chunk.
/// </summary>
private readonly BackgroundColorHandling backgroundColorHandling;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebpAnimationDecoder"/> class. /// Initializes a new instance of the <see cref="WebpAnimationDecoder"/> class.
/// </summary> /// </summary>
/// <param name="memoryAllocator">The memory allocator.</param> /// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="maxFrames">The maximum number of frames to decode. Inclusive.</param> /// <param name="maxFrames">The maximum number of frames to decode. Inclusive.</param>
public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames) /// <param name="backgroundColorHandling">The flag to decide how to handle the background color in the Animation Chunk.</param>
public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames, BackgroundColorHandling backgroundColorHandling)
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.configuration = configuration; this.configuration = configuration;
this.maxFrames = maxFrames; this.maxFrames = maxFrames;
this.backgroundColorHandling = backgroundColorHandling;
} }
/// <summary> /// <summary>
@ -94,7 +101,10 @@ internal class WebpAnimationDecoder : IDisposable
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Animation: case WebpChunkType.Animation:
uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, features.AnimationBackgroundColor!.Value); Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? new Color(new Bgra32(0, 0, 0, 0))
: features.AnimationBackgroundColor!.Value;
uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, backgroundColor);
remainingBytes -= (int)dataSize; remainingBytes -= (int)dataSize;
break; break;
case WebpChunkType.Xmp: case WebpChunkType.Xmp:

18
src/ImageSharp/Formats/Webp/WebpDecoder.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>
/// Image decoder for generating an image out of a webp stream. /// Image decoder for generating an image out of a webp stream.
/// </summary> /// </summary>
public sealed class WebpDecoder : ImageDecoder public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
{ {
private WebpDecoder() private WebpDecoder()
{ {
@ -25,25 +25,33 @@ public sealed class WebpDecoder : ImageDecoder
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
using WebpDecoderCore decoder = new(options); using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = options });
return decoder.Identify(options.Configuration, stream, cancellationToken); return decoder.Identify(options.Configuration, stream, cancellationToken);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken) protected override Image<TPixel> Decode<TPixel>(WebpDecoderOptions options, Stream stream, CancellationToken cancellationToken)
{ {
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
using WebpDecoderCore decoder = new(options); using WebpDecoderCore decoder = new(options);
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken); Image<TPixel> image = decoder.Decode<TPixel>(options.GeneralOptions.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image); ScaleToTargetSize(options.GeneralOptions, image);
return image; return image;
} }
/// <inheritdoc/>
protected override Image Decode(WebpDecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken); => this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options)
=> new() { GeneralOptions = options };
} }

18
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -48,16 +48,22 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// </summary> /// </summary>
private WebpImageInfo? webImageInfo; private WebpImageInfo? webImageInfo;
/// <summary>
/// The flag to decide how to handle the background color in the Animation Chunk.
/// </summary>
private BackgroundColorHandling backgroundColorHandling;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebpDecoderCore"/> class. /// Initializes a new instance of the <see cref="WebpDecoderCore"/> class.
/// </summary> /// </summary>
/// <param name="options">The decoder options.</param> /// <param name="options">The decoder options.</param>
public WebpDecoderCore(DecoderOptions options) public WebpDecoderCore(WebpDecoderOptions options)
{ {
this.Options = options; this.Options = options.GeneralOptions;
this.configuration = options.Configuration; this.backgroundColorHandling = options.BackgroundColorHandling;
this.skipMetadata = options.SkipMetadata; this.configuration = options.GeneralOptions.Configuration;
this.maxFrames = options.MaxFrames; this.skipMetadata = options.GeneralOptions.SkipMetadata;
this.maxFrames = options.GeneralOptions.MaxFrames;
this.memoryAllocator = this.configuration.MemoryAllocator; this.memoryAllocator = this.configuration.MemoryAllocator;
} }
@ -83,7 +89,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{ {
if (this.webImageInfo.Features is { Animation: true }) if (this.webImageInfo.Features is { Animation: true })
{ {
using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames); using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames, this.backgroundColorHandling);
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
} }

21
src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Configuration options for decoding webp images.
/// </summary>
public sealed class WebpDecoderOptions : ISpecializedDecoderOptions
{
/// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new();
/// <summary>
/// Gets the flag to decide how to handle the background color Animation Chunk.
/// The specification is vague on how to handle the background color of the animation chunk.
/// This option let's the user choose how to deal with it.
/// </summary>
/// <see href="https://developers.google.com/speed/webp/docs/riff_container#animation"/>
public BackgroundColorHandling BackgroundColorHandling { get; init; } = BackgroundColorHandling.Standard;
}

2
src/ImageSharp/Formats/Webp/WebpEncoder.cs

@ -82,7 +82,7 @@ public sealed class WebpEncoder : ImageEncoder
/// <inheritdoc/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {
WebpEncoderCore encoder = new(this, image.GetConfiguration()); WebpEncoderCore encoder = new(this, image.Configuration);
encoder.Encode(image, stream, cancellationToken); encoder.Encode(image, stream, cancellationToken);
} }
} }

18
src/ImageSharp/IO/BufferedReadStream.cs

@ -68,6 +68,11 @@ internal sealed class BufferedReadStream : Stream
this.readBufferIndex = int.MinValue; this.readBufferIndex = int.MinValue;
} }
/// <summary>
/// Gets the number indicating the EOF hits occured while reading from this instance.
/// </summary>
public int EofHitCount { get; private set; }
/// <summary> /// <summary>
/// Gets the size, in bytes, of the underlying buffer. /// Gets the size, in bytes, of the underlying buffer.
/// </summary> /// </summary>
@ -142,6 +147,7 @@ internal sealed class BufferedReadStream : Stream
{ {
if (this.readerPosition >= this.Length) if (this.readerPosition >= this.Length)
{ {
this.EofHitCount++;
return -1; return -1;
} }
@ -294,7 +300,7 @@ internal sealed class BufferedReadStream : Stream
this.readerPosition += n; this.readerPosition += n;
this.readBufferIndex += n; this.readBufferIndex += n;
this.CheckEof(n);
return n; return n;
} }
@ -352,6 +358,7 @@ internal sealed class BufferedReadStream : Stream
this.Position += n; this.Position += n;
this.CheckEof(n);
return n; return n;
} }
@ -418,4 +425,13 @@ internal sealed class BufferedReadStream : Stream
Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count);
} }
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CheckEof(int read)
{
if (read == 0)
{
this.EofHitCount++;
}
}
} }

13
src/ImageSharp/Image.cs

@ -17,7 +17,6 @@ namespace SixLabors.ImageSharp;
public abstract partial class Image : IDisposable, IConfigurationProvider public abstract partial class Image : IDisposable, IConfigurationProvider
{ {
private bool isDisposed; private bool isDisposed;
private readonly Configuration configuration;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Image"/> class. /// Initializes a new instance of the <see cref="Image"/> class.
@ -26,12 +25,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
/// <param name="pixelType">The pixel type information.</param> /// <param name="pixelType">The pixel type information.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="size">The size in px units.</param> /// <param name="size">The size in px units.</param>
protected Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetadata? metadata, Size size) protected Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetadata metadata, Size size)
{ {
this.configuration = configuration; this.Configuration = configuration;
this.PixelType = pixelType; this.PixelType = pixelType;
this.Size = size; this.Size = size;
this.Metadata = metadata ?? new ImageMetadata(); this.Metadata = metadata;
} }
/// <summary> /// <summary>
@ -45,7 +44,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
internal Image( internal Image(
Configuration configuration, Configuration configuration,
PixelTypeInfo pixelType, PixelTypeInfo pixelType,
ImageMetadata? metadata, ImageMetadata metadata,
int width, int width,
int height) int height)
: this(configuration, pixelType, metadata, new Size(width, height)) : this(configuration, pixelType, metadata, new Size(width, height))
@ -53,7 +52,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
} }
/// <inheritdoc/> /// <inheritdoc/>
Configuration IConfigurationProvider.Configuration => this.configuration; public Configuration Configuration { get; }
/// <summary> /// <summary>
/// Gets information about the image pixels. /// Gets information about the image pixels.
@ -147,7 +146,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
/// <typeparam name="TPixel2">The pixel format.</typeparam> /// <typeparam name="TPixel2">The pixel format.</typeparam>
/// <returns>The <see cref="Image{TPixel2}"/></returns> /// <returns>The <see cref="Image{TPixel2}"/></returns>
public Image<TPixel2> CloneAs<TPixel2>() public Image<TPixel2> CloneAs<TPixel2>()
where TPixel2 : unmanaged, IPixel<TPixel2> => this.CloneAs<TPixel2>(this.GetConfiguration()); where TPixel2 : unmanaged, IPixel<TPixel2> => this.CloneAs<TPixel2>(this.Configuration);
/// <summary> /// <summary>
/// Returns a copy of the image in the given pixel format. /// Returns a copy of the image in the given pixel format.

12
src/ImageSharp/ImageExtensions.cs

@ -47,7 +47,7 @@ public static partial class ImageExtensions
{ {
Guard.NotNull(path, nameof(path)); Guard.NotNull(path, nameof(path));
Guard.NotNull(encoder, nameof(encoder)); Guard.NotNull(encoder, nameof(encoder));
using Stream fs = source.GetConfiguration().FileSystem.Create(path); using Stream fs = source.Configuration.FileSystem.Create(path);
source.Save(fs, encoder); source.Save(fs, encoder);
} }
@ -70,7 +70,7 @@ public static partial class ImageExtensions
Guard.NotNull(path, nameof(path)); Guard.NotNull(path, nameof(path));
Guard.NotNull(encoder, nameof(encoder)); Guard.NotNull(encoder, nameof(encoder));
await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path); await using Stream fs = source.Configuration.FileSystem.CreateAsynchronous(path);
await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false); await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false);
} }
@ -94,14 +94,14 @@ public static partial class ImageExtensions
throw new NotSupportedException("Cannot write to the stream."); throw new NotSupportedException("Cannot write to the stream.");
} }
IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.GetEncoder(format); IImageEncoder encoder = source.Configuration.ImageFormatsManager.GetEncoder(format);
if (encoder is null) if (encoder is null)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:");
foreach (KeyValuePair<IImageFormat, IImageEncoder> val in source.GetConfiguration().ImageFormatsManager.ImageEncoders) foreach (KeyValuePair<IImageFormat, IImageEncoder> val in source.Configuration.ImageFormatsManager.ImageEncoders)
{ {
sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine);
} }
@ -138,14 +138,14 @@ public static partial class ImageExtensions
throw new NotSupportedException("Cannot write to the stream."); throw new NotSupportedException("Cannot write to the stream.");
} }
IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.GetEncoder(format); IImageEncoder encoder = source.Configuration.ImageFormatsManager.GetEncoder(format);
if (encoder is null) if (encoder is null)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:");
foreach (KeyValuePair<IImageFormat, IImageEncoder> val in source.GetConfiguration().ImageFormatsManager.ImageEncoders) foreach (KeyValuePair<IImageFormat, IImageEncoder> val in source.Configuration.ImageFormatsManager.ImageEncoders)
{ {
sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine);
} }

14
src/ImageSharp/ImageFrame.cs

@ -15,8 +15,6 @@ namespace SixLabors.ImageSharp;
/// </summary> /// </summary>
public abstract partial class ImageFrame : IConfigurationProvider, IDisposable public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
{ {
private readonly Configuration configuration;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageFrame"/> class. /// Initializes a new instance of the <see cref="ImageFrame"/> class.
/// </summary> /// </summary>
@ -26,10 +24,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
/// <param name="metadata">The <see cref="ImageFrameMetadata"/>.</param> /// <param name="metadata">The <see cref="ImageFrameMetadata"/>.</param>
protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata) protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata)
{ {
Guard.NotNull(configuration, nameof(configuration)); this.Configuration = configuration;
Guard.NotNull(metadata, nameof(metadata));
this.configuration = configuration ?? Configuration.Default;
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
this.Metadata = metadata; this.Metadata = metadata;
@ -51,19 +46,19 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
public ImageFrameMetadata Metadata { get; } public ImageFrameMetadata Metadata { get; }
/// <inheritdoc/> /// <inheritdoc/>
Configuration IConfigurationProvider.Configuration => this.configuration; public Configuration Configuration { get; }
/// <summary> /// <summary>
/// Gets the size of the frame. /// Gets the size of the frame.
/// </summary> /// </summary>
/// <returns>The <see cref="Size"/></returns> /// <returns>The <see cref="Size"/></returns>
public Size Size() => new Size(this.Width, this.Height); public Size Size() => new(this.Width, this.Height);
/// <summary> /// <summary>
/// Gets the bounds of the frame. /// Gets the bounds of the frame.
/// </summary> /// </summary>
/// <returns>The <see cref="Rectangle"/></returns> /// <returns>The <see cref="Rectangle"/></returns>
public Rectangle Bounds() => new Rectangle(0, 0, this.Width, this.Height); public Rectangle Bounds() => new(0, 0, this.Width, this.Height);
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
@ -84,6 +79,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
/// <summary> /// <summary>
/// Updates the size of the image frame. /// Updates the size of the image frame.
/// </summary> /// </summary>
/// <param name="size">The size.</param>
internal void UpdateSize(Size size) internal void UpdateSize(Size size)
{ {
this.Width = size.Width; this.Width = size.Width;

20
src/ImageSharp/ImageFrameCollection{TPixel}.cs

@ -24,7 +24,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
// Frames are already cloned within the caller // Frames are already cloned within the caller
this.frames.Add(new ImageFrame<TPixel>(parent.GetConfiguration(), width, height, backgroundColor)); this.frames.Add(new ImageFrame<TPixel>(parent.Configuration, width, height, backgroundColor));
} }
internal ImageFrameCollection(Image<TPixel> parent, int width, int height, MemoryGroup<TPixel> memorySource) internal ImageFrameCollection(Image<TPixel> parent, int width, int height, MemoryGroup<TPixel> memorySource)
@ -32,7 +32,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
// Frames are already cloned within the caller // Frames are already cloned within the caller
this.frames.Add(new ImageFrame<TPixel>(parent.GetConfiguration(), width, height, memorySource)); this.frames.Add(new ImageFrame<TPixel>(parent.Configuration, width, height, memorySource));
} }
internal ImageFrameCollection(Image<TPixel> parent, IEnumerable<ImageFrame<TPixel>> frames) internal ImageFrameCollection(Image<TPixel> parent, IEnumerable<ImageFrame<TPixel>> frames)
@ -138,7 +138,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
this.EnsureNotDisposed(); this.EnsureNotDisposed();
this.ValidateFrame(source); this.ValidateFrame(source);
ImageFrame<TPixel> clonedFrame = source.Clone(this.parent.GetConfiguration()); ImageFrame<TPixel> clonedFrame = source.Clone(this.parent.Configuration);
this.frames.Insert(index, clonedFrame); this.frames.Insert(index, clonedFrame);
return clonedFrame; return clonedFrame;
} }
@ -153,7 +153,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
this.EnsureNotDisposed(); this.EnsureNotDisposed();
this.ValidateFrame(source); this.ValidateFrame(source);
ImageFrame<TPixel> clonedFrame = source.Clone(this.parent.GetConfiguration()); ImageFrame<TPixel> clonedFrame = source.Clone(this.parent.Configuration);
this.frames.Add(clonedFrame); this.frames.Add(clonedFrame);
return clonedFrame; return clonedFrame;
} }
@ -169,7 +169,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
this.EnsureNotDisposed(); this.EnsureNotDisposed();
ImageFrame<TPixel> frame = ImageFrame.LoadPixelData( ImageFrame<TPixel> frame = ImageFrame.LoadPixelData(
this.parent.GetConfiguration(), this.parent.Configuration,
source, source,
this.RootFrame.Width, this.RootFrame.Width,
this.RootFrame.Height); this.RootFrame.Height);
@ -270,7 +270,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
this.frames.Remove(frame); this.frames.Remove(frame);
return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.Metadata.DeepClone(), new[] { frame }); return new Image<TPixel>(this.parent.Configuration, this.parent.Metadata.DeepClone(), new[] { frame });
} }
/// <summary> /// <summary>
@ -285,7 +285,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
ImageFrame<TPixel> frame = this[index]; ImageFrame<TPixel> frame = this[index];
ImageFrame<TPixel> clonedFrame = frame.Clone(); ImageFrame<TPixel> clonedFrame = frame.Clone();
return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.Metadata.DeepClone(), new[] { clonedFrame }); return new Image<TPixel>(this.parent.Configuration, this.parent.Metadata.DeepClone(), new[] { clonedFrame });
} }
/// <summary> /// <summary>
@ -299,7 +299,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
this.EnsureNotDisposed(); this.EnsureNotDisposed();
ImageFrame<TPixel> frame = new( ImageFrame<TPixel> frame = new(
this.parent.GetConfiguration(), this.parent.Configuration,
this.RootFrame.Width, this.RootFrame.Width,
this.RootFrame.Height); this.RootFrame.Height);
this.frames.Add(frame); this.frames.Add(frame);
@ -365,7 +365,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
public ImageFrame<TPixel> CreateFrame(TPixel backgroundColor) public ImageFrame<TPixel> CreateFrame(TPixel backgroundColor)
{ {
ImageFrame<TPixel> frame = new( ImageFrame<TPixel> frame = new(
this.parent.GetConfiguration(), this.parent.Configuration,
this.RootFrame.Width, this.RootFrame.Width,
this.RootFrame.Height, this.RootFrame.Height,
backgroundColor); backgroundColor);
@ -414,7 +414,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
private ImageFrame<TPixel> CopyNonCompatibleFrame(ImageFrame source) private ImageFrame<TPixel> CopyNonCompatibleFrame(ImageFrame source)
{ {
ImageFrame<TPixel> result = new( ImageFrame<TPixel> result = new(
this.parent.GetConfiguration(), this.parent.Configuration,
source.Size(), source.Size(),
source.Metadata.DeepClone()); source.Metadata.DeepClone());
source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup); source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup);

22
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>
@ -56,7 +66,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height)); Guard.MustBeGreaterThan(height, 0, nameof(height));
this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D<TPixel>( this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(
width, width,
height, height,
configuration.PreferContiguousImageBuffers, configuration.PreferContiguousImageBuffers,
@ -89,7 +99,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height)); Guard.MustBeGreaterThan(height, 0, nameof(height));
this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D<TPixel>( this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(
width, width,
height, height,
configuration.PreferContiguousImageBuffers); configuration.PreferContiguousImageBuffers);
@ -136,7 +146,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(source, nameof(source)); Guard.NotNull(source, nameof(source));
this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D<TPixel>( this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(
source.PixelBuffer.Width, source.PixelBuffer.Width,
source.PixelBuffer.Height, source.PixelBuffer.Height,
configuration.PreferContiguousImageBuffers); configuration.PreferContiguousImageBuffers);
@ -361,7 +371,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
} }
this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d)
=> PixelOperations<TPixel>.Instance.To(this.GetConfiguration(), s, d)); => PixelOperations<TPixel>.Instance.To(this.Configuration, s, d));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -371,7 +381,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// Clones the current instance. /// Clones the current instance.
/// </summary> /// </summary>
/// <returns>The <see cref="ImageFrame{TPixel}"/></returns> /// <returns>The <see cref="ImageFrame{TPixel}"/></returns>
internal ImageFrame<TPixel> Clone() => this.Clone(this.GetConfiguration()); internal ImageFrame<TPixel> Clone() => this.Clone(this.Configuration);
/// <summary> /// <summary>
/// Clones the current instance. /// Clones the current instance.
@ -386,7 +396,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// <typeparam name="TPixel2">The pixel format.</typeparam> /// <typeparam name="TPixel2">The pixel format.</typeparam>
/// <returns>The <see cref="ImageFrame{TPixel2}"/></returns> /// <returns>The <see cref="ImageFrame{TPixel2}"/></returns>
internal ImageFrame<TPixel2>? CloneAs<TPixel2>() internal ImageFrame<TPixel2>? CloneAs<TPixel2>()
where TPixel2 : unmanaged, IPixel<TPixel2> => this.CloneAs<TPixel2>(this.GetConfiguration()); where TPixel2 : unmanaged, IPixel<TPixel2> => this.CloneAs<TPixel2>(this.Configuration);
/// <summary> /// <summary>
/// Returns a copy of the image frame in the given pixel format. /// Returns a copy of the image frame in the given pixel format.

4
src/ImageSharp/ImageSharp.csproj

@ -22,8 +22,8 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<!--Bump to V3 prior to tagged release.--> <!--Bump to v3.1 prior to tagged release.-->
<MinVerMinimumMajorMinor>3.0</MinVerMinimumMajorMinor> <MinVerMinimumMajorMinor>3.1</MinVerMinimumMajorMinor>
</PropertyGroup> </PropertyGroup>
<Choose> <Choose>

8
src/ImageSharp/Image{TPixel}.cs

@ -78,12 +78,12 @@ public sealed class Image<TPixel> : Image
/// <param name="height">The height of the image in pixels.</param> /// <param name="height">The height of the image in pixels.</param>
/// <param name="metadata">The images metadata.</param> /// <param name="metadata">The images metadata.</param>
internal Image(Configuration configuration, int width, int height, ImageMetadata? metadata) internal Image(Configuration configuration, int width, int height, ImageMetadata? metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height) : base(configuration, PixelTypeInfo.Create<TPixel>(), metadata ?? new(), width, height)
=> this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel)); => this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class /// Initializes a new instance of the <see cref="Image{TPixel}"/> class
/// wrapping an external <see cref="Buffer2D{TPixel}"/> pixel bufferx. /// wrapping an external <see cref="Buffer2D{TPixel}"/> pixel buffer.
/// </summary> /// </summary>
/// <param name="configuration">The configuration providing initialization code which allows extending the library.</param> /// <param name="configuration">The configuration providing initialization code which allows extending the library.</param>
/// <param name="pixelBuffer">Pixel buffer.</param> /// <param name="pixelBuffer">Pixel buffer.</param>
@ -129,7 +129,7 @@ public sealed class Image<TPixel> : Image
int height, int height,
TPixel backgroundColor, TPixel backgroundColor,
ImageMetadata? metadata) ImageMetadata? metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height) : base(configuration, PixelTypeInfo.Create<TPixel>(), metadata ?? new(), width, height)
=> this.frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor); => this.frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor);
/// <summary> /// <summary>
@ -328,7 +328,7 @@ public sealed class Image<TPixel> : Image
/// Clones the current image /// Clones the current image
/// </summary> /// </summary>
/// <returns>Returns a new image with all the same metadata as the original.</returns> /// <returns>Returns a new image with all the same metadata as the original.</returns>
public Image<TPixel> Clone() => this.Clone(this.GetConfiguration()); public Image<TPixel> Clone() => this.Clone(this.Configuration);
/// <summary> /// <summary>
/// Clones the current image with the given configuration. /// Clones the current image with the given configuration.

5
src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs

@ -117,6 +117,11 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
AllocationOptions options = AllocationOptions.None) AllocationOptions options = AllocationOptions.None)
{ {
long totalLengthInBytes = totalLength * Unsafe.SizeOf<T>(); long totalLengthInBytes = totalLength * Unsafe.SizeOf<T>();
if (totalLengthInBytes < 0)
{
throw new InvalidMemoryOperationException("Attempted to allocate a MemoryGroup of a size that is not representable.");
}
if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes) if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{ {
var buffer = new SharedArrayPoolBuffer<T>((int)totalLength); var buffer = new SharedArrayPoolBuffer<T>((int)totalLength);

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

@ -9,9 +9,6 @@ namespace SixLabors.ImageSharp.Memory;
/// Represents a buffer of value type objects /// Represents a buffer of value type objects
/// interpreted as a 2D region of <see cref="Width"/> x <see cref="Height"/> elements. /// interpreted as a 2D region of <see cref="Width"/> x <see cref="Height"/> elements.
/// </summary> /// </summary>
/// <remarks>
/// Before RC1, this class might be target of API changes, use it on your own risk!
/// </remarks>
/// <typeparam name="T">The value type.</typeparam> /// <typeparam name="T">The value type.</typeparam>
public sealed class Buffer2D<T> : IDisposable public sealed class Buffer2D<T> : IDisposable
where T : struct where T : struct
@ -173,13 +170,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 +200,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}");
} }

5
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs

@ -56,11 +56,6 @@ public abstract partial class ExifTag
/// </summary> /// </summary>
public static ExifTag<uint[]> IntergraphRegisters { get; } = new ExifTag<uint[]>(ExifTagValue.IntergraphRegisters); public static ExifTag<uint[]> IntergraphRegisters { get; } = new ExifTag<uint[]>(ExifTagValue.IntergraphRegisters);
/// <summary>
/// Gets the TimeZoneOffset exif tag.
/// </summary>
public static ExifTag<uint[]> TimeZoneOffset { get; } = new ExifTag<uint[]>(ExifTagValue.TimeZoneOffset);
/// <summary> /// <summary>
/// Gets the offset to child IFDs exif tag. /// Gets the offset to child IFDs exif tag.
/// </summary> /// </summary>

5
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs

@ -165,4 +165,9 @@ public abstract partial class ExifTag
/// Gets the GPSDestDistance exif tag. /// Gets the GPSDestDistance exif tag.
/// </summary> /// </summary>
public static ExifTag<Rational> GPSDestDistance { get; } = new ExifTag<Rational>(ExifTagValue.GPSDestDistance); public static ExifTag<Rational> GPSDestDistance { get; } = new ExifTag<Rational>(ExifTagValue.GPSDestDistance);
/// <summary>
/// Gets the GPSHPositioningError exif tag.
/// </summary>
public static ExifTag<Rational> GPSHPositioningError { get; } = new ExifTag<Rational>(ExifTagValue.GPSHPositioningError);
} }

13
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs

@ -0,0 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
/// <content/>
public abstract partial class ExifTag
{
/// <summary>
/// Gets the TimeZoneOffset exif tag.
/// </summary>
public static ExifTag<short[]> TimeZoneOffset { get; } = new ExifTag<short[]>(ExifTagValue.TimeZoneOffset);
}

5
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs

@ -1691,6 +1691,11 @@ internal enum ExifTagValue
/// </summary> /// </summary>
GPSDifferential = 0x001E, GPSDifferential = 0x001E,
/// <summary>
/// GPSHPositioningError
/// </summary>
GPSHPositioningError = 0x001F,
/// <summary> /// <summary>
/// Used in the Oce scanning process. /// Used in the Oce scanning process.
/// Identifies the scanticket used in the scanning process. /// Identifies the scanticket used in the scanning process.

5
src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs

@ -5,6 +5,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
internal sealed class ExifSignedShortArray : ExifArrayValue<short> internal sealed class ExifSignedShortArray : ExifArrayValue<short>
{ {
public ExifSignedShortArray(ExifTag<short[]> tag)
: base(tag)
{
}
public ExifSignedShortArray(ExifTagValue tag) public ExifSignedShortArray(ExifTagValue tag)
: base(tag) : base(tag)
{ {

7
src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs

@ -144,8 +144,6 @@ internal static partial class ExifValues
return new ExifLongArray(ExifTag.StripRowCounts); return new ExifLongArray(ExifTag.StripRowCounts);
case ExifTagValue.IntergraphRegisters: case ExifTagValue.IntergraphRegisters:
return new ExifLongArray(ExifTag.IntergraphRegisters); return new ExifLongArray(ExifTag.IntergraphRegisters);
case ExifTagValue.TimeZoneOffset:
return new ExifLongArray(ExifTag.TimeZoneOffset);
case ExifTagValue.SubIFDs: case ExifTagValue.SubIFDs:
return new ExifLongArray(ExifTag.SubIFDs); return new ExifLongArray(ExifTag.SubIFDs);
@ -243,6 +241,8 @@ internal static partial class ExifValues
return new ExifRational(ExifTag.GPSDestBearing); return new ExifRational(ExifTag.GPSDestBearing);
case ExifTagValue.GPSDestDistance: case ExifTagValue.GPSDestDistance:
return new ExifRational(ExifTag.GPSDestDistance); return new ExifRational(ExifTag.GPSDestDistance);
case ExifTagValue.GPSHPositioningError:
return new ExifRational(ExifTag.GPSHPositioningError);
case ExifTagValue.WhitePoint: case ExifTagValue.WhitePoint:
return new ExifRationalArray(ExifTag.WhitePoint); return new ExifRationalArray(ExifTag.WhitePoint);
@ -417,6 +417,9 @@ internal static partial class ExifValues
case ExifTagValue.Decode: case ExifTagValue.Decode:
return new ExifSignedRationalArray(ExifTag.Decode); return new ExifSignedRationalArray(ExifTag.Decode);
case ExifTagValue.TimeZoneOffset:
return new ExifSignedShortArray(ExifTag.TimeZoneOffset);
case ExifTagValue.ImageDescription: case ExifTagValue.ImageDescription:
return new ExifString(ExifTag.ImageDescription); return new ExifString(ExifTag.ImageDescription);
case ExifTagValue.Make: case ExifTagValue.Make:

4
src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs

@ -88,9 +88,9 @@ internal sealed class IccReader
foreach (IccTagTableEntry tag in tagTable) foreach (IccTagTableEntry tag in tagTable)
{ {
IccTagDataEntry entry; IccTagDataEntry entry;
if (store.ContainsKey(tag.Offset)) if (store.TryGetValue(tag.Offset, out IccTagDataEntry? value))
{ {
entry = store[tag.Offset]; entry = value;
} }
else else
{ {

2
src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs

@ -54,7 +54,7 @@ public static partial class ProcessingExtensions
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this ImageFrame<TPixel> source, Rectangle bounds) public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this ImageFrame<TPixel> source, Rectangle bounds)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Configuration configuration = source.GetConfiguration(); Configuration configuration = source.Configuration;
var interest = Rectangle.Intersect(bounds, source.Bounds()); var interest = Rectangle.Intersect(bounds, source.Bounds());
int startY = interest.Y; int startY = interest.Y;

12
src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs

@ -22,7 +22,7 @@ public static partial class ProcessingExtensions
/// <exception cref="ObjectDisposedException">The source has been disposed.</exception> /// <exception cref="ObjectDisposedException">The source has been disposed.</exception>
/// <exception cref="ImageProcessingException">The processing operation failed.</exception> /// <exception cref="ImageProcessingException">The processing operation failed.</exception>
public static void Mutate(this Image source, Action<IImageProcessingContext> operation) public static void Mutate(this Image source, Action<IImageProcessingContext> operation)
=> Mutate(source, source.GetConfiguration(), operation); => Mutate(source, source.Configuration, operation);
/// <summary> /// <summary>
/// Mutates the source image by applying the image operation to it. /// Mutates the source image by applying the image operation to it.
@ -57,7 +57,7 @@ public static partial class ProcessingExtensions
/// <exception cref="ImageProcessingException">The processing operation failed.</exception> /// <exception cref="ImageProcessingException">The processing operation failed.</exception>
public static void Mutate<TPixel>(this Image<TPixel> source, Action<IImageProcessingContext> operation) public static void Mutate<TPixel>(this Image<TPixel> source, Action<IImageProcessingContext> operation)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> Mutate(source, source.GetConfiguration(), operation); => Mutate(source, source.Configuration, operation);
/// <summary> /// <summary>
/// Mutates the source image by applying the image operation to it. /// Mutates the source image by applying the image operation to it.
@ -97,7 +97,7 @@ public static partial class ProcessingExtensions
/// <exception cref="ImageProcessingException">The processing operation failed.</exception> /// <exception cref="ImageProcessingException">The processing operation failed.</exception>
public static void Mutate<TPixel>(this Image<TPixel> source, params IImageProcessor[] operations) public static void Mutate<TPixel>(this Image<TPixel> source, params IImageProcessor[] operations)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> Mutate(source, source.GetConfiguration(), operations); => Mutate(source, source.Configuration, operations);
/// <summary> /// <summary>
/// Mutates the source image by applying the operations to it. /// Mutates the source image by applying the operations to it.
@ -135,7 +135,7 @@ public static partial class ProcessingExtensions
/// <exception cref="ObjectDisposedException">The source has been disposed.</exception> /// <exception cref="ObjectDisposedException">The source has been disposed.</exception>
/// <exception cref="ImageProcessingException">The processing operation failed.</exception> /// <exception cref="ImageProcessingException">The processing operation failed.</exception>
public static Image Clone(this Image source, Action<IImageProcessingContext> operation) public static Image Clone(this Image source, Action<IImageProcessingContext> operation)
=> Clone(source, source.GetConfiguration(), operation); => Clone(source, source.Configuration, operation);
/// <summary> /// <summary>
/// Creates a deep clone of the current image. The clone is then mutated by the given operation. /// Creates a deep clone of the current image. The clone is then mutated by the given operation.
@ -174,7 +174,7 @@ public static partial class ProcessingExtensions
/// <returns>The new <see cref="Image{TPixel}"/>.</returns> /// <returns>The new <see cref="Image{TPixel}"/>.</returns>
public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, Action<IImageProcessingContext> operation) public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, Action<IImageProcessingContext> operation)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> Clone(source, source.GetConfiguration(), operation); => Clone(source, source.Configuration, operation);
/// <summary> /// <summary>
/// Creates a deep clone of the current image. The clone is then mutated by the given operation. /// Creates a deep clone of the current image. The clone is then mutated by the given operation.
@ -217,7 +217,7 @@ public static partial class ProcessingExtensions
/// <returns>The new <see cref="Image{TPixel}"/></returns> /// <returns>The new <see cref="Image{TPixel}"/></returns>
public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, params IImageProcessor[] operations) public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, params IImageProcessor[] operations)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> Clone(source, source.GetConfiguration(), operations); => Clone(source, source.Configuration, operations);
/// <summary> /// <summary>
/// Creates a deep clone of the current image. The clone is then mutated by the given operations. /// Creates a deep clone of the current image. The clone is then mutated by the given operations.

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);
} }
} }

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

@ -111,7 +111,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
public QuantizerOptions Options { get; } public QuantizerOptions Options { get; }
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette public readonly ReadOnlyMemory<TPixel> Palette
{ {
get get
{ {
@ -362,7 +362,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// </summary> /// </summary>
/// <param name="source">The source data.</param> /// <param name="source">The source data.</param>
/// <param name="bounds">The bounds within the source image to quantize.</param> /// <param name="bounds">The bounds within the source image to quantize.</param>
private void Build3DHistogram(Buffer2D<TPixel> source, Rectangle bounds) private readonly void Build3DHistogram(Buffer2D<TPixel> source, Rectangle bounds)
{ {
Span<Moment> momentSpan = this.momentsOwner.GetSpan(); Span<Moment> momentSpan = this.momentsOwner.GetSpan();
@ -393,7 +393,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
/// </summary> /// </summary>
/// <param name="allocator">The memory allocator used for allocating buffers.</param> /// <param name="allocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator allocator) private readonly void Get3DMoments(MemoryAllocator allocator)
{ {
using IMemoryOwner<Moment> volume = allocator.Allocate<Moment>(IndexCount * IndexAlphaCount); using IMemoryOwner<Moment> volume = allocator.Allocate<Moment>(IndexCount * IndexAlphaCount);
using IMemoryOwner<Moment> area = allocator.Allocate<Moment>(IndexAlphaCount); using IMemoryOwner<Moment> area = allocator.Allocate<Moment>(IndexAlphaCount);
@ -462,7 +462,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// </summary> /// </summary>
/// <param name="cube">The cube.</param> /// <param name="cube">The cube.</param>
/// <returns>The <see cref="float"/>.</returns> /// <returns>The <see cref="float"/>.</returns>
private double Variance(ref Box cube) private readonly double Variance(ref Box cube)
{ {
ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan(); ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
@ -503,7 +503,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// <param name="cut">The cutting point.</param> /// <param name="cut">The cutting point.</param>
/// <param name="whole">The whole moment.</param> /// <param name="whole">The whole moment.</param>
/// <returns>The <see cref="float"/>.</returns> /// <returns>The <see cref="float"/>.</returns>
private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole) private readonly float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole)
{ {
ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan(); ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
Moment bottom = Bottom(ref cube, direction, momentSpan); Moment bottom = Bottom(ref cube, direction, momentSpan);
@ -634,7 +634,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// </summary> /// </summary>
/// <param name="cube">The cube.</param> /// <param name="cube">The cube.</param>
/// <param name="label">A label.</param> /// <param name="label">A label.</param>
private void Mark(ref Box cube, byte label) private readonly void Mark(ref Box cube, byte label)
{ {
Span<byte> tagSpan = this.tagsOwner.GetSpan(); Span<byte> tagSpan = this.tagsOwner.GetSpan();

2
src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs

@ -38,7 +38,7 @@ internal class EntropyCropProcessor<TPixel> : ImageProcessor<TPixel>
// All frames have be the same size so we only need to calculate the correct dimensions for the first frame // All frames have be the same size so we only need to calculate the correct dimensions for the first frame
using (Image<TPixel> temp = new(this.Configuration, this.Source.Metadata.DeepClone(), new[] { this.Source.Frames.RootFrame.Clone() })) using (Image<TPixel> temp = new(this.Configuration, this.Source.Metadata.DeepClone(), new[] { this.Source.Frames.RootFrame.Clone() }))
{ {
Configuration configuration = this.Source.GetConfiguration(); Configuration configuration = this.Source.Configuration;
// Detect the edges. // Detect the edges.
new EdgeDetector2DProcessor(KnownEdgeDetectorKernels.Sobel, false).Execute(this.Configuration, temp, this.SourceRectangle); new EdgeDetector2DProcessor(KnownEdgeDetectorKernels.Sobel, false).Execute(this.Configuration, temp, this.SourceRectangle);

40
tests/ImageSharp.Tests/Color/ColorTests.cs

@ -18,25 +18,42 @@ public partial class ColorTests
Assert.Equal(expected, (Rgba32)c2); Assert.Equal(expected, (Rgba32)c2);
} }
[Fact] [Theory]
public void Equality_WhenTrue() [InlineData(false)]
[InlineData(true)]
public void Equality_WhenTrue(bool highPrecision)
{ {
Color c1 = new Rgba64(100, 2000, 3000, 40000); Color c1 = new Rgba64(100, 2000, 3000, 40000);
Color c2 = new Rgba64(100, 2000, 3000, 40000); Color c2 = new Rgba64(100, 2000, 3000, 40000);
if (highPrecision)
{
c1 = Color.FromPixel(c1.ToPixel<RgbaVector>());
c2 = Color.FromPixel(c2.ToPixel<RgbaVector>());
}
Assert.True(c1.Equals(c2)); Assert.True(c1.Equals(c2));
Assert.True(c1 == c2); Assert.True(c1 == c2);
Assert.False(c1 != c2); Assert.False(c1 != c2);
Assert.True(c1.GetHashCode() == c2.GetHashCode()); Assert.True(c1.GetHashCode() == c2.GetHashCode());
} }
[Fact] [Theory]
public void Equality_WhenFalse() [InlineData(false)]
[InlineData(true)]
public void Equality_WhenFalse(bool highPrecision)
{ {
Color c1 = new Rgba64(100, 2000, 3000, 40000); Color c1 = new Rgba64(100, 2000, 3000, 40000);
Color c2 = new Rgba64(101, 2000, 3000, 40000); Color c2 = new Rgba64(101, 2000, 3000, 40000);
Color c3 = new Rgba64(100, 2000, 3000, 40001); Color c3 = new Rgba64(100, 2000, 3000, 40001);
if (highPrecision)
{
c1 = Color.FromPixel(c1.ToPixel<RgbaVector>());
c2 = Color.FromPixel(c2.ToPixel<RgbaVector>());
c3 = Color.FromPixel(c3.ToPixel<RgbaVector>());
}
Assert.False(c1.Equals(c2)); Assert.False(c1.Equals(c2));
Assert.False(c2.Equals(c3)); Assert.False(c2.Equals(c3));
Assert.False(c3.Equals(c1)); Assert.False(c3.Equals(c1));
@ -47,13 +64,20 @@ public partial class ColorTests
Assert.False(c1.Equals(null)); Assert.False(c1.Equals(null));
} }
[Fact] [Theory]
public void ToHex() [InlineData(false)]
[InlineData(true)]
public void ToHex(bool highPrecision)
{ {
string expected = "ABCD1234"; string expected = "ABCD1234";
var color = Color.ParseHex(expected); Color color = Color.ParseHex(expected);
string actual = color.ToHex();
if (highPrecision)
{
color = Color.FromPixel(color.ToPixel<RgbaVector>());
}
string actual = color.ToHex();
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }

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);
} }

21
tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs

@ -1,9 +1,11 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Text;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using static SixLabors.ImageSharp.Tests.TestImages.Pbm; using static SixLabors.ImageSharp.Tests.TestImages.Pbm;
@ -120,4 +122,23 @@ public class PbmDecoderTests
testOutputDetails: details, testOutputDetails: details,
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);
} }
[Fact]
public void PlainText_PrematureEof()
{
byte[] bytes = Encoding.ASCII.GetBytes($"P1\n100 100\n1 0 1 0 1 0");
using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(bytes);
Assert.True(eofHitCounter.EofHitCount <= 2);
Assert.Equal(new Size(100, 100), eofHitCounter.Image.Size);
}
[Fact]
public void Binary_PrematureEof()
{
using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(RgbBinaryPrematureEof);
Assert.True(eofHitCounter.EofHitCount <= 2);
Assert.Equal(new Size(29, 30), eofHitCounter.Image.Size);
}
} }

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

@ -83,12 +83,9 @@ public class PbmMetadataTests
} }
[Fact] [Fact]
public void Identify_HandlesCraftedDenialOfServiceString() public void Identify_EofInHeader_ThrowsInvalidImageContentException()
{ {
byte[] bytes = Convert.FromBase64String("UDEjWAAACQAAAAA="); byte[] bytes = Convert.FromBase64String("UDEjWAAACQAAAAA=");
ImageInfo info = Image.Identify(bytes); Assert.Throws<InvalidImageContentException>(() => Image.Identify(bytes));
Assert.Equal(default, info.Size);
Configuration.Default.ImageFormatsManager.TryFindFormatByFileExtension("pbm", out IImageFormat format);
Assert.Equal(format!, info.Metadata.DecodedImageFormat);
} }
} }

6
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -546,7 +546,8 @@ public partial class PngDecoderTests
{ {
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance); using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
PngMetadata metadata = image.Metadata.GetPngMetadata(); PngMetadata metadata = image.Metadata.GetPngMetadata();
Assert.True(metadata.HasTransparency); Assert.NotNull(metadata.ColorTable);
Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToRgba32().A < 255);
} }
// https://github.com/SixLabors/ImageSharp/issues/2209 // https://github.com/SixLabors/ImageSharp/issues/2209
@ -558,7 +559,8 @@ public partial class PngDecoderTests
using MemoryStream stream = new(testFile.Bytes, false); using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream); ImageInfo imageInfo = Image.Identify(stream);
PngMetadata metadata = imageInfo.Metadata.GetPngMetadata(); PngMetadata metadata = imageInfo.Metadata.GetPngMetadata();
Assert.True(metadata.HasTransparency); Assert.NotNull(metadata.ColorTable);
Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToRgba32().A < 255);
} }
// https://github.com/SixLabors/ImageSharp/issues/410 // https://github.com/SixLabors/ImageSharp/issues/410

42
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)]
@ -459,44 +462,17 @@ public partial class PngEncoderTests
TestFile testFile = TestFile.Create(imagePath); TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image(); using Image<Rgba32> input = testFile.CreateRgba32Image();
PngMetadata inMeta = input.Metadata.GetPngMetadata(); PngMetadata inMeta = input.Metadata.GetPngMetadata();
Assert.True(inMeta.HasTransparency); Assert.True(inMeta.TransparentColor.HasValue);
using MemoryStream memStream = new(); using MemoryStream memStream = new();
input.Save(memStream, PngEncoder); input.Save(memStream, PngEncoder);
memStream.Position = 0; memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream); using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
PngMetadata outMeta = output.Metadata.GetPngMetadata(); PngMetadata outMeta = output.Metadata.GetPngMetadata();
Assert.True(outMeta.HasTransparency); Assert.True(outMeta.TransparentColor.HasValue);
Assert.Equal(inMeta.TransparentColor, outMeta.TransparentColor);
switch (pngColorType) Assert.Equal(pngBitDepth, outMeta.BitDepth);
{ Assert.Equal(pngColorType, outMeta.ColorType);
case PngColorType.Grayscale:
if (pngBitDepth.Equals(PngBitDepth.Bit16))
{
Assert.True(outMeta.TransparentL16.HasValue);
Assert.Equal(inMeta.TransparentL16, outMeta.TransparentL16);
}
else
{
Assert.True(outMeta.TransparentL8.HasValue);
Assert.Equal(inMeta.TransparentL8, outMeta.TransparentL8);
}
break;
case PngColorType.Rgb:
if (pngBitDepth.Equals(PngBitDepth.Bit16))
{
Assert.True(outMeta.TransparentRgb48.HasValue);
Assert.Equal(inMeta.TransparentRgb48, outMeta.TransparentRgb48);
}
else
{
Assert.True(outMeta.TransparentRgb24.HasValue);
Assert.Equal(inMeta.TransparentRgb24, outMeta.TransparentRgb24);
}
break;
}
} }
[Theory] [Theory]
@ -608,7 +584,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);

28
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff;
@ -666,6 +665,33 @@ public class TiffDecoderTests : TiffDecoderBaseTester
public void TiffDecoder_CanDecode_TiledWithNonEqualWidthAndHeight<TPixel>(TestImageProvider<TPixel> provider) public void TiffDecoder_CanDecode_TiledWithNonEqualWidthAndHeight<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider); where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(JpegCompressedGray0000539558, PixelTypes.Rgba32)]
public void TiffDecoder_ThrowsException_WithCircular_IFD_Offsets<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
=> Assert.Throws<ImageFormatException>(
() =>
{
using (provider.GetImage(TiffDecoder.Instance))
{
}
});
[Theory]
[WithFile(Tiled0000023664, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_TiledWithBadZlib<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance);
// ImageMagick cannot decode this image.
image.DebugSave(provider);
image.CompareToReferenceOutput(
ImageComparer.Exact,
provider,
appendPixelTypeToFileName: false);
}
[Theory] [Theory]
[WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)] [WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)]
public void DecodeMultiframe<TPixel>(TestImageProvider<TPixel> provider) public void DecodeMultiframe<TPixel>(TestImageProvider<TPixel> provider)

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);
}
} }

19
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.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 System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
@ -340,6 +339,24 @@ public class WebpDecoderTests
Assert.Equal(1, image.Frames.Count); Assert.Equal(1, image.Frames.Count);
} }
[Theory]
[WithFile(Lossy.AnimatedIssue2528, PixelTypes.Rgba32)]
public void Decode_AnimatedLossy_IgnoreBackgroundColor_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
WebpDecoderOptions options = new()
{
BackgroundColorHandling = BackgroundColorHandling.Ignore,
GeneralOptions = new DecoderOptions()
{
MaxFrames = 1
}
};
using Image<TPixel> image = provider.GetImage(WebpDecoder.Instance, options);
image.DebugSave(provider);
image.CompareToOriginal(provider, ReferenceDecoder);
}
[Theory] [Theory]
[WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)] [WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)]
[WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)] [WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)]

4
tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs

@ -42,7 +42,7 @@ public class YuvConversionTests
{ {
// arrange // arrange
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
Configuration config = image.GetConfiguration(); Configuration config = image.Configuration;
MemoryAllocator memoryAllocator = config.MemoryAllocator; MemoryAllocator memoryAllocator = config.MemoryAllocator;
int pixels = image.Width * image.Height; int pixels = image.Width * image.Height;
int uvWidth = (image.Width + 1) >> 1; int uvWidth = (image.Width + 1) >> 1;
@ -158,7 +158,7 @@ public class YuvConversionTests
{ {
// arrange // arrange
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
Configuration config = image.GetConfiguration(); Configuration config = image.Configuration;
MemoryAllocator memoryAllocator = config.MemoryAllocator; MemoryAllocator memoryAllocator = config.MemoryAllocator;
int pixels = image.Width * image.Height; int pixels = image.Width * image.Height;
int uvWidth = (image.Width + 1) >> 1; int uvWidth = (image.Width + 1) >> 1;

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

@ -20,7 +20,7 @@ public abstract partial class ImageFrameCollectionTests
public void AddFrame_OfDifferentPixelType() public void AddFrame_OfDifferentPixelType()
{ {
using (Image<Bgra32> sourceImage = new( using (Image<Bgra32> sourceImage = new(
this.Image.GetConfiguration(), this.Image.Configuration,
this.Image.Width, this.Image.Width,
this.Image.Height, this.Image.Height,
Color.Blue)) Color.Blue))
@ -41,7 +41,7 @@ public abstract partial class ImageFrameCollectionTests
public void InsertFrame_OfDifferentPixelType() public void InsertFrame_OfDifferentPixelType()
{ {
using (Image<Bgra32> sourceImage = new( using (Image<Bgra32> sourceImage = new(
this.Image.GetConfiguration(), this.Image.Configuration,
this.Image.Width, this.Image.Width,
this.Image.Height, this.Image.Height,
Color.Blue)) Color.Blue))
@ -278,7 +278,8 @@ public abstract partial class ImageFrameCollectionTests
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
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.Configuration, 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);
}
} }
} }
} }

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

@ -70,7 +70,7 @@ public partial class ImageTests
{ {
using Image<Rgba32> image = new(5, 5); using Image<Rgba32> image = new(5, 5);
string ext = Path.GetExtension(filename); string ext = Path.GetExtension(filename);
image.GetConfiguration().ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat format); image.Configuration.ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat format);
Assert.Equal(mimeType, format!.DefaultMimeType); Assert.Equal(mimeType, format!.DefaultMimeType);
using MemoryStream stream = new(); using MemoryStream stream = new();

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

@ -136,7 +136,7 @@ public partial class ImageTests
ref Rgba32 pixel0 = ref imageMem.Span[0]; ref Rgba32 pixel0 = ref imageMem.Span[0];
Assert.True(Unsafe.AreSame(ref array[0], ref pixel0)); Assert.True(Unsafe.AreSame(ref array[0], ref pixel0));
Assert.Equal(cfg, image.GetConfiguration()); Assert.Equal(cfg, image.Configuration);
Assert.Equal(metaData, image.Metadata); Assert.Equal(metaData, image.Metadata);
} }
} }
@ -239,7 +239,7 @@ public partial class ImageTests
ref Rgba32 pixel0 = ref imageMem.Span[0]; ref Rgba32 pixel0 = ref imageMem.Span[0];
Assert.True(Unsafe.AreSame(ref Unsafe.As<byte, Rgba32>(ref array[0]), ref pixel0)); Assert.True(Unsafe.AreSame(ref Unsafe.As<byte, Rgba32>(ref array[0]), ref pixel0));
Assert.Equal(cfg, image.GetConfiguration()); Assert.Equal(cfg, image.Configuration);
Assert.Equal(metaData, image.Metadata); Assert.Equal(metaData, image.Metadata);
} }
} }
@ -336,7 +336,7 @@ public partial class ImageTests
ref Rgba32 pixel_1 = ref imageSpan[imageSpan.Length - 1]; ref Rgba32 pixel_1 = ref imageSpan[imageSpan.Length - 1];
Assert.True(Unsafe.AreSame(ref array[array.Length - 1], ref pixel_1)); Assert.True(Unsafe.AreSame(ref array[array.Length - 1], ref pixel_1));
Assert.Equal(cfg, image.GetConfiguration()); Assert.Equal(cfg, image.Configuration);
Assert.Equal(metaData, image.Metadata); Assert.Equal(metaData, image.Metadata);
} }
} }

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

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Memory; using SixLabors.ImageSharp.Tests.Memory;
@ -31,10 +32,14 @@ public partial class ImageTests
Assert.Equal(11 * 23, imageMem.Length); Assert.Equal(11 * 23, imageMem.Length);
image.ComparePixelBufferTo(default(Rgba32)); image.ComparePixelBufferTo(default(Rgba32));
Assert.Equal(Configuration.Default, image.GetConfiguration()); Assert.Equal(Configuration.Default, image.Configuration);
} }
} }
[Fact]
public void Width_Height_SizeNotRepresentable_ThrowsInvalidImageOperationException()
=> Assert.Throws<InvalidMemoryOperationException>(() => new Image<Rgba32>(int.MaxValue, int.MaxValue));
[Fact] [Fact]
public void Configuration_Width_Height() public void Configuration_Width_Height()
{ {
@ -48,7 +53,7 @@ public partial class ImageTests
Assert.Equal(11 * 23, imageMem.Length); Assert.Equal(11 * 23, imageMem.Length);
image.ComparePixelBufferTo(default(Rgba32)); image.ComparePixelBufferTo(default(Rgba32));
Assert.Equal(configuration, image.GetConfiguration()); Assert.Equal(configuration, image.Configuration);
} }
} }
@ -66,7 +71,7 @@ public partial class ImageTests
Assert.Equal(11 * 23, imageMem.Length); Assert.Equal(11 * 23, imageMem.Length);
image.ComparePixelBufferTo(color); image.ComparePixelBufferTo(color);
Assert.Equal(configuration, image.GetConfiguration()); Assert.Equal(configuration, image.Configuration);
} }
} }
@ -83,7 +88,7 @@ public partial class ImageTests
{ {
Assert.Equal(21, image.Width); Assert.Equal(21, image.Width);
Assert.Equal(22, image.Height); Assert.Equal(22, image.Height);
Assert.Same(configuration, image.GetConfiguration()); Assert.Same(configuration, image.Configuration);
Assert.Same(metadata, image.Metadata); Assert.Same(metadata, image.Metadata);
Assert.Equal(dirtyValue, image[5, 5].PackedValue); Assert.Equal(dirtyValue, image[5, 5].PackedValue);

7
tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs

@ -107,6 +107,13 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
} }
} }
[Fact]
public void AllocateGroup_SizeInBytesOverLongMaxValue_ThrowsInvalidMemoryOperationException()
{
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(null);
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<S4>(int.MaxValue * (long)int.MaxValue, int.MaxValue));
}
[Fact] [Fact]
public unsafe void Allocate_MemoryIsPinnableMultipleTimes() public unsafe void Allocate_MemoryIsPinnableMultipleTimes()
{ {

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,

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save