Browse Source

Merge branch 'main' into icc-color-conversion

pull/1567/head
James Jackson-South 3 years ago
parent
commit
a567613f0a
  1. 40
      src/ImageSharp/Advanced/AdvancedImageExtensions.cs
  2. 2
      src/ImageSharp/Advanced/IConfigurationProvider.cs
  3. 12
      src/ImageSharp/Color/Color.cs
  4. 53
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  5. 2
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  6. 2
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  7. 2
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  8. 2
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  9. 10
      src/ImageSharp/Formats/ImageDecoderUtilities.cs
  10. 4
      src/ImageSharp/Formats/ImageEncoder.cs
  11. 72
      src/ImageSharp/Formats/ImageExtensions.Save.cs
  12. 8
      src/ImageSharp/Formats/ImageExtensions.Save.tt
  13. 2
      src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs
  14. 2
      src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs
  15. 29
      src/ImageSharp/Formats/Pbm/BinaryDecoder.cs
  16. 41
      src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs
  17. 25
      src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
  18. 2
      src/ImageSharp/Formats/Pbm/PbmEncoder.cs
  19. 2
      src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
  20. 109
      src/ImageSharp/Formats/Pbm/PlainDecoder.cs
  21. 47
      src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
  22. 160
      src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
  23. 2
      src/ImageSharp/Formats/Png/Chunks/PngHeader.cs
  24. 14
      src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
  25. 2
      src/ImageSharp/Formats/Png/Chunks/PngTextData.cs
  26. 20
      src/ImageSharp/Formats/Png/MetadataExtensions.cs
  27. 22
      src/ImageSharp/Formats/Png/PngBlendMethod.cs
  28. 7
      src/ImageSharp/Formats/Png/PngChunk.cs
  29. 65
      src/ImageSharp/Formats/Png/PngChunkType.cs
  30. 8
      src/ImageSharp/Formats/Png/PngConstants.cs
  31. 8
      src/ImageSharp/Formats/Png/PngDecoder.cs
  32. 571
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  33. 25
      src/ImageSharp/Formats/Png/PngDisposalMethod.cs
  34. 14
      src/ImageSharp/Formats/Png/PngEncoder.cs
  35. 480
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  36. 5
      src/ImageSharp/Formats/Png/PngFormat.cs
  37. 62
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  38. 45
      src/ImageSharp/Formats/Png/PngMetadata.cs
  39. 488
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  40. 26
      src/ImageSharp/Formats/Png/PngThrowHelper.cs
  41. 4
      src/ImageSharp/Formats/Qoi/QoiEncoder.cs
  42. 5
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  43. 6
      src/ImageSharp/Formats/Tga/TgaEncoder.cs
  44. 2
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  45. 2
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
  46. 1
      src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
  47. 22
      src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
  48. 3
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  49. 2
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  50. 21
      src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs
  51. 2
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  52. 14
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  53. 18
      src/ImageSharp/Formats/Webp/WebpDecoder.cs
  54. 18
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  55. 21
      src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs
  56. 2
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  57. 18
      src/ImageSharp/IO/BufferedReadStream.cs
  58. 13
      src/ImageSharp/Image.cs
  59. 12
      src/ImageSharp/ImageExtensions.cs
  60. 14
      src/ImageSharp/ImageFrame.cs
  61. 21
      src/ImageSharp/ImageFrameCollection{TPixel}.cs
  62. 12
      src/ImageSharp/ImageFrame{TPixel}.cs
  63. 4
      src/ImageSharp/ImageSharp.csproj
  64. 8
      src/ImageSharp/Image{TPixel}.cs
  65. 5
      src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
  66. 3
      src/ImageSharp/Memory/Buffer2D{T}.cs
  67. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
  68. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs
  69. 13
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs
  70. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs
  71. 5
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs
  72. 7
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs
  73. 18
      src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
  74. 17
      src/ImageSharp/Primitives/Rational.cs
  75. 2
      src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
  76. 12
      src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs
  77. 12
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
  78. 2
      src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs
  79. 40
      tests/ImageSharp.Tests/Color/ColorTests.cs
  80. 21
      tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
  81. 7
      tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs
  82. 32
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  83. 71
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  84. 35
      tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs
  85. 14
      tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
  86. 2
      tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
  87. 28
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  88. 19
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  89. 4
      tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs
  90. 8
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
  91. 2
      tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
  92. 6
      tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs
  93. 13
      tests/ImageSharp.Tests/Image/ImageTests.cs
  94. 7
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  95. 24
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs
  96. 2
      tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs
  97. 2
      tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs
  98. 15
      tests/ImageSharp.Tests/TestImages.cs
  99. 36
      tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs
  100. 2
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs

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.

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

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

@ -123,12 +123,12 @@ internal sealed class ZlibInflateStream : Stream
/// <inheritdoc/> /// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{ {
if (this.currentDataRemaining == 0) if (this.currentDataRemaining is 0)
{ {
// Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks. // Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks.
this.currentDataRemaining = this.getData(); this.currentDataRemaining = this.getData();
if (this.currentDataRemaining == 0) if (this.currentDataRemaining is 0)
{ {
return 0; return 0;
} }
@ -142,11 +142,11 @@ internal sealed class ZlibInflateStream : Stream
// Keep reading data until we've reached the end of the stream or filled the buffer. // Keep reading data until we've reached the end of the stream or filled the buffer.
int bytesRead = 0; int bytesRead = 0;
offset += totalBytesRead; offset += totalBytesRead;
while (this.currentDataRemaining == 0 && totalBytesRead < count) while (this.currentDataRemaining is 0 && totalBytesRead < count)
{ {
this.currentDataRemaining = this.getData(); this.currentDataRemaining = this.getData();
if (this.currentDataRemaining == 0) if (this.currentDataRemaining is 0)
{ {
return totalBytesRead; return totalBytesRead;
} }
@ -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;
} }

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

@ -32,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);
} }
} }

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

@ -119,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;

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

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

@ -189,7 +189,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// This frame is reused to store de-duplicated pixel buffers. // 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 // This is more expensive memory-wise than de-duplicating indexed buffer but allows us to deduplicate
// frames using both local and global palettes. // frames using both local and global palettes.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.GetConfiguration(), previousFrame.Size()); using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size());
for (int i = 1; i < image.Frames.Count; i++) for (int i = 1; i < image.Frames.Count; i++)
{ {

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

47
src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs

@ -0,0 +1,47 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Png.Chunks;
internal readonly struct AnimationControl
{
public const int Size = 8;
public AnimationControl(int numberFrames, int numberPlays)
{
this.NumberFrames = numberFrames;
this.NumberPlays = numberPlays;
}
/// <summary>
/// Gets the number of frames
/// </summary>
public int NumberFrames { get; }
/// <summary>
/// Gets the number of times to loop this APNG. 0 indicates infinite looping.
/// </summary>
public int NumberPlays { get; }
/// <summary>
/// Writes the acTL to the given buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteTo(Span<byte> buffer)
{
BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames);
BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays);
}
/// <summary>
/// Parses the APngAnimationControl from the given data buffer.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed acTL.</returns>
public static AnimationControl Parse(ReadOnlySpan<byte> data)
=> new(
numberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
numberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
}

160
src/ImageSharp/Formats/Png/Chunks/FrameControl.cs

@ -0,0 +1,160 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Png.Chunks;
internal readonly struct FrameControl
{
public const int Size = 26;
public FrameControl(uint width, uint height)
: this(0, width, height, 0, 0, 0, 0, default, default)
{
}
public FrameControl(
uint sequenceNumber,
uint width,
uint height,
uint xOffset,
uint yOffset,
ushort delayNumerator,
ushort delayDenominator,
PngDisposalMethod disposeOperation,
PngBlendMethod blendOperation)
{
this.SequenceNumber = sequenceNumber;
this.Width = width;
this.Height = height;
this.XOffset = xOffset;
this.YOffset = yOffset;
this.DelayNumerator = delayNumerator;
this.DelayDenominator = delayDenominator;
this.DisposeOperation = disposeOperation;
this.BlendOperation = blendOperation;
}
/// <summary>
/// Gets the sequence number of the animation chunk, starting from 0
/// </summary>
public uint SequenceNumber { get; }
/// <summary>
/// Gets the width of the following frame
/// </summary>
public uint Width { get; }
/// <summary>
/// Gets the height of the following frame
/// </summary>
public uint Height { get; }
/// <summary>
/// Gets the X position at which to render the following frame
/// </summary>
public uint XOffset { get; }
/// <summary>
/// Gets the Y position at which to render the following frame
/// </summary>
public uint YOffset { get; }
/// <summary>
/// Gets the X limit at which to render the following frame
/// </summary>
public uint XMax => this.XOffset + this.Width;
/// <summary>
/// Gets the Y limit at which to render the following frame
/// </summary>
public uint YMax => this.YOffset + this.Height;
/// <summary>
/// Gets the frame delay fraction numerator
/// </summary>
public ushort DelayNumerator { get; }
/// <summary>
/// Gets the frame delay fraction denominator
/// </summary>
public ushort DelayDenominator { get; }
/// <summary>
/// Gets the type of frame area disposal to be done after rendering this frame
/// </summary>
public PngDisposalMethod DisposeOperation { get; }
/// <summary>
/// Gets the type of frame area rendering for this frame
/// </summary>
public PngBlendMethod BlendOperation { get; }
public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height);
/// <summary>
/// Validates the APng fcTL.
/// </summary>
/// <param name="header">The header.</param>
/// <exception cref="NotSupportedException">
/// Thrown if the image does pass validation.
/// </exception>
public void Validate(PngHeader header)
{
if (this.Width == 0)
{
PngThrowHelper.ThrowInvalidParameter(this.Width, "Expected > 0");
}
if (this.Height == 0)
{
PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0");
}
if (this.XMax > header.Width)
{
PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The x-offset plus width > {nameof(PngHeader)}.{nameof(PngHeader.Width)}");
}
if (this.YMax > header.Height)
{
PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The y-offset plus height > {nameof(PngHeader)}.{nameof(PngHeader.Height)}");
}
}
/// <summary>
/// Writes the fcTL to the given buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteTo(Span<byte> buffer)
{
BinaryPrimitives.WriteUInt32BigEndian(buffer[..4], this.SequenceNumber);
BinaryPrimitives.WriteUInt32BigEndian(buffer[4..8], this.Width);
BinaryPrimitives.WriteUInt32BigEndian(buffer[8..12], this.Height);
BinaryPrimitives.WriteUInt32BigEndian(buffer[12..16], this.XOffset);
BinaryPrimitives.WriteUInt32BigEndian(buffer[16..20], this.YOffset);
BinaryPrimitives.WriteUInt16BigEndian(buffer[20..22], this.DelayNumerator);
BinaryPrimitives.WriteUInt16BigEndian(buffer[22..24], this.DelayDenominator);
buffer[24] = (byte)this.DisposeOperation;
buffer[25] = (byte)this.BlendOperation;
}
/// <summary>
/// Parses the APngFrameControl from the given data buffer.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed fcTL.</returns>
public static FrameControl Parse(ReadOnlySpan<byte> data)
=> new(
sequenceNumber: BinaryPrimitives.ReadUInt32BigEndian(data[..4]),
width: BinaryPrimitives.ReadUInt32BigEndian(data[4..8]),
height: BinaryPrimitives.ReadUInt32BigEndian(data[8..12]),
xOffset: BinaryPrimitives.ReadUInt32BigEndian(data[12..16]),
yOffset: BinaryPrimitives.ReadUInt32BigEndian(data[16..20]),
delayNumerator: BinaryPrimitives.ReadUInt16BigEndian(data[20..22]),
delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]),
disposeOperation: (PngDisposalMethod)data[24],
blendOperation: (PngBlendMethod)data[25]);
}

2
src/ImageSharp/Formats/Png/PngHeader.cs → src/ImageSharp/Formats/Png/Chunks/PngHeader.cs

@ -4,7 +4,7 @@
using System.Buffers.Binary; using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png.Chunks;
/// <summary> /// <summary>
/// Represents the png header chunk. /// Represents the png header chunk.

14
src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs → src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary; using System.Buffers.Binary;
@ -10,11 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks;
/// <summary> /// <summary>
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary> /// </summary>
internal readonly struct PhysicalChunkData internal readonly struct PngPhysical
{ {
public const int Size = 9; public const int Size = 9;
public PhysicalChunkData(uint x, uint y, byte unitSpecifier) public PngPhysical(uint x, uint y, byte unitSpecifier)
{ {
this.XAxisPixelsPerUnit = x; this.XAxisPixelsPerUnit = x;
this.YAxisPixelsPerUnit = y; this.YAxisPixelsPerUnit = y;
@ -44,13 +44,13 @@ internal readonly struct PhysicalChunkData
/// </summary> /// </summary>
/// <param name="data">The data buffer.</param> /// <param name="data">The data buffer.</param>
/// <returns>The parsed PhysicalChunkData.</returns> /// <returns>The parsed PhysicalChunkData.</returns>
public static PhysicalChunkData Parse(ReadOnlySpan<byte> data) public static PngPhysical Parse(ReadOnlySpan<byte> data)
{ {
uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]); uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]);
uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4)); uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4));
byte unit = data[8]; byte unit = data[8];
return new PhysicalChunkData(hResolution, vResolution, unit); return new PngPhysical(hResolution, vResolution, unit);
} }
/// <summary> /// <summary>
@ -59,7 +59,7 @@ internal readonly struct PhysicalChunkData
/// </summary> /// </summary>
/// <param name="meta">The metadata.</param> /// <param name="meta">The metadata.</param>
/// <returns>The constructed PngPhysicalChunkData instance.</returns> /// <returns>The constructed PngPhysicalChunkData instance.</returns>
public static PhysicalChunkData FromMetadata(ImageMetadata meta) public static PngPhysical FromMetadata(ImageMetadata meta)
{ {
byte unitSpecifier = 0; byte unitSpecifier = 0;
uint x; uint x;
@ -92,7 +92,7 @@ internal readonly struct PhysicalChunkData
break; break;
} }
return new PhysicalChunkData(x, y, unitSpecifier); return new PngPhysical(x, y, unitSpecifier);
} }
/// <summary> /// <summary>

2
src/ImageSharp/Formats/Png/PngTextData.cs → src/ImageSharp/Formats/Png/Chunks/PngTextData.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png.Chunks;
/// <summary> /// <summary>
/// Stores text data contained in the iTXt, tEXt, and zTXt chunks. /// Stores text data contained in the iTXt, tEXt, and zTXt chunks.

20
src/ImageSharp/Formats/Png/MetadataExtensions.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.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
@ -14,7 +15,22 @@ public static partial class MetadataExtensions
/// <summary> /// <summary>
/// Gets the png format specific metadata for the image. /// Gets the png format specific metadata for the image.
/// </summary> /// </summary>
/// <param name="metadata">The metadata this method extends.</param> /// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngMetadata"/>.</returns> /// <returns>The <see cref="PngMetadata"/>.</returns>
public static PngMetadata GetPngMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PngFormat.Instance); public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
/// <summary>
/// Gets the aPng format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
/// <summary>
/// Gets the aPng format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
} }

22
src/ImageSharp/Formats/Png/PngBlendMethod.cs

@ -0,0 +1,22 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Specifies whether the frame is to be alpha blended into the current output buffer content,
/// or whether it should completely replace its region in the output buffer.
/// </summary>
public enum PngBlendMethod
{
/// <summary>
/// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
/// </summary>
Source,
/// <summary>
/// The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as
/// described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2].
/// </summary>
Over
}

7
src/ImageSharp/Formats/Png/PngChunk.cs

@ -42,7 +42,8 @@ internal readonly struct PngChunk
/// Gets a value indicating whether the given chunk is critical to decoding /// Gets a value indicating whether the given chunk is critical to decoding
/// </summary> /// </summary>
public bool IsCritical => public bool IsCritical =>
this.Type == PngChunkType.Header || this.Type is PngChunkType.Header or
this.Type == PngChunkType.Palette || PngChunkType.Palette or
this.Type == PngChunkType.Data; PngChunkType.Data or
PngChunkType.FrameData;
} }

65
src/ImageSharp/Formats/Png/PngChunkType.cs

@ -9,15 +9,17 @@ namespace SixLabors.ImageSharp.Formats.Png;
internal enum PngChunkType : uint internal enum PngChunkType : uint
{ {
/// <summary> /// <summary>
/// The IDAT chunk contains the actual image data. The image can contains more /// This chunk contains the actual image data. The image can contains more
/// than one chunk of this type. All chunks together are the whole image. /// than one chunk of this type. All chunks together are the whole image.
/// </summary> /// </summary>
/// <remarks>IDAT (Multiple)</remarks>
Data = 0x49444154U, Data = 0x49444154U,
/// <summary> /// <summary>
/// This chunk must appear last. It marks the end of the PNG data stream. /// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty. /// The chunk's data field is empty.
/// </summary> /// </summary>
/// <remarks>IEND (Single)</remarks>
End = 0x49454E44U, End = 0x49454E44U,
/// <summary> /// <summary>
@ -25,34 +27,40 @@ internal enum PngChunkType : uint
/// common information like the width and the height of the image or /// common information like the width and the height of the image or
/// the used compression method. /// the used compression method.
/// </summary> /// </summary>
/// <remarks>IHDR (Single)</remarks>
Header = 0x49484452U, Header = 0x49484452U,
/// <summary> /// <summary>
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format. /// series in the RGB format.
/// </summary> /// </summary>
/// <remarks>PLTE (Single)</remarks>
Palette = 0x504C5445U, Palette = 0x504C5445U,
/// <summary> /// <summary>
/// The eXIf data chunk which contains the Exif profile. /// The eXIf data chunk which contains the Exif profile.
/// </summary> /// </summary>
/// <remarks>eXIF (Single)</remarks>
Exif = 0x65584966U, Exif = 0x65584966U,
/// <summary> /// <summary>
/// This chunk specifies the relationship between the image samples and the desired /// This chunk specifies the relationship between the image samples and the desired
/// display output intensity. /// display output intensity.
/// </summary> /// </summary>
/// <remarks>gAMA (Single)</remarks>
Gamma = 0x67414D41U, Gamma = 0x67414D41U,
/// <summary> /// <summary>
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. /// This chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary> /// </summary>
/// <remarks>pHYs (Single)</remarks>
Physical = 0x70485973U, Physical = 0x70485973U,
/// <summary> /// <summary>
/// Textual information that the encoder wishes to record with the image can be stored in /// Textual information that the encoder wishes to record with the image can be stored in
/// tEXt chunks. Each tEXt chunk contains a keyword and a text string. /// tEXt chunks. Each tEXt chunk contains a keyword and a text string.
/// </summary> /// </summary>
/// <remarks>tEXT (Multiple)</remarks>
Text = 0x74455874U, Text = 0x74455874U,
/// <summary> /// <summary>
@ -60,70 +68,103 @@ internal enum PngChunkType : uint
/// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and /// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and
/// a compressed text string. /// a compressed text string.
/// </summary> /// </summary>
/// <remarks>zTXt (Multiple)</remarks>
CompressedText = 0x7A545874U, CompressedText = 0x7A545874U,
/// <summary> /// <summary>
/// The iTXt chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword /// This chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword
/// and the actual text string, which can be compressed or uncompressed. /// and the actual text string, which can be compressed or uncompressed.
/// </summary> /// </summary>
/// <remarks>iTXt (Multiple)</remarks>
InternationalText = 0x69545874U, InternationalText = 0x69545874U,
/// <summary> /// <summary>
/// The tRNS chunk specifies that the image uses simple transparency: /// This chunk specifies that the image uses simple transparency:
/// either alpha values associated with palette entries (for indexed-color images) /// either alpha values associated with palette entries (for indexed-color images)
/// or a single transparent color (for grayscale and true color images). /// or a single transparent color (for grayscale and true color images).
/// </summary> /// </summary>
/// <remarks>tRNS (Single)</remarks>
Transparency = 0x74524E53U, Transparency = 0x74524E53U,
/// <summary> /// <summary>
/// The tIME chunk gives the time of the last image modification (not the time of initial image creation). /// This chunk gives the time of the last image modification (not the time of initial image creation).
/// </summary> /// </summary>
/// <remarks>tIME (Single)</remarks>
Time = 0x74494d45, Time = 0x74494d45,
/// <summary> /// <summary>
/// The bKGD chunk specifies a default background colour to present the image against. /// This chunk specifies a default background colour to present the image against.
/// If there is any other preferred background, either user-specified or part of a larger page (as in a browser), /// If there is any other preferred background, either user-specified or part of a larger page (as in a browser),
/// the bKGD chunk should be ignored. /// the bKGD chunk should be ignored.
/// </summary> /// </summary>
/// <remarks>bKGD (Single)</remarks>
Background = 0x624b4744, Background = 0x624b4744,
/// <summary> /// <summary>
/// The iCCP chunk contains a embedded color profile. If the iCCP chunk is present, /// This chunk contains a embedded color profile. If the iCCP chunk is present,
/// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium. /// the image samples conform to the colour space represented by the embedded ICC profile as defined by the International Color Consortium.
/// </summary> /// </summary>
/// <remarks>iCCP (Single)</remarks>
EmbeddedColorProfile = 0x69434350, EmbeddedColorProfile = 0x69434350,
/// <summary> /// <summary>
/// The sBIT chunk defines the original number of significant bits (which can be less than or equal to the sample depth). /// This chunk defines the original number of significant bits (which can be less than or equal to the sample depth).
/// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG. /// This allows PNG decoders to recover the original data losslessly even if the data had a sample depth not directly supported by PNG.
/// </summary> /// </summary>
/// <remarks>sBIT (Single)</remarks>
SignificantBits = 0x73424954, SignificantBits = 0x73424954,
/// <summary> /// <summary>
/// If the sRGB chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed /// If the this chunk is present, the image samples conform to the sRGB colour space [IEC 61966-2-1] and should be displayed
/// using the specified rendering intent defined by the International Color Consortium. /// using the specified rendering intent defined by the International Color Consortium.
/// </summary> /// </summary>
/// <remarks>sRGB (Single)</remarks>
StandardRgbColourSpace = 0x73524742, StandardRgbColourSpace = 0x73524742,
/// <summary> /// <summary>
/// The hIST chunk gives the approximate usage frequency of each colour in the palette. /// This chunk gives the approximate usage frequency of each colour in the palette.
/// </summary> /// </summary>
/// <remarks>hIST (Single)</remarks>
Histogram = 0x68495354, Histogram = 0x68495354,
/// <summary> /// <summary>
/// The sPLT chunk contains the suggested palette. /// This chunk contains the suggested palette.
/// </summary> /// </summary>
/// <remarks>sPLT (Single)</remarks>
SuggestedPalette = 0x73504c54, SuggestedPalette = 0x73504c54,
/// <summary> /// <summary>
/// The cHRM chunk may be used to specify the 1931 CIE x,y chromaticities of the red, /// This chunk may be used to specify the 1931 CIE x,y chromaticities of the red,
/// green, and blue display primaries used in the image, and the referenced white point. /// green, and blue display primaries used in the image, and the referenced white point.
/// </summary> /// </summary>
/// <remarks>cHRM (Single)</remarks>
Chroma = 0x6348524d, Chroma = 0x6348524d,
/// <summary>
/// This chunk is an ancillary chunk as defined in the PNG Specification.
/// It must appear before the first IDAT chunk within a valid PNG stream.
/// </summary>
/// <remarks>acTL (Single, APNG)</remarks>
AnimationControl = 0x6163544cU,
/// <summary>
/// This chunk is an ancillary chunk as defined in the PNG Specification.
/// It must appear before the IDAT or fdAT chunks of the frame to which it applies.
/// </summary>
/// <remarks>fcTL (Multiple, APNG)</remarks>
FrameControl = 0x6663544cU,
/// <summary>
/// This chunk has the same purpose as an IDAT chunk.
/// It has the same structure as an IDAT chunk, except preceded by a sequence number.
/// </summary>
/// <remarks>fdAT (Multiple, APNG)</remarks>
FrameData = 0x66644154U,
/// <summary> /// <summary>
/// Malformed chunk named CgBI produced by apple, which is not conform to the specification. /// Malformed chunk named CgBI produced by apple, which is not conform to the specification.
/// Related issue is here https://github.com/SixLabors/ImageSharp/issues/410 /// Related issue is here https://github.com/SixLabors/ImageSharp/issues/410
/// </summary> /// </summary>
/// <remarks>CgBI</remarks>
ProprietaryApple = 0x43674249 ProprietaryApple = 0x43674249
} }

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

@ -28,12 +28,12 @@ internal static class PngConstants
/// <summary> /// <summary>
/// The list of mimetypes that equate to a Png. /// The list of mimetypes that equate to a Png.
/// </summary> /// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/png" }; public static readonly IEnumerable<string> MimeTypes = new[] { "image/png", "image/apng" };
/// <summary> /// <summary>
/// The list of file extensions that equate to a Png. /// The list of file extensions that equate to a Png.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "png" }; public static readonly IEnumerable<string> FileExtensions = new[] { "png", "apng" };
/// <summary> /// <summary>
/// The header bytes as a big-endian coded ulong. /// The header bytes as a big-endian coded ulong.
@ -43,7 +43,7 @@ internal static class PngConstants
/// <summary> /// <summary>
/// The dictionary of available color types. /// The dictionary of available color types.
/// </summary> /// </summary>
public static readonly Dictionary<PngColorType, byte[]> ColorTypes = new Dictionary<PngColorType, byte[]> public static readonly Dictionary<PngColorType, byte[]> ColorTypes = new()
{ {
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 }, [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 },
[PngColorType.Rgb] = new byte[] { 8, 16 }, [PngColorType.Rgb] = new byte[] { 8, 16 },
@ -80,7 +80,7 @@ internal static class PngConstants
/// <summary> /// <summary>
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> XmpKeyword => new byte[] public static ReadOnlySpan<byte> XmpKeyword => new[]
{ {
(byte)'X', (byte)'X',
(byte)'M', (byte)'M',

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

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

File diff suppressed because it is too large

25
src/ImageSharp/Formats/Png/PngDisposalMethod.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
/// </summary>
public enum PngDisposalMethod
{
/// <summary>
/// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
/// </summary>
None,
/// <summary>
/// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
/// </summary>
Background,
/// <summary>
/// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
/// </summary>
Previous
}

14
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;
@ -11,6 +11,16 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary> /// </summary>
public class PngEncoder : QuantizingImageEncoder public class PngEncoder : QuantizingImageEncoder
{ {
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
/// </summary>
public PngEncoder()
// 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).
/// Not all values are allowed for all <see cref="ColorType" /> values. /// Not all values are allowed for all <see cref="ColorType" /> values.
@ -68,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);
} }
} }

480
src/ImageSharp/Formats/Png/PngEncoderCore.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.
#nullable disable
using System.Buffers; 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;
@ -101,29 +99,34 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <summary> /// <summary>
/// The raw data of previous scanline. /// The raw data of previous scanline.
/// </summary> /// </summary>
private IMemoryOwner<byte> previousScanline; private IMemoryOwner<byte> previousScanline = null!;
/// <summary> /// <summary>
/// The raw data of current scanline. /// The raw data of current scanline.
/// </summary> /// </summary>
private IMemoryOwner<byte> currentScanline; private IMemoryOwner<byte> currentScanline = null!;
/// <summary> /// <summary>
/// The color profile name. /// The color profile name.
/// </summary> /// </summary>
private const string ColorProfileName = "ICC Profile"; private const string ColorProfileName = "ICC Profile";
/// <summary>
/// The encoder quantizer, if present.
/// </summary>
private IQuantizer? quantizer;
/// <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;
this.quantizer = encoder.Quantizer;
} }
/// <summary> /// <summary>
@ -143,20 +146,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.height = image.Height; this.height = image.Height;
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
Image<TPixel> clonedImage = null;
bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear; stream.Write(PngConstants.HeaderBytes);
ImageFrame<TPixel>? clonedFrame = null;
ImageFrame<TPixel> currentFrame = image.Frames.RootFrame;
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
if (clearTransparency) if (clearTransparency)
{ {
clonedImage = image.Clone(); currentFrame = clonedFrame = currentFrame.Clone();
ClearTransparentPixels(clonedImage); ClearTransparentPixels(currentFrame);
} }
IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage); // Do not move this. We require an accurate bit depth for the header chunk.
IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null);
stream.Write(PngConstants.HeaderBytes);
this.WriteHeaderChunk(stream); this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream); this.WriteGammaChunk(stream);
@ -167,13 +173,58 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.WriteExifChunk(stream, metadata); this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata); this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata); this.WriteTextChunks(stream, pngMetadata);
this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
if (image.Frames.Count > 1)
{
this.WriteAnimationControlChunk(stream, image.Frames.Count, pngMetadata.RepeatCount);
// TODO: We should attempt to optimize the output by clipping the indexed result to
// non-transparent bounds. That way we can assign frame control bounds and encode
// less data. See GifEncoder for the implementation there.
// Write the first frame.
FrameControl frameControl = this.WriteFrameControlChunk(stream, currentFrame, 0);
this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false);
// Capture the global palette for reuse on subsequent frames.
ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
// Write following frames.
uint increment = 0;
for (int i = 1; i < image.Frames.Count; i++)
{
currentFrame = image.Frames[i];
if (clearTransparency)
{
// Dispose of previous clone and reassign.
clonedFrame?.Dispose();
currentFrame = clonedFrame = currentFrame.Clone();
ClearTransparentPixels(currentFrame);
}
// Each frame control sequence number must be incremented by the
// number of frame data chunks that follow.
frameControl = this.WriteFrameControlChunk(stream, currentFrame, (uint)i + increment);
// Dispose of previous quantized frame and reassign.
quantized?.Dispose();
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, previousPalette);
increment += this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true);
}
}
else
{
FrameControl frameControl = new((uint)this.width, (uint)this.height);
this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false);
}
this.WriteEndChunk(stream); this.WriteEndChunk(stream);
stream.Flush(); stream.Flush();
// Dispose of allocations from final frame.
clonedFrame?.Dispose();
quantized?.Dispose(); quantized?.Dispose();
clonedImage?.Dispose();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -181,18 +232,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
this.previousScanline?.Dispose(); this.previousScanline?.Dispose();
this.currentScanline?.Dispose(); this.currentScanline?.Dispose();
this.previousScanline = null;
this.currentScanline = null;
} }
/// <summary> /// <summary>
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases. /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The cloned image where the transparent pixels will be changed.</param> /// <param name="clone">The cloned image frame where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(Image<TPixel> image) private static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone)
where TPixel : unmanaged, IPixel<TPixel> => where TPixel : unmanaged, IPixel<TPixel>
image.ProcessPixelRows(accessor => => clone.ProcessPixelRows(accessor =>
{ {
// TODO: We should be able to speed this up with SIMD and masking. // TODO: We should be able to speed this up with SIMD and masking.
Rgba32 rgba32 = default; Rgba32 rgba32 = default;
@ -204,7 +253,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
span[x].ToRgba32(ref rgba32); span[x].ToRgba32(ref rgba32);
if (rgba32.A == 0) if (rgba32.A is 0)
{ {
span[x].FromRgba32(transparent); span[x].FromRgba32(transparent);
} }
@ -216,24 +265,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Creates the quantized image and calculates and sets the bit depth. /// Creates the quantized image and calculates and sets the bit depth.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image to quantize.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param> /// <param name="frame">The frame to quantize.</param>
/// <param name="previousPalette">Any previously derived palette.</param>
/// <returns>The quantized image.</returns> /// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel> CreateQuantizedImageAndUpdateBitDepth<TPixel>( private IndexedImageFrame<TPixel>? CreateQuantizedImageAndUpdateBitDepth<TPixel>(
Image<TPixel> image, PngMetadata metadata,
Image<TPixel> clonedImage) ImageFrame<TPixel> frame,
ReadOnlyMemory<TPixel>? previousPalette)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
IndexedImageFrame<TPixel> quantized; IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, previousPalette);
if (this.encoder.TransparentColorMode == PngTransparentColorMode.Clear)
{
quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, clonedImage);
}
else
{
quantized = CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, image);
}
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized; return quantized;
} }
@ -244,9 +286,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private void CollectGrayscaleBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan) private void CollectGrayscaleBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
Span<byte> rawScanlineSpan = this.currentScanline.GetSpan(); Span<byte> rawScanlineSpan = this.currentScanline.GetSpan();
ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan);
if (this.colorType == PngColorType.Grayscale) if (this.colorType == PngColorType.Grayscale)
{ {
@ -402,20 +442,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="rowSpan">The row span.</param> /// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param> /// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param> /// <param name="row">The row.</param>
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row) private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel>? quantized, int row)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
switch (this.colorType) switch (this.colorType)
{ {
case PngColorType.Palette: case PngColorType.Palette:
if (this.bitDepth < 8) if (this.bitDepth < 8)
{ {
PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.DangerousGetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth); PngEncoderHelpers.ScaleDownFrom8BitArray(quantized!.DangerousGetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
} }
else else
{ {
quantized.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan()); quantized?.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan());
} }
break; break;
@ -476,7 +515,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
ReadOnlySpan<TPixel> rowSpan, ReadOnlySpan<TPixel> rowSpan,
ref Span<byte> filter, ref Span<byte> filter,
ref Span<byte> attempt, ref Span<byte> attempt,
IndexedImageFrame<TPixel> quantized, IndexedImageFrame<TPixel>? quantized,
int row) int row)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -576,6 +615,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size); this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size);
} }
/// <summary>
/// Writes the animation control chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="framesCount">The number of frames.</param>
/// <param name="playsCount">The number of times to loop this APNG.</param>
private void WriteAnimationControlChunk(Stream stream, int framesCount, int playsCount)
{
AnimationControl acTL = new(framesCount, playsCount);
acTL.WriteTo(this.chunkDataBuffer.Span);
this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, AnimationControl.Size);
}
/// <summary> /// <summary>
/// Writes the palette chunk to the stream. /// Writes the palette chunk to the stream.
/// Should be written before the first IDAT chunk. /// Should be written before the first IDAT chunk.
@ -583,7 +637,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="quantized">The quantized frame.</param> /// <param name="quantized">The quantized frame.</param>
private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel> quantized) private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel>? quantized)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (quantized is null) if (quantized is null)
@ -642,14 +696,14 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="meta">The image metadata.</param> /// <param name="meta">The image metadata.</param>
private void WritePhysicalChunk(Stream stream, ImageMetadata meta) private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
{ {
if ((this.chunkFilter & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk) if (this.chunkFilter.HasFlag(PngChunkFilter.ExcludePhysicalChunk))
{ {
return; return;
} }
PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span); PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span);
this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PhysicalChunkData.Size); this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PngPhysical.Size);
} }
/// <summary> /// <summary>
@ -691,9 +745,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
return; return;
} }
byte[] xmpData = meta.XmpProfile.Data; byte[]? xmpData = meta.XmpProfile.Data;
if (xmpData.Length == 0) if (xmpData?.Length is 0 or null)
{ {
return; return;
} }
@ -760,18 +814,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
} }
const int maxLatinCode = 255; const int maxLatinCode = 255;
for (int i = 0; i < meta.TextData.Count; i++) foreach (PngTextData textData in meta.TextData)
{ {
PngTextData textData = meta.TextData[i]; bool hasUnicodeCharacters = textData.Value.Any(c => c > maxLatinCode);
bool hasUnicodeCharacters = false;
foreach (char c in textData.Value)
{
if (c > maxLatinCode)
{
hasUnicodeCharacters = true;
break;
}
}
if (hasUnicodeCharacters || !string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword)) if (hasUnicodeCharacters || !string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))
{ {
@ -875,7 +920,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
// 4-byte unsigned integer of gamma * 100,000. // 4-byte unsigned integer of gamma * 100,000.
uint gammaValue = (uint)(this.gamma * 100_000F); uint gammaValue = (uint)(this.gamma * 100_000F);
BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span.Slice(0, 4), gammaValue); BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue);
this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4); this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4);
} }
@ -889,7 +934,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;
} }
@ -897,19 +942,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span<byte> alpha = this.chunkDataBuffer.Span; Span<byte> alpha = this.chunkDataBuffer.Span;
if (pngMetadata.ColorType == PngColorType.Rgb) if (pngMetadata.ColorType == PngColorType.Rgb)
{ {
if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit) if (this.use16Bit)
{ {
Rgb48 rgb = pngMetadata.TransparentRgb48.Value; Rgb48 rgb = pngMetadata.TransparentColor.Value.ToPixel<Rgb48>();
BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R); BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); 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);
} }
else if (pngMetadata.TransparentRgb24.HasValue) else
{ {
alpha.Clear(); alpha.Clear();
Rgb24 rgb = pngMetadata.TransparentRgb24.Value; Rgb24 rgb = pngMetadata.TransparentColor.Value.ToRgb24();
alpha[1] = rgb.R; alpha[1] = rgb.R;
alpha[3] = rgb.G; alpha[3] = rgb.G;
alpha[5] = rgb.B; alpha[5] = rgb.B;
@ -918,28 +963,61 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
} }
else if (pngMetadata.ColorType == PngColorType.Grayscale) else if (pngMetadata.ColorType == PngColorType.Grayscale)
{ {
if (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);
} }
else if (pngMetadata.TransparentL8.HasValue) else
{ {
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);
} }
} }
} }
/// <summary>
/// Writes the animation control chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="imageFrame">The image frame.</param>
/// <param name="sequenceNumber">The frame sequence number.</param>
private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber)
{
PngFrameMetadata frameMetadata = imageFrame.Metadata.GetPngFrameMetadata();
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
FrameControl fcTL = new(
sequenceNumber: sequenceNumber,
width: (uint)imageFrame.Width,
height: (uint)imageFrame.Height,
xOffset: 0,
yOffset: 0,
delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator,
delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator,
disposeOperation: frameMetadata.DisposalMethod,
blendOperation: frameMetadata.BlendMethod);
fcTL.WriteTo(this.chunkDataBuffer.Span);
this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, FrameControl.Size);
return fcTL;
}
/// <summary> /// <summary>
/// Writes the pixel information to the stream. /// Writes the pixel information to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The image.</param> /// <param name="frameControl">The frame control</param>
/// <param name="pixels">The frame.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param> /// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
private void WriteDataChunks<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream) /// <param name="isFrame">Is writing fdAT or IDAT.</param>
private uint WriteDataChunks<TPixel>(FrameControl frameControl, ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, Stream stream, bool isFrame)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
byte[] buffer; byte[] buffer;
@ -949,20 +1027,20 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel)) using (ZlibDeflateStream deflateStream = new(this.memoryAllocator, memoryStream, this.encoder.CompressionLevel))
{ {
if (this.interlaceMode == PngInterlaceMode.Adam7) if (this.interlaceMode is PngInterlaceMode.Adam7)
{ {
if (quantized != null) if (quantized is not null)
{ {
this.EncodeAdam7IndexedPixels(quantized, deflateStream); this.EncodeAdam7IndexedPixels(frameControl, quantized, deflateStream);
} }
else else
{ {
this.EncodeAdam7Pixels(pixels, deflateStream); this.EncodeAdam7Pixels(frameControl, pixels, deflateStream);
} }
} }
else else
{ {
this.EncodePixels(pixels, quantized, deflateStream); this.EncodePixels(frameControl, pixels, quantized, deflateStream);
} }
} }
@ -972,24 +1050,42 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
// Store the chunks in repeated 64k blocks. // Store the chunks in repeated 64k blocks.
// This reduces the memory load for decoding the image for many decoders. // This reduces the memory load for decoding the image for many decoders.
int numChunks = bufferLength / MaxBlockSize; int maxBlockSize = MaxBlockSize;
if (isFrame)
{
maxBlockSize -= 4;
}
if (bufferLength % MaxBlockSize != 0) int numChunks = bufferLength / maxBlockSize;
if (bufferLength % maxBlockSize != 0)
{ {
numChunks++; numChunks++;
} }
for (int i = 0; i < numChunks; i++) for (int i = 0; i < numChunks; i++)
{ {
int length = bufferLength - (i * MaxBlockSize); int length = bufferLength - (i * maxBlockSize);
if (length > MaxBlockSize) if (length > maxBlockSize)
{ {
length = MaxBlockSize; length = maxBlockSize;
} }
this.WriteChunk(stream, PngChunkType.Data, buffer, i * MaxBlockSize, length); if (isFrame)
{
// We increment the sequence number for each frame chunk.
// '1' is added to the sequence number to account for the preceding frame control chunk.
uint sequenceNumber = (uint)(frameControl.SequenceNumber + 1 + i);
this.WriteFrameDataChunk(stream, sequenceNumber, buffer, i * maxBlockSize, length);
}
else
{
this.WriteChunk(stream, PngChunkType.Data, buffer, i * maxBlockSize, length);
}
} }
return (uint)numChunks;
} }
/// <summary> /// <summary>
@ -1009,13 +1105,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Encodes the pixels. /// Encodes the pixels.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="pixels">The pixels.</param> /// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param> /// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</param> /// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream) private void EncodePixels<TPixel>(FrameControl frameControl, ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesPerScanline = this.CalculateScanlineLength(this.width); int width = (int)frameControl.Width;
int height = (int)frameControl.Height;
int bytesPerScanline = this.CalculateScanlineLength(width);
int filterLength = bytesPerScanline + 1; int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline); this.AllocateScanlineBuffers(bytesPerScanline);
@ -1026,7 +1126,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
Span<byte> filter = filterBuffer.GetSpan(); Span<byte> filter = filterBuffer.GetSpan();
Span<byte> attempt = attemptBuffer.GetSpan(); Span<byte> attempt = attemptBuffer.GetSpan();
for (int y = 0; y < this.height; y++) for (int y = (int)frameControl.YOffset; y < frameControl.YMax; y++)
{ {
this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y); this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y);
deflateStream.Write(filter); deflateStream.Write(filter);
@ -1039,18 +1139,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Interlaced encoding the pixels. /// Interlaced encoding the pixels.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image.</param> /// <param name="frameControl">The frame control</param>
/// <param name="frame">The image frame.</param>
/// <param name="deflateStream">The deflate stream.</param> /// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7Pixels<TPixel>(Image<TPixel> image, ZlibDeflateStream deflateStream) private void EncodeAdam7Pixels<TPixel>(FrameControl frameControl, ImageFrame<TPixel> frame, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = image.Width; int width = (int)frameControl.XMax;
int height = image.Height; int height = (int)frameControl.YMax;
Buffer2D<TPixel> pixelBuffer = image.Frames.RootFrame.PixelBuffer; Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
for (int pass = 0; pass < 7; pass++) for (int pass = 0; pass < 7; pass++)
{ {
int startRow = Adam7.FirstRow[pass]; int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset;
int startCol = Adam7.FirstColumn[pass]; int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset;
int blockWidth = Adam7.ComputeBlockWidth(width, pass); int blockWidth = Adam7.ComputeBlockWidth(width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1 int bytesPerScanline = this.bytesPerPixel <= 1
@ -1072,7 +1173,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{ {
// Collect pixel data // Collect pixel data
Span<TPixel> srcRow = pixelBuffer.DangerousGetRowSpan(row); Span<TPixel> srcRow = pixelBuffer.DangerousGetRowSpan(row);
for (int col = startCol, i = 0; col < width; col += Adam7.ColumnIncrement[pass]) for (int col = startCol, i = 0; col < frameControl.XMax; col += Adam7.ColumnIncrement[pass])
{ {
block[i++] = srcRow[col]; block[i++] = srcRow[col];
} }
@ -1092,17 +1193,18 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Interlaced encoding the quantized (indexed, with palette) pixels. /// Interlaced encoding the quantized (indexed, with palette) pixels.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="quantized">The quantized.</param> /// <param name="quantized">The quantized.</param>
/// <param name="deflateStream">The deflate stream.</param> /// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7IndexedPixels<TPixel>(IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream) private void EncodeAdam7IndexedPixels<TPixel>(FrameControl frameControl, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = quantized.Width; int width = (int)frameControl.Width;
int height = quantized.Height; int endRow = (int)frameControl.YMax;
for (int pass = 0; pass < 7; pass++) for (int pass = 0; pass < 7; pass++)
{ {
int startRow = Adam7.FirstRow[pass]; int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset;
int startCol = Adam7.FirstColumn[pass]; int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset;
int blockWidth = Adam7.ComputeBlockWidth(width, pass); int blockWidth = Adam7.ComputeBlockWidth(width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1 int bytesPerScanline = this.bytesPerPixel <= 1
@ -1121,17 +1223,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span<byte> filter = filterBuffer.GetSpan(); Span<byte> filter = filterBuffer.GetSpan();
Span<byte> attempt = attemptBuffer.GetSpan(); Span<byte> attempt = attemptBuffer.GetSpan();
for (int row = startRow; for (int row = startRow; row < endRow; row += Adam7.RowIncrement[pass])
row < height;
row += Adam7.RowIncrement[pass])
{ {
// Collect data // Collect data
ReadOnlySpan<byte> srcRow = quantized.DangerousGetRowSpan(row); ReadOnlySpan<byte> srcRow = quantized.DangerousGetRowSpan(row);
for (int col = startCol, i = 0; for (int col = startCol, i = 0;
col < width; col < frameControl.XMax;
col += Adam7.ColumnIncrement[pass]) col += Adam7.ColumnIncrement[pass])
{ {
block[i++] = srcRow[col]; block[i] = srcRow[col];
i++;
} }
// Encode data // Encode data
@ -1163,7 +1264,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </summary> /// </summary>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="type">The type of chunk to write.</param> /// <param name="type">The type of chunk to write.</param>
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param> /// <param name="data">The <see cref="Span{Byte}"/> containing data.</param>
/// <param name="offset">The position to offset the data at.</param> /// <param name="offset">The position to offset the data at.</param>
/// <param name="length">The of the data to write.</param> /// <param name="length">The of the data to write.</param>
private void WriteChunk(Stream stream, PngChunkType type, Span<byte> data, int offset, int length) private void WriteChunk(Stream stream, PngChunkType type, Span<byte> data, int offset, int length)
@ -1175,7 +1276,39 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
stream.Write(buffer); stream.Write(buffer);
uint crc = Crc32.Calculate(buffer.Slice(4)); // Write the type buffer uint crc = Crc32.Calculate(buffer[4..]); // Write the type buffer
if (data.Length > 0 && length > 0)
{
stream.Write(data, offset, length);
crc = Crc32.Calculate(crc, data.Slice(offset, length));
}
BinaryPrimitives.WriteUInt32BigEndian(buffer, crc);
stream.Write(buffer, 0, 4); // write the crc
}
/// <summary>
/// Writes a frame data chunk of a specified length to the stream at the given offset.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="sequenceNumber">The frame sequence number.</param>
/// <param name="data">The <see cref="Span{Byte}"/> containing data.</param>
/// <param name="offset">The position to offset the data at.</param>
/// <param name="length">The of the data to write.</param>
private void WriteFrameDataChunk(Stream stream, uint sequenceNumber, Span<byte> data, int offset, int length)
{
Span<byte> buffer = stackalloc byte[12];
BinaryPrimitives.WriteInt32BigEndian(buffer, length + 4);
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), (uint)PngChunkType.FrameData);
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(8, 4), sequenceNumber);
stream.Write(buffer);
uint crc = Crc32.Calculate(buffer[4..]); // Write the type buffer
if (data.Length > 0 && length > 0) if (data.Length > 0 && length > 0)
{ {
@ -1198,7 +1331,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// </returns> /// </returns>
private int CalculateScanlineLength(int width) private int CalculateScanlineLength(int width)
{ {
int mod = this.bitDepth == 16 ? 16 : 8; int mod = this.bitDepth is 16 ? 16 : 8;
int scanlineLength = width * this.bitDepth * this.bytesPerPixel; int scanlineLength = width * this.bitDepth * this.bytesPerPixel;
int amount = scanlineLength % mod; int amount = scanlineLength % mod;
@ -1242,14 +1375,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
if (!encoder.FilterMethod.HasValue) if (!encoder.FilterMethod.HasValue)
{ {
// Specification recommends default filter method None for paletted images and Paeth for others. // Specification recommends default filter method None for paletted images and Paeth for others.
if (this.colorType == PngColorType.Palette) this.filterMethod = this.colorType is PngColorType.Palette ? PngFilterMethod.None : PngFilterMethod.Paeth;
{
this.filterMethod = PngFilterMethod.None;
}
else
{
this.filterMethod = PngFilterMethod.Paeth;
}
} }
// Ensure bit depth and color type are a supported combination. // Ensure bit depth and color type are a supported combination.
@ -1265,7 +1391,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
use16Bit = bits == (byte)PngBitDepth.Bit16; use16Bit = bits == (byte)PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit); bytesPerPixel = CalculateBytesPerPixel(this.colorType, use16Bit);
this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod).Value; this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod)!.Value;
this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None; this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None;
} }
@ -1276,28 +1402,50 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="encoder">The png encoder.</param> /// <param name="encoder">The png encoder.</param>
/// <param name="colorType">The color type.</param> /// <param name="colorType">The color type.</param>
/// <param name="bitDepth">The bits per component.</param> /// <param name="bitDepth">The bits per component.</param>
/// <param name="image">The image.</param> /// <param name="metadata">The image metadata.</param>
private static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>( /// <param name="frame">The frame to quantize.</param>
/// <param name="previousPalette">Any previously derived palette.</param>
private IndexedImageFrame<TPixel>? CreateQuantizedFrame<TPixel>(
QuantizingImageEncoder encoder, QuantizingImageEncoder encoder,
PngColorType colorType, PngColorType colorType,
byte bitDepth, byte bitDepth,
Image<TPixel> image) PngMetadata metadata,
ImageFrame<TPixel> frame,
ReadOnlyMemory<TPixel>? previousPalette)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (colorType != PngColorType.Palette) if (colorType is not PngColorType.Palette)
{ {
return null; return null;
} }
if (previousPalette is not null)
{
// Use the previously derived palette created by quantizing the root frame to quantize the current frame.
using PaletteQuantizer<TPixel> paletteQuantizer = new(this.configuration, this.quantizer!.Options, previousPalette.Value, -1);
paletteQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
return paletteQuantizer.QuantizeFrame(frame, frame.Bounds());
}
// 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.
IQuantizer quantizer = encoder.Quantizer if (this.quantizer is null)
?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); {
if (metadata.ColorTable is not null)
{
// Use the provided palette. The caller is responsible for setting values.
this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value);
}
else
{
this.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>(image.GetConfiguration()); using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(frame.Configuration);
frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image); frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
return frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); return frameQuantizer.QuantizeFrame(frame, frame.Bounds());
} }
/// <summary> /// <summary>
@ -1311,25 +1459,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private static byte CalculateBitDepth<TPixel>( private static byte CalculateBitDepth<TPixel>(
PngColorType colorType, PngColorType colorType,
byte bitDepth, byte bitDepth,
IndexedImageFrame<TPixel> quantizedFrame) IndexedImageFrame<TPixel>? quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (colorType == PngColorType.Palette) if (colorType is PngColorType.Palette)
{ {
byte quantizedBits = (byte)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(quantizedFrame.Palette.Length), 1, 8); byte quantizedBits = (byte)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(quantizedFrame!.Palette.Length), 1, 8);
byte bits = Math.Max(bitDepth, quantizedBits); byte bits = Math.Max(bitDepth, quantizedBits);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
// We check again for the bit depth as the bit depth of the color palette from a given quantizer might not // We check again for the bit depth as the bit depth of the color palette from a given quantizer might not
// be within the acceptable range. // be within the acceptable range.
if (bits == 3) bits = bits switch
{ {
bits = 4; 3 => 4,
} >= 5 and <= 7 => 8,
else if (bits is >= 5 and <= 7) _ => bits
{ };
bits = 8;
}
bitDepth = bits; bitDepth = bits;
} }
@ -1367,21 +1513,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <typeparam name="TPixel">The type of pixel format.</typeparam> /// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngColorType SuggestColorType<TPixel>() private static PngColorType SuggestColorType<TPixel>()
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> typeof(TPixel) switch => default(TPixel) switch
{ {
Type t when t == typeof(A8) => PngColorType.GrayscaleWithAlpha, A8 => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Argb32) => PngColorType.RgbWithAlpha, Argb32 => PngColorType.RgbWithAlpha,
Type t when t == typeof(Bgr24) => PngColorType.Rgb, Bgr24 => PngColorType.Rgb,
Type t when t == typeof(Bgra32) => PngColorType.RgbWithAlpha, Bgra32 => PngColorType.RgbWithAlpha,
Type t when t == typeof(L8) => PngColorType.Grayscale, L8 => PngColorType.Grayscale,
Type t when t == typeof(L16) => PngColorType.Grayscale, L16 => PngColorType.Grayscale,
Type t when t == typeof(La16) => PngColorType.GrayscaleWithAlpha, La16 => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(La32) => PngColorType.GrayscaleWithAlpha, La32 => PngColorType.GrayscaleWithAlpha,
Type t when t == typeof(Rgb24) => PngColorType.Rgb, Rgb24 => PngColorType.Rgb,
Type t when t == typeof(Rgba32) => PngColorType.RgbWithAlpha, Rgba32 => PngColorType.RgbWithAlpha,
Type t when t == typeof(Rgb48) => PngColorType.Rgb, Rgb48 => PngColorType.Rgb,
Type t when t == typeof(Rgba64) => PngColorType.RgbWithAlpha, Rgba64 => PngColorType.RgbWithAlpha,
Type t when t == typeof(RgbaVector) => PngColorType.RgbWithAlpha, RgbaVector => PngColorType.RgbWithAlpha,
_ => PngColorType.RgbWithAlpha _ => PngColorType.RgbWithAlpha
}; };
@ -1392,27 +1538,27 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <typeparam name="TPixel">The type of pixel format.</typeparam> /// <typeparam name="TPixel">The type of pixel format.</typeparam>
private static PngBitDepth SuggestBitDepth<TPixel>() private static PngBitDepth SuggestBitDepth<TPixel>()
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> typeof(TPixel) switch => default(TPixel) switch
{ {
Type t when t == typeof(A8) => PngBitDepth.Bit8, A8 => PngBitDepth.Bit8,
Type t when t == typeof(Argb32) => PngBitDepth.Bit8, Argb32 => PngBitDepth.Bit8,
Type t when t == typeof(Bgr24) => PngBitDepth.Bit8, Bgr24 => PngBitDepth.Bit8,
Type t when t == typeof(Bgra32) => PngBitDepth.Bit8, Bgra32 => PngBitDepth.Bit8,
Type t when t == typeof(L8) => PngBitDepth.Bit8, L8 => PngBitDepth.Bit8,
Type t when t == typeof(L16) => PngBitDepth.Bit16, L16 => PngBitDepth.Bit16,
Type t when t == typeof(La16) => PngBitDepth.Bit8, La16 => PngBitDepth.Bit8,
Type t when t == typeof(La32) => PngBitDepth.Bit16, La32 => PngBitDepth.Bit16,
Type t when t == typeof(Rgb24) => PngBitDepth.Bit8, Rgb24 => PngBitDepth.Bit8,
Type t when t == typeof(Rgba32) => PngBitDepth.Bit8, Rgba32 => PngBitDepth.Bit8,
Type t when t == typeof(Rgb48) => PngBitDepth.Bit16, Rgb48 => PngBitDepth.Bit16,
Type t when t == typeof(Rgba64) => PngBitDepth.Bit16, Rgba64 => PngBitDepth.Bit16,
Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16, RgbaVector => PngBitDepth.Bit16,
_ => PngBitDepth.Bit8 _ => PngBitDepth.Bit8
}; };
private unsafe struct ScratchBuffer private unsafe struct ScratchBuffer
{ {
private const int Size = 16; private const int Size = 26;
private fixed byte scratch[Size]; private fixed byte scratch[Size];
public Span<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); public Span<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);

5
src/ImageSharp/Formats/Png/PngFormat.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary> /// <summary>
/// Registers the image encoders, decoders and mime type detectors for the png format. /// Registers the image encoders, decoders and mime type detectors for the png format.
/// </summary> /// </summary>
public sealed class PngFormat : IImageFormat<PngMetadata> public sealed class PngFormat : IImageFormat<PngMetadata, PngFrameMetadata>
{ {
private PngFormat() private PngFormat()
{ {
@ -31,4 +31,7 @@ public sealed class PngFormat : IImageFormat<PngMetadata>
/// <inheritdoc/> /// <inheritdoc/>
public PngMetadata CreateDefaultFormatMetadata() => new(); public PngMetadata CreateDefaultFormatMetadata() => new();
/// <inheritdoc/>
public PngFrameMetadata CreateDefaultFormatFrameMetadata() => new();
} }

62
src/ImageSharp/Formats/Png/PngFrameMetadata.cs

@ -0,0 +1,62 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks;
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Provides APng specific metadata information for the image frame.
/// </summary>
public class PngFrameMetadata : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
/// </summary>
public PngFrameMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private PngFrameMetadata(PngFrameMetadata other)
{
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
this.BlendMethod = other.BlendMethod;
}
/// <summary>
/// Gets or sets the frame delay for animated images.
/// If not 0, when utilized in Png animation, this field specifies the number of hundredths (1/100) of a second to
/// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered.
/// </summary>
public Rational FrameDelay { get; set; }
/// <summary>
/// Gets or sets the type of frame area disposal to be done after rendering this frame
/// </summary>
public PngDisposalMethod DisposalMethod { get; set; }
/// <summary>
/// Gets or sets the type of frame area rendering for this frame
/// </summary>
public PngBlendMethod BlendMethod { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PngFrameMetadata"/> class.
/// </summary>
/// <param name="frameControl">The chunk to create an instance from.</param>
internal void FromChunk(in FrameControl frameControl)
{
this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator);
this.DisposalMethod = frameControl.DisposeOperation;
this.BlendMethod = frameControl.BlendOperation;
}
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngFrameMetadata(this);
}

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
@ -27,11 +28,13 @@ 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.RepeatCount = other.RepeatCount;
this.TransparentL16 = other.TransparentL16;
this.TransparentRgb24 = other.TransparentRgb24; if (other.ColorTable?.Length > 0)
this.TransparentRgb48 = other.TransparentRgb48; {
this.ColorTable = other.ColorTable.Value.ToArray();
}
for (int i = 0; i < other.TextData.Count; i++) for (int i = 0; i < other.TextData.Count; i++)
{ {
@ -61,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> /// </summary>
public Rgb48? TransparentRgb48 { get; set; } public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary> /// <summary>
/// Gets or sets the 8 bit grayscale transparent color. /// Gets or sets the transparent color used with non palette based images, if a transparency chunk and markers were decoded.
/// This represents any color in an 8 bit grayscale encoded png that should be transparent.
/// </summary> /// </summary>
public L8? TransparentL8 { get; set; } public Color? TransparentColor { 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>
public L16? TransparentL16 { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the image contains a transparency chunk and markers were decoded.
/// </summary>
public bool HasTransparency { 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.
@ -95,6 +79,11 @@ public class PngMetadata : IDeepCloneable
/// </summary> /// </summary>
public IList<PngTextData> TextData { get; set; } = new List<PngTextData>(); public IList<PngTextData> TextData { get; set; } = new List<PngTextData>();
/// <summary>
/// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping.
/// </summary>
public int RepeatCount { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetadata(this); public IDeepCloneable DeepClone() => new PngMetadata(this);
} }

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

@ -4,6 +4,7 @@
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.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
@ -15,96 +16,43 @@ namespace SixLabors.ImageSharp.Formats.Png;
internal static class PngScanlineProcessor internal static class PngScanlineProcessor
{ {
public static void ProcessGrayscaleScanline<TPixel>( public static void ProcessGrayscaleScanline<TPixel>(
in PngHeader header, int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
bool hasTrans, Color? transparentColor)
L16 luminance16Trans, where TPixel : unmanaged, IPixel<TPixel> =>
L8 luminanceTrans) ProcessInterlacedGrayscaleScanline(
where TPixel : unmanaged, IPixel<TPixel> bitDepth,
{ frameControl,
TPixel pixel = default; scanlineSpan,
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); rowSpan,
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); 0,
int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1); 1,
transparentColor);
if (!hasTrans)
{
if (header.BitDepth == 16)
{
int o = 0;
for (nuint x = 0; x < (uint)header.Width; x++, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
pixel.FromL16(Unsafe.As<ushort, L16>(ref luminance));
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
for (nuint x = 0; x < (uint)header.Width; x++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
pixel.FromL8(Unsafe.As<byte, L8>(ref luminance));
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return;
}
if (header.BitDepth == 16)
{
La32 source = default;
int o = 0;
for (nuint x = 0; x < (uint)header.Width; x++, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.L = luminance;
source.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue;
pixel.FromLa32(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
La16 source = default;
byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor);
for (nuint x = 0; x < (uint)header.Width; x++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
source.L = luminance;
source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue;
pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedGrayscaleScanline<TPixel>( public static void ProcessInterlacedGrayscaleScanline<TPixel>(
in PngHeader header, int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
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 + 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);
int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1); int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(bitDepth) - 1);
if (!hasTrans) if (transparentColor is null)
{ {
if (header.BitDepth == 16) if (bitDepth == 16)
{ {
int o = 0; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 2) for (nuint x = offset; x < frameControl.XMax; x += increment, o += 2)
{ {
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
pixel.FromL16(Unsafe.As<ushort, L16>(ref luminance)); pixel.FromL16(Unsafe.As<ushort, L16>(ref luminance));
@ -113,7 +61,7 @@ internal static class PngScanlineProcessor
} }
else else
{ {
for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++)
{ {
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
pixel.FromL8(Unsafe.As<byte, L8>(ref luminance)); pixel.FromL8(Unsafe.As<byte, L8>(ref luminance));
@ -124,15 +72,16 @@ internal static class PngScanlineProcessor
return; return;
} }
if (header.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 = pixelOffset; x < (uint)header.Width; x += increment, o += 2) for (nuint x = offset; x < frameControl.XMax; 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;
@ -140,13 +89,13 @@ 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); for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++)
for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; 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;
@ -155,47 +104,26 @@ internal static class PngScanlineProcessor
} }
public static void ProcessGrayscaleWithAlphaScanline<TPixel>( public static void ProcessGrayscaleWithAlphaScanline<TPixel>(
in PngHeader header, int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
uint bytesPerPixel, uint bytesPerPixel,
uint bytesPerSample) uint bytesPerSample)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel> =>
{ ProcessInterlacedGrayscaleWithAlphaScanline(
TPixel pixel = default; bitDepth,
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); frameControl,
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); scanlineSpan,
rowSpan,
if (header.BitDepth == 16) 0,
{ 1,
La32 source = default; bytesPerPixel,
int o = 0; bytesPerSample);
for (nuint x = 0; x < (uint)header.Width; x++, o += 4)
{
source.L = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
pixel.FromLa32(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
La16 source = default;
for (nuint x = 0; x < (uint)header.Width; x++)
{
nuint offset = x * bytesPerPixel;
source.L = Unsafe.Add(ref scanlineSpanRef, offset);
source.A = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample);
pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedGrayscaleWithAlphaScanline<TPixel>( public static void ProcessInterlacedGrayscaleWithAlphaScanline<TPixel>(
in PngHeader header, int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
uint pixelOffset, uint pixelOffset,
@ -204,15 +132,16 @@ internal static class PngScanlineProcessor
uint bytesPerSample) uint bytesPerSample)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
uint offset = pixelOffset + 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);
if (header.BitDepth == 16) if (bitDepth == 16)
{ {
La32 source = default; La32 source = default;
int o = 0; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += 4) for (nuint x = offset; x < frameControl.XMax; x += increment, o += 4)
{ {
source.L = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); source.L = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
source.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); source.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
@ -224,135 +153,108 @@ internal static class PngScanlineProcessor
else else
{ {
La16 source = default; La16 source = default;
nuint offset = 0; nuint offset2 = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment) for (nuint x = offset; x < frameControl.XMax; x += increment)
{ {
source.L = Unsafe.Add(ref scanlineSpanRef, offset); source.L = Unsafe.Add(ref scanlineSpanRef, offset2);
source.A = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample); source.A = Unsafe.Add(ref scanlineSpanRef, offset2 + bytesPerSample);
pixel.FromLa16(source); pixel.FromLa16(source);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
offset += bytesPerPixel; offset2 += bytesPerPixel;
} }
} }
} }
public static void ProcessPaletteScanline<TPixel>( public static void ProcessPaletteScanline<TPixel>(
in PngHeader header, in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
ReadOnlySpan<byte> palette, ReadOnlyMemory<Color>? palette)
byte[] paletteAlpha) where TPixel : unmanaged, IPixel<TPixel> =>
where TPixel : unmanaged, IPixel<TPixel> ProcessInterlacedPaletteScanline(
{ frameControl,
if (palette.IsEmpty) scanlineSpan,
{ rowSpan,
PngThrowHelper.ThrowMissingPalette(); 0,
} 1,
palette);
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(palette);
ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
if (paletteAlpha?.Length > 0)
{
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
Rgba32 rgba = default;
ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha);
for (nuint x = 0; x < (uint)header.Width; x++)
{
uint index = Unsafe.Add(ref scanlineSpanRef, x);
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue;
pixel.FromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
for (nuint x = 0; x < (uint)header.Width; x++)
{
int index = Unsafe.Add(ref scanlineSpanRef, x);
Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index);
pixel.FromRgb24(rgb);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedPaletteScanline<TPixel>( public static void ProcessInterlacedPaletteScanline<TPixel>(
in PngHeader header, in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
uint pixelOffset, uint pixelOffset,
uint increment, uint increment,
ReadOnlySpan<byte> palette, ReadOnlyMemory<Color>? palette)
byte[] paletteAlpha)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (palette is null)
{
PngThrowHelper.ThrowMissingPalette();
}
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)
{
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
Rgba32 rgba = default;
ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha);
for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++)
{
uint index = Unsafe.Add(ref scanlineSpanRef, o);
rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue;
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
pixel.FromRgba32(rgba); for (nuint x = pixelOffset, o = 0; x < frameControl.XMax; x += increment, o++)
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{ {
for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) uint index = Unsafe.Add(ref scanlineSpanRef, o);
{ pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());
int index = Unsafe.Add(ref scanlineSpanRef, o); Unsafe.Add(ref rowSpanRef, x) = pixel;
Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index);
pixel.FromRgb24(rgb);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
} }
} }
public static void ProcessRgbScanline<TPixel>( public static void ProcessRgbScanline<TPixel>(
Configuration configuration, Configuration configuration,
in PngHeader header, int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample,
Color? transparentColor)
where TPixel : unmanaged, IPixel<TPixel> =>
ProcessInterlacedRgbScanline(
configuration,
bitDepth,
frameControl,
scanlineSpan,
rowSpan,
0,
1,
bytesPerPixel,
bytesPerSample,
transparentColor);
public static void ProcessInterlacedRgbScanline<TPixel>(
Configuration configuration,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
uint pixelOffset,
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 + frameControl.XOffset;
TPixel pixel = default; TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (!hasTrans) if (transparentColor is null)
{ {
if (header.BitDepth == 16) if (bitDepth == 16)
{ {
Rgb48 rgb48 = default; Rgb48 rgb48 = default;
int o = 0; int o = 0;
for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel) for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{ {
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
@ -362,27 +264,47 @@ internal static class PngScanlineProcessor
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
} }
} }
else if (pixelOffset == 0 && increment == 1)
{
PixelOperations<TPixel>.Instance.FromRgb24Bytes(
configuration,
scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)],
rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width),
(int)frameControl.Width);
}
else else
{ {
PixelOperations<TPixel>.Instance.FromRgb24Bytes(configuration, scanlineSpan, rowSpan, header.Width); Rgb24 rgb = default;
int o = 0;
for (nuint x = offset; x < frameControl.XMax; 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 (header.BitDepth == 16) if (bitDepth == 16)
{ {
Rgb48 transparent = transparentColor.Value.ToPixel<Rgb48>();
Rgb48 rgb48 = default; Rgb48 rgb48 = default;
Rgba64 rgba64 = default; Rgba64 rgba64 = default;
int o = 0; int o = 0;
for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel) for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{ {
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.Rgb = rgb48; rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue;
pixel.FromRgba64(rgba64); pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -390,142 +312,47 @@ internal static class PngScanlineProcessor
} }
else else
{ {
Rgba32 rgba32 = default; Rgb24 transparent = transparentColor.Value.ToPixel<Rgb24>();
ReadOnlySpan<Rgb24> rgb24Span = MemoryMarshal.Cast<byte, Rgb24>(scanlineSpan);
ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span);
for (nuint x = 0; x < (uint)header.Width; x++)
{
ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x);
rgba32.Rgb = rgb24;
rgba32.A = rgb24.Equals(rgb24Trans) ? byte.MinValue : byte.MaxValue;
pixel.FromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedRgbScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
uint pixelOffset,
uint increment,
int bytesPerPixel,
int bytesPerSample,
bool hasTrans,
Rgb48 rgb48Trans,
Rgb24 rgb24Trans)
where TPixel : unmanaged, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
if (hasTrans)
{
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
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(rgb48Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgb48 rgb48 = 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));
pixel.FromRgb48(rgb48);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return;
}
if (hasTrans)
{
Rgba32 rgba = default; Rgba32 rgba = default;
int o = 0; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{ {
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;
} }
} }
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;
}
}
} }
public static void ProcessRgbaScanline<TPixel>( public static void ProcessRgbaScanline<TPixel>(
Configuration configuration, Configuration configuration,
in PngHeader header, int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
int bytesPerPixel, int bytesPerPixel,
int bytesPerSample) int bytesPerSample)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel> =>
{ ProcessInterlacedRgbaScanline(
TPixel pixel = default; configuration,
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); bitDepth,
frameControl,
if (header.BitDepth == 16) scanlineSpan,
{ rowSpan,
Rgba64 rgba64 = default; 0,
int o = 0; 1,
for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel) bytesPerPixel,
{ bytesPerSample);
rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample));
pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
PixelOperations<TPixel>.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan, header.Width);
}
}
public static void ProcessInterlacedRgbaScanline<TPixel>( public static void ProcessInterlacedRgbaScanline<TPixel>(
in PngHeader header, Configuration configuration,
int bitDepth,
in FrameControl frameControl,
ReadOnlySpan<byte> scanlineSpan, ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan, Span<TPixel> rowSpan,
uint pixelOffset, uint pixelOffset,
@ -534,15 +361,15 @@ internal static class PngScanlineProcessor
int bytesPerSample) int bytesPerSample)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
uint offset = pixelOffset + frameControl.XOffset;
TPixel pixel = default; TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16) if (bitDepth == 16)
{ {
Rgba64 rgba64 = default; Rgba64 rgba64 = default;
int o = 0; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{ {
rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
@ -553,11 +380,20 @@ internal static class PngScanlineProcessor
Unsafe.Add(ref rowSpanRef, x) = pixel; Unsafe.Add(ref rowSpanRef, x) = pixel;
} }
} }
else if (pixelOffset == 0 && increment == 1)
{
PixelOperations<TPixel>.Instance.FromRgba32Bytes(
configuration,
scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)],
rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width),
(int)frameControl.Width);
}
else else
{ {
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
Rgba32 rgba = default; Rgba32 rgba = default;
int o = 0; int o = 0;
for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel)
{ {
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));

26
src/ImageSharp/Formats/Png/PngThrowHelper.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
@ -12,13 +13,22 @@ internal static class PngThrowHelper
=> throw new InvalidImageContentException(errorMessage, innerException); => throw new InvalidImageContentException(errorMessage, innerException);
[DoesNotReturn] [DoesNotReturn]
public static void ThrowNoHeader() => throw new InvalidImageContentException("PNG Image does not contain a header chunk"); public static void ThrowInvalidHeader() => throw new InvalidImageContentException("PNG Image must contain a header chunk and it must be located before any other chunks.");
[DoesNotReturn] [DoesNotReturn]
public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk"); public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk.");
[DoesNotReturn] [DoesNotReturn]
public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk"); public static void ThrowMissingDefaultData() => throw new InvalidImageContentException("APNG Image does not contain a default data chunk.");
[DoesNotReturn]
public static void ThrowInvalidAnimationControl() => throw new InvalidImageContentException("APNG Image must contain a acTL chunk and it must be located before any IDAT and fdAT chunks.");
[DoesNotReturn]
public static void ThrowMissingFrameControl() => throw new InvalidImageContentException("One of APNG Image's frames do not have a frame control chunk.");
[DoesNotReturn]
public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk.");
[DoesNotReturn] [DoesNotReturn]
public static void ThrowInvalidChunkType() => throw new InvalidImageContentException("Invalid PNG data."); public static void ThrowInvalidChunkType() => throw new InvalidImageContentException("Invalid PNG data.");
@ -30,7 +40,15 @@ internal static class PngThrowHelper
public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!");
[DoesNotReturn] [DoesNotReturn]
public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type"); public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression(nameof(value))] string name = "")
=> throw new NotSupportedException($"Invalid {name}. {message}. Was '{value}'.");
[DoesNotReturn]
public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression(nameof(value1))] string name1 = "", [CallerArgumentExpression(nameof(value1))] string name2 = "")
=> throw new NotSupportedException($"Invalid {name1} or {name2}. {message}. Was '{value1}' and '{value2}'.");
[DoesNotReturn]
public static void ThrowNotSupportedColor() => throw new NotSupportedException("Unsupported PNG color type.");
[DoesNotReturn] [DoesNotReturn]
public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type."); public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type.");

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 />

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

3
src/ImageSharp/Formats/Tiff/TiffEncoder.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.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; using SixLabors.ImageSharp.Processing;
@ -48,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);
} }
} }

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

@ -128,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();

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;

21
src/ImageSharp/ImageFrameCollection{TPixel}.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Collections; using System.Collections;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -24,7 +23,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 +31,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 +137,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 +152,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 +168,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 +269,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 +284,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 +298,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 +364,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 +413,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);

12
src/ImageSharp/ImageFrame{TPixel}.cs

@ -66,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,
@ -99,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);
@ -146,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);
@ -371,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/>
@ -381,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.
@ -396,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);

3
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

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:

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

@ -87,15 +87,21 @@ internal sealed class IccReader
foreach (IccTagTableEntry tag in tagTable) foreach (IccTagTableEntry tag in tagTable)
{ {
IccTagDataEntry entry; IccTagDataEntry entry;
if (store.TryGetValue(tag.Offset, out IccTagDataEntry? value))
try
{ {
entry = reader.ReadTagDataEntry(tag); entry = value;
} }
catch else
{ {
// Ignore tags that could not be read try
continue; {
entry = reader.ReadTagDataEntry(tag);
}
catch
{
// Ignore tags that could not be read
continue;
}
} }
entry.TagSignature = tag.Signature; entry.TagSignature = tag.Signature;

17
src/ImageSharp/Primitives/Rational.cs

@ -70,7 +70,7 @@ public readonly struct Rational : IEquatable<Rational>
/// <param name="bestPrecision">Whether to use the best possible precision when parsing the value.</param> /// <param name="bestPrecision">Whether to use the best possible precision when parsing the value.</param>
public Rational(double value, bool bestPrecision) public Rational(double value, bool bestPrecision)
{ {
var rational = LongRational.FromDouble(Math.Abs(value), bestPrecision); LongRational rational = LongRational.FromDouble(Math.Abs(value), bestPrecision);
this.Numerator = (uint)rational.Numerator; this.Numerator = (uint)rational.Numerator;
this.Denominator = (uint)rational.Denominator; this.Denominator = (uint)rational.Denominator;
@ -109,7 +109,7 @@ public readonly struct Rational : IEquatable<Rational>
/// <returns> /// <returns>
/// The <see cref="Rational"/>. /// The <see cref="Rational"/>.
/// </returns> /// </returns>
public static Rational FromDouble(double value) => new Rational(value, false); public static Rational FromDouble(double value) => new(value, false);
/// <summary> /// <summary>
/// Converts the specified <see cref="double"/> to an instance of this type. /// Converts the specified <see cref="double"/> to an instance of this type.
@ -119,24 +119,19 @@ public readonly struct Rational : IEquatable<Rational>
/// <returns> /// <returns>
/// The <see cref="Rational"/>. /// The <see cref="Rational"/>.
/// </returns> /// </returns>
public static Rational FromDouble(double value, bool bestPrecision) => new Rational(value, bestPrecision); public static Rational FromDouble(double value, bool bestPrecision) => new(value, bestPrecision);
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object? obj) => obj is Rational other && this.Equals(other); public override bool Equals(object? obj) => obj is Rational other && this.Equals(other);
/// <inheritdoc/> /// <inheritdoc/>
public bool Equals(Rational other) public bool Equals(Rational other)
{ => this.Numerator == other.Numerator && this.Denominator == other.Denominator;
var left = new LongRational(this.Numerator, this.Denominator);
var right = new LongRational(other.Numerator, other.Denominator);
return left.Equals(right);
}
/// <inheritdoc/> /// <inheritdoc/>
public override int GetHashCode() public override int GetHashCode()
{ {
var self = new LongRational(this.Numerator, this.Denominator); LongRational self = new(this.Numerator, this.Denominator);
return self.GetHashCode(); return self.GetHashCode();
} }
@ -169,7 +164,7 @@ public readonly struct Rational : IEquatable<Rational>
/// <returns>The <see cref="string"/></returns> /// <returns>The <see cref="string"/></returns>
public string ToString(IFormatProvider provider) public string ToString(IFormatProvider provider)
{ {
var rational = new LongRational(this.Numerator, this.Denominator); LongRational rational = new(this.Numerator, this.Denominator);
return rational.ToString(provider); return rational.ToString(provider);
} }
} }

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.

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

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

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

@ -7,7 +7,6 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
@ -79,6 +78,18 @@ public partial class PngDecoderTests
{ TestImages.Png.Rgba64Bpp, typeof(Image<Rgba64>) }, { TestImages.Png.Rgba64Bpp, typeof(Image<Rgba64>) },
}; };
public static readonly string[] MultiFrameTestFiles =
{
TestImages.Png.APng,
TestImages.Png.SplitIDatZeroLength,
TestImages.Png.DisposeNone,
TestImages.Png.DisposeBackground,
TestImages.Png.DisposeBackgroundRegion,
TestImages.Png.DisposePreviousFirst,
TestImages.Png.DisposeBackgroundBeforeRegion,
TestImages.Png.BlendOverMultiple
};
[Theory] [Theory]
[MemberData(nameof(PixelFormatRange))] [MemberData(nameof(PixelFormatRange))]
public void Decode_NonGeneric_CreatesCorrectImageType(string path, Type type) public void Decode_NonGeneric_CreatesCorrectImageType(string path, Type type)
@ -107,6 +118,19 @@ public partial class PngDecoderTests
image.CompareToOriginal(provider, ImageComparer.Exact); image.CompareToOriginal(provider, ImageComparer.Exact);
} }
[Theory]
[WithFileCollection(nameof(MultiFrameTestFiles), PixelTypes.Rgba32)]
public void Decode_VerifyAllFrames<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
// Some images have many frames, only compare a selection of them.
static bool Predicate(int i, int _) => i <= 8 || i % 8 == 0;
image.DebugSaveMultiFrame(provider, predicate: Predicate);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact, predicate: Predicate);
}
[Theory] [Theory]
[WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)]
public void PngDecoder_Decode_Resize<TPixel>(TestImageProvider<TPixel> provider) public void PngDecoder_Decode_Resize<TPixel>(TestImageProvider<TPixel> provider)
@ -539,7 +563,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
@ -551,7 +576,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

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

@ -442,6 +442,40 @@ public partial class PngEncoderTests
}); });
} }
[Theory]
[WithFile(TestImages.Png.APng, PixelTypes.Rgba32)]
public void Encode_APng<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, PngEncoder);
memStream.Position = 0;
image.DebugSave(provider: provider, encoder: PngEncoder, null, false);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageComparer.Exact.VerifySimilarity(output, image);
Assert.Equal(5, image.Frames.Count);
Assert.Equal(image.Frames.Count, output.Frames.Count);
PngMetadata originalMetadata = image.Metadata.GetPngMetadata();
PngMetadata outputMetadata = output.Metadata.GetPngMetadata();
Assert.Equal(originalMetadata.RepeatCount, outputMetadata.RepeatCount);
for (int i = 0; i < image.Frames.Count; i++)
{
PngFrameMetadata originalFrameMetadata = image.Frames[i].Metadata.GetPngFrameMetadata();
PngFrameMetadata outputFrameMetadata = output.Frames[i].Metadata.GetPngFrameMetadata();
Assert.Equal(originalFrameMetadata.FrameDelay, outputFrameMetadata.FrameDelay);
Assert.Equal(originalFrameMetadata.BlendMethod, outputFrameMetadata.BlendMethod);
Assert.Equal(originalFrameMetadata.DisposalMethod, outputFrameMetadata.DisposalMethod);
}
}
[Theory] [Theory]
[MemberData(nameof(PngTrnsFiles))] [MemberData(nameof(PngTrnsFiles))]
public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType) public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType)
@ -449,44 +483,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]

35
tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png;
namespace SixLabors.ImageSharp.Tests.Formats.Png;
[Trait("Format", "Png")]
public class PngFrameMetadataTests
{
[Fact]
public void CloneIsDeep()
{
PngFrameMetadata meta = new()
{
FrameDelay = new(1, 0),
DisposalMethod = PngDisposalMethod.Background,
BlendMethod = PngBlendMethod.Over,
};
PngFrameMetadata clone = (PngFrameMetadata)meta.DeepClone();
Assert.True(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.True(meta.DisposalMethod.Equals(clone.DisposalMethod));
Assert.True(meta.BlendMethod.Equals(clone.BlendMethod));
clone.FrameDelay = new(2, 1);
clone.DisposalMethod = PngDisposalMethod.Previous;
clone.BlendMethod = PngBlendMethod.Source;
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod));
Assert.False(meta.BlendMethod.Equals(clone.BlendMethod));
}
}

14
tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs

@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Chunks;
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;
@ -30,15 +31,25 @@ public class PngMetadataTests
ColorType = PngColorType.GrayscaleWithAlpha, ColorType = PngColorType.GrayscaleWithAlpha,
InterlaceMethod = PngInterlaceMode.Adam7, InterlaceMethod = PngInterlaceMode.Adam7,
Gamma = 2, Gamma = 2,
TextData = new List<PngTextData> { new PngTextData("name", "value", "foo", "bar") } TextData = new List<PngTextData> { new PngTextData("name", "value", "foo", "bar") },
RepeatCount = 123
}; };
PngMetadata clone = (PngMetadata)meta.DeepClone(); PngMetadata clone = (PngMetadata)meta.DeepClone();
Assert.True(meta.BitDepth == clone.BitDepth);
Assert.True(meta.ColorType == clone.ColorType);
Assert.True(meta.InterlaceMethod == clone.InterlaceMethod);
Assert.True(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
Assert.True(meta.RepeatCount == clone.RepeatCount);
clone.BitDepth = PngBitDepth.Bit2; clone.BitDepth = PngBitDepth.Bit2;
clone.ColorType = PngColorType.Palette; clone.ColorType = PngColorType.Palette;
clone.InterlaceMethod = PngInterlaceMode.None; clone.InterlaceMethod = PngInterlaceMode.None;
clone.Gamma = 1; clone.Gamma = 1;
clone.RepeatCount = 321;
Assert.False(meta.BitDepth == clone.BitDepth); Assert.False(meta.BitDepth == clone.BitDepth);
Assert.False(meta.ColorType == clone.ColorType); Assert.False(meta.ColorType == clone.ColorType);
@ -46,6 +57,7 @@ public class PngMetadataTests
Assert.False(meta.Gamma.Equals(clone.Gamma)); Assert.False(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData)); Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData)); Assert.True(meta.TextData.SequenceEqual(clone.TextData));
Assert.False(meta.RepeatCount == clone.RepeatCount);
} }
[Theory] [Theory]

2
tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png.Chunks;
namespace SixLabors.ImageSharp.Tests.Formats.Png; namespace SixLabors.ImageSharp.Tests.Formats.Png;

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)

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;

8
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,8 +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);

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

24
tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs

@ -70,8 +70,7 @@ public class ExifValuesTests
{ ExifTag.JPEGDCTables }, { ExifTag.JPEGDCTables },
{ ExifTag.JPEGACTables }, { ExifTag.JPEGACTables },
{ ExifTag.StripRowCounts }, { ExifTag.StripRowCounts },
{ ExifTag.IntergraphRegisters }, { ExifTag.IntergraphRegisters }
{ ExifTag.TimeZoneOffset }
}; };
public static TheoryData<ExifTag> NumberTags => new TheoryData<ExifTag> public static TheoryData<ExifTag> NumberTags => new TheoryData<ExifTag>
@ -129,6 +128,7 @@ public class ExifValuesTests
{ ExifTag.GPSImgDirection }, { ExifTag.GPSImgDirection },
{ ExifTag.GPSDestBearing }, { ExifTag.GPSDestBearing },
{ ExifTag.GPSDestDistance }, { ExifTag.GPSDestDistance },
{ ExifTag.GPSHPositioningError },
}; };
public static TheoryData<ExifTag> RationalArrayTags => new TheoryData<ExifTag> public static TheoryData<ExifTag> RationalArrayTags => new TheoryData<ExifTag>
@ -235,6 +235,11 @@ public class ExifValuesTests
{ ExifTag.Decode } { ExifTag.Decode }
}; };
public static TheoryData<ExifTag> SignedShortArrayTags => new TheoryData<ExifTag>
{
{ ExifTag.TimeZoneOffset }
};
public static TheoryData<ExifTag> StringTags => new TheoryData<ExifTag> public static TheoryData<ExifTag> StringTags => new TheoryData<ExifTag>
{ {
{ ExifTag.ImageDescription }, { ExifTag.ImageDescription },
@ -559,6 +564,21 @@ public class ExifValuesTests
Assert.Equal(expected, typed.Value); Assert.Equal(expected, typed.Value);
} }
[Theory]
[MemberData(nameof(SignedShortArrayTags))]
public void ExifSignedShortArrayTests(ExifTag tag)
{
short[] expected = new short[] { 21, 42 };
ExifValue value = ExifValues.Create(tag);
Assert.False(value.TrySetValue(expected.ToString()));
Assert.True(value.TrySetValue(expected));
var typed = (ExifSignedShortArray)value;
Assert.Equal(expected, typed.Value);
}
[Theory] [Theory]
[MemberData(nameof(StringTags))] [MemberData(nameof(StringTags))]
public void ExifStringTests(ExifTag tag) public void ExifStringTests(ExifTag tag)

2
tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs

@ -22,7 +22,7 @@ public abstract class BaseImageOperationsExtensionTest : IDisposable
this.options = new GraphicsOptions { Antialias = false }; this.options = new GraphicsOptions { Antialias = false };
this.source = new Image<Rgba32>(91 + 324, 123 + 56); this.source = new Image<Rgba32>(91 + 324, 123 + 56);
this.rect = new Rectangle(91, 123, 324, 56); // make this random? this.rect = new Rectangle(91, 123, 324, 56); // make this random?
this.internalOperations = new FakeImageOperationsProvider.FakeImageOperations<Rgba32>(this.source.GetConfiguration(), this.source, false); this.internalOperations = new FakeImageOperationsProvider.FakeImageOperations<Rgba32>(this.source.Configuration, this.source, false);
this.internalOperations.SetGraphicsOptions(this.options); this.internalOperations.SetGraphicsOptions(this.options);
this.operations = this.internalOperations; this.operations = this.internalOperations;
} }

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

@ -65,7 +65,7 @@ public class BokehBlurTest
// Make sure the kernel components are the same // Make sure the kernel components are the same
using Image<Rgb24> image = new(1, 1); using Image<Rgb24> image = new(1, 1);
Configuration configuration = image.GetConfiguration(); Configuration configuration = image.Configuration;
BokehBlurProcessor definition = new(10, BokehBlurProcessor.DefaultComponents, BokehBlurProcessor.DefaultGamma); BokehBlurProcessor definition = new(10, BokehBlurProcessor.DefaultComponents, BokehBlurProcessor.DefaultGamma);
using BokehBlurProcessor<Rgb24> processor = (BokehBlurProcessor<Rgb24>)definition.CreatePixelSpecificProcessor(configuration, image, image.Bounds); using BokehBlurProcessor<Rgb24> processor = (BokehBlurProcessor<Rgb24>)definition.CreatePixelSpecificProcessor(configuration, image, image.Bounds);

15
tests/ImageSharp.Tests/TestImages.cs

@ -62,6 +62,17 @@ public static class TestImages
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";
public const string XmpColorPalette = "Png/xmp-colorpalette.png"; public const string XmpColorPalette = "Png/xmp-colorpalette.png";
// Animated
// https://philip.html5.org/tests/apng/tests.html
public const string APng = "Png/animated/apng.png";
public const string SplitIDatZeroLength = "Png/animated/4-split-idat-zero-length.png";
public const string DisposeNone = "Png/animated/7-dispose-none.png";
public const string DisposeBackground = "Png/animated/8-dispose-background.png";
public const string DisposeBackgroundBeforeRegion = "Png/animated/14-dispose-background-before-region.png";
public const string DisposeBackgroundRegion = "Png/animated/15-dispose-background-region.png";
public const string DisposePreviousFirst = "Png/animated/12-dispose-prev-first.png";
public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png"; public const string Filter0 = "Png/filter0.png";
public const string SubFilter3BytesPerPixel = "Png/filter1.png"; public const string SubFilter3BytesPerPixel = "Png/filter1.png";
@ -688,6 +699,7 @@ public static class TestImages
public const string WithXmp = "Webp/xmp_lossy.webp"; public const string WithXmp = "Webp/xmp_lossy.webp";
public const string BikeSmall = "Webp/bike_lossy_small.webp"; public const string BikeSmall = "Webp/bike_lossy_small.webp";
public const string Animated = "Webp/leo_animated_lossy.webp"; public const string Animated = "Webp/leo_animated_lossy.webp";
public const string AnimatedIssue2528 = "Webp/issues/Issue2528.webp";
// Lossy images without macroblock filtering. // Lossy images without macroblock filtering.
public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp"; public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp";
@ -990,6 +1002,8 @@ public static class TestImages
public const string Issues2149 = "Tiff/Issues/Group4CompressionWithStrips.tiff"; public const string Issues2149 = "Tiff/Issues/Group4CompressionWithStrips.tiff";
public const string Issues2255 = "Tiff/Issues/Issue2255.png"; public const string Issues2255 = "Tiff/Issues/Issue2255.png";
public const string Issues2435 = "Tiff/Issues/Issue2435.tiff"; public const string Issues2435 = "Tiff/Issues/Issue2435.tiff";
public const string JpegCompressedGray0000539558 = "Tiff/Issues/JpegCompressedGray-0000539558.tiff";
public const string Tiled0000023664 = "Tiff/Issues/tiled-0000023664.tiff";
public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff"; public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff";
public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff"; public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff";
@ -1051,6 +1065,7 @@ public static class TestImages
public const string GrayscalePlainNormalized = "Pbm/grayscale_plain_normalized.pgm"; public const string GrayscalePlainNormalized = "Pbm/grayscale_plain_normalized.pgm";
public const string GrayscalePlainMagick = "Pbm/grayscale_plain_magick.pgm"; public const string GrayscalePlainMagick = "Pbm/grayscale_plain_magick.pgm";
public const string RgbBinary = "Pbm/00000_00000.ppm"; public const string RgbBinary = "Pbm/00000_00000.ppm";
public const string RgbBinaryPrematureEof = "Pbm/00000_00000_premature_eof.ppm";
public const string RgbPlain = "Pbm/rgb_plain.ppm"; public const string RgbPlain = "Pbm/rgb_plain.ppm";
public const string RgbPlainNormalized = "Pbm/rgb_plain_normalized.ppm"; public const string RgbPlainNormalized = "Pbm/rgb_plain_normalized.ppm";
public const string RgbPlainMagick = "Pbm/rgb_plain_magick.ppm"; public const string RgbPlainMagick = "Pbm/rgb_plain_magick.ppm";

36
tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs

@ -0,0 +1,36 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Tests.TestUtilities;
internal class EofHitCounter : IDisposable
{
private readonly BufferedReadStream stream;
public EofHitCounter(BufferedReadStream stream, Image image)
{
this.stream = stream;
this.Image = image;
}
public int EofHitCount => this.stream.EofHitCount;
public Image Image { get; private set; }
public static EofHitCounter RunDecoder(string testImage) => RunDecoder(TestFile.Create(testImage).Bytes);
public static EofHitCounter RunDecoder(byte[] imageData)
{
BufferedReadStream stream = new(Configuration.Default, new MemoryStream(imageData));
Image image = Image.Load(stream);
return new EofHitCounter(stream, image);
}
public void Dispose()
{
this.stream.Dispose();
this.Image.Dispose();
}
}

2
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs

@ -28,7 +28,7 @@ public class ExactImageComparer : ImageComparer
var bBuffer = new Rgba64[width]; var bBuffer = new Rgba64[width];
var differences = new List<PixelDifference>(); var differences = new List<PixelDifference>();
Configuration configuration = expected.GetConfiguration(); Configuration configuration = expected.Configuration;
Buffer2D<TPixelA> expectedBuffer = expected.PixelBuffer; Buffer2D<TPixelA> expectedBuffer = expected.PixelBuffer;
Buffer2D<TPixelB> actualBuffer = actual.PixelBuffer; Buffer2D<TPixelB> actualBuffer = actual.PixelBuffer;

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

Loading…
Cancel
Save