diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index c3a9c212ee..a451e111d2 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -27,11 +27,11 @@ public static class AdvancedImageExtensions Guard.NotNull(filePath, nameof(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(); 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); } @@ -39,13 +39,13 @@ public static class AdvancedImageExtensions throw new UnknownImageFormatException(sb.ToString()); } - IImageEncoder? encoder = source.GetConfiguration().ImageFormatsManager.GetEncoder(format); + IImageEncoder? encoder = source.Configuration.ImageFormatsManager.GetEncoder(format); if (encoder is null) { StringBuilder sb = new(); sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:"); - foreach (KeyValuePair enc in source.GetConfiguration().ImageFormatsManager.ImageEncoders) + foreach (KeyValuePair enc in source.Configuration.ImageFormatsManager.ImageEncoders) { 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) => source.AcceptAsync(visitor, cancellationToken); - /// - /// Gets the configuration for the image. - /// - /// The source image. - /// Returns the configuration. - public static Configuration GetConfiguration(this Image source) - => GetConfiguration((IConfigurationProvider)source); - - /// - /// Gets the configuration for the image frame. - /// - /// The source image. - /// Returns the configuration. - public static Configuration GetConfiguration(this ImageFrame source) - => GetConfiguration((IConfigurationProvider)source); - - /// - /// Gets the configuration. - /// - /// The source image - /// Returns the bounds of the image - private static Configuration GetConfiguration(IConfigurationProvider source) - => source?.Configuration ?? Configuration.Default; - /// /// Gets the representation of the pixels as a containing the backing pixel data of the image /// stored in row major order, as a list of contiguous blocks in the source image's pixel format. @@ -167,12 +143,4 @@ public static class AdvancedImageExtensions return source.Frames.RootFrame.PixelBuffer.GetSafeRowMemory(rowIndex); } - - /// - /// Gets the assigned to 'source'. - /// - /// The source image. - /// Returns the configuration. - internal static MemoryAllocator GetMemoryAllocator(this IConfigurationProvider source) - => GetConfiguration(source).MemoryAllocator; } diff --git a/src/ImageSharp/Advanced/IConfigurationProvider.cs b/src/ImageSharp/Advanced/IConfigurationProvider.cs index 086461f448..bb6d124f68 100644 --- a/src/ImageSharp/Advanced/IConfigurationProvider.cs +++ b/src/ImageSharp/Advanced/IConfigurationProvider.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Advanced; /// /// Defines the contract for objects that can provide access to configuration. /// -internal interface IConfigurationProvider +public interface IConfigurationProvider { /// /// Gets the configuration which allows altering default behaviour or extending the library. diff --git a/src/ImageSharp/Color/Color.cs b/src/ImageSharp/Color/Color.cs index 13af25f6c7..cebceabe09 100644 --- a/src/ImageSharp/Color/Color.cs +++ b/src/ImageSharp/Color/Color.cs @@ -251,7 +251,17 @@ public readonly partial struct Color : IEquatable /// /// A hexadecimal string representation of the value. [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(); + } /// public override string ToString() => this.ToHex(); diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs index 06a7c3928c..1d743bf3a5 100644 --- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs +++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs @@ -123,12 +123,12 @@ internal sealed class ZlibInflateStream : Stream /// 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. this.currentDataRemaining = this.getData(); - if (this.currentDataRemaining == 0) + if (this.currentDataRemaining is 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. int bytesRead = 0; offset += totalBytesRead; - while (this.currentDataRemaining == 0 && totalBytesRead < count) + while (this.currentDataRemaining is 0 && totalBytesRead < count) { this.currentDataRemaining = this.getData(); - if (this.currentDataRemaining == 0) + if (this.currentDataRemaining is 0) { return totalBytesRead; } @@ -161,6 +161,11 @@ internal sealed class ZlibInflateStream : Stream bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining); this.currentDataRemaining -= bytesToRead; bytesRead = this.innerStream.Read(buffer, offset, bytesToRead); + if (bytesRead == 0) + { + return totalBytesRead; + } + totalBytesRead += bytesRead; } @@ -168,22 +173,13 @@ internal sealed class ZlibInflateStream : Stream } /// - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// - public override void SetLength(long value) - { - throw new NotSupportedException(); - } + public override void SetLength(long value) => throw new NotSupportedException(); /// - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); /// 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. 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 { - if (isCriticalChunk) - { - throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); - } - else - { - return false; - } + return false; } // 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. // 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; } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index aecea7dedf..0081f6a1ae 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -32,7 +32,7 @@ public sealed class BmpEncoder : QuantizingImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - BmpEncoderCore encoder = new(this, image.GetMemoryAllocator()); + BmpEncoderCore encoder = new(this, image.Configuration.MemoryAllocator); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 9d5b8d0cfd..076d1adf00 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -119,7 +119,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; ImageMetadata metadata = image.Metadata; BmpMetadata bmpMetadata = metadata.GetBmpMetadata(); this.bitsPerPixel ??= bmpMetadata.BitsPerPixel; diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 386b1bd1c3..150ee9ccf0 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -18,7 +18,7 @@ public sealed class GifEncoder : QuantizingImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - GifEncoderCore encoder = new(image.GetConfiguration(), this); + GifEncoderCore encoder = new(image.Configuration, this); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index ccf8feaccd..926cc091c7 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/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 is more expensive memory-wise than de-duplicating indexed buffer but allows us to deduplicate // frames using both local and global palettes. - using ImageFrame encodingFrame = new(previousFrame.GetConfiguration(), previousFrame.Size()); + using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size()); for (int i = 1; i < image.Frames.Count; i++) { diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs index e2c61c8eb3..a1abd7dc30 100644 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs @@ -50,7 +50,8 @@ internal static class ImageDecoderUtilities CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - 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 { @@ -64,6 +65,13 @@ internal static class ImageDecoderUtilities { throw; } + finally + { + if (bufferedReadStream != stream) + { + bufferedReadStream.Dispose(); + } + } } private static InvalidImageContentException DefaultLargeImageExceptionFactory( diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index d6870f716b..4acd29e81c 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -42,7 +42,7 @@ public abstract class ImageEncoder : IImageEncoder private void EncodeWithSeekableStream(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; if (stream.CanSeek) { this.Encode(image, stream, cancellationToken); @@ -59,7 +59,7 @@ public abstract class ImageEncoder : IImageEncoder private async Task EncodeWithSeekableStreamAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; if (stream.CanSeek) { await DoEncodeAsync(stream).ConfigureAwait(false); diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs index 30f576e5c4..7e5989d6fc 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.cs +++ b/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) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance), cancellationToken); /// @@ -106,7 +106,7 @@ public static partial class ImageExtensions public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance), cancellationToken); /// @@ -161,7 +161,7 @@ public static partial class ImageExtensions public static void SaveAsGif(this Image source, string path, GifEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance), cancellationToken); /// @@ -208,7 +208,7 @@ public static partial class ImageExtensions public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance), cancellationToken); /// @@ -263,7 +263,7 @@ public static partial class ImageExtensions public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance), cancellationToken); /// @@ -310,7 +310,7 @@ public static partial class ImageExtensions public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance), cancellationToken); /// @@ -365,7 +365,7 @@ public static partial class ImageExtensions public static void SaveAsPbm(this Image source, string path, PbmEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance), cancellationToken); /// @@ -412,7 +412,7 @@ public static partial class ImageExtensions public static void SaveAsPbm(this Image source, Stream stream, PbmEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance), cancellationToken); /// @@ -467,7 +467,7 @@ public static partial class ImageExtensions public static void SaveAsPng(this Image source, string path, PngEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance), cancellationToken); /// @@ -514,7 +514,7 @@ public static partial class ImageExtensions public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance), cancellationToken); /// @@ -569,7 +569,7 @@ public static partial class ImageExtensions public static void SaveAsQoi(this Image source, string path, QoiEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance), cancellationToken); /// @@ -616,7 +616,7 @@ public static partial class ImageExtensions public static void SaveAsQoi(this Image source, Stream stream, QoiEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance), cancellationToken); /// @@ -671,7 +671,7 @@ public static partial class ImageExtensions public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance), cancellationToken); /// @@ -718,7 +718,7 @@ public static partial class ImageExtensions public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance), cancellationToken); /// @@ -773,7 +773,7 @@ public static partial class ImageExtensions public static void SaveAsTiff(this Image source, string path, TiffEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance), cancellationToken); /// @@ -820,7 +820,7 @@ public static partial class ImageExtensions public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance), cancellationToken); /// @@ -875,7 +875,7 @@ public static partial class ImageExtensions public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance), cancellationToken); /// @@ -922,7 +922,7 @@ public static partial class ImageExtensions public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance), cancellationToken); } diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.tt b/src/ImageSharp/Formats/ImageExtensions.Save.tt index 538f62d041..d4f1ed233b 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.tt +++ b/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) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); /// /// 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) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), cancellationToken); /// @@ -125,7 +125,7 @@ public static partial class ImageExtensions public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); /// /// 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) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), cancellationToken); <# diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs index 97a4a2dc0e..6ba0b82723 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs @@ -20,7 +20,7 @@ internal sealed class JpegFrame : IDisposable this.PixelWidth = image.Width; this.PixelHeight = image.Height; - MemoryAllocator allocator = image.GetConfiguration().MemoryAllocator; + MemoryAllocator allocator = image.Configuration.MemoryAllocator; JpegComponentConfig[] componentConfigs = frameConfig.Components; this.Components = new Component[componentConfigs.Length]; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs index 47a6029065..fc93db9bb0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs @@ -32,7 +32,7 @@ internal class SpectralConverter : SpectralConverter, IDisposable public SpectralConverter(JpegFrame frame, Image image, Block8x8F[] dequantTables) { - MemoryAllocator allocator = image.GetConfiguration().MemoryAllocator; + MemoryAllocator allocator = image.Configuration.MemoryAllocator; // iteration data int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width); diff --git a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs index f629282340..ce7e379fc5 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs @@ -71,7 +71,11 @@ internal class BinaryDecoder for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL8Bytes( configuration, @@ -93,7 +97,11 @@ internal class BinaryDecoder for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL16Bytes( configuration, @@ -115,7 +123,11 @@ internal class BinaryDecoder for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgb24Bytes( configuration, @@ -137,7 +149,11 @@ internal class BinaryDecoder for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgb48Bytes( configuration, @@ -161,6 +177,11 @@ internal class BinaryDecoder for (int x = 0; x < width;) { int raw = stream.ReadByte(); + if (raw < 0) + { + return; + } + int stopBit = Math.Min(8, width - x); for (int bit = 0; bit < stopBit; bit++) { diff --git a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs index 5d5537e398..3b0e41a02d 100644 --- a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs +++ b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs @@ -11,14 +11,20 @@ namespace SixLabors.ImageSharp.Formats.Pbm; internal static class BufferedReadStreamExtensions { /// - /// Skip over any whitespace or any comments. + /// Skip over any whitespace or any comments and signal if EOF has been reached. /// - public static void SkipWhitespaceAndComments(this BufferedReadStream stream) + /// The buffered read stream. + /// if EOF has been reached while reading the stream; see langword="true"/> otherwise. + public static bool SkipWhitespaceAndComments(this BufferedReadStream stream) { bool isWhitespace; do { int val = stream.ReadByte(); + if (val < 0) + { + return false; + } // Comments start with '#' and end at the next new-line. if (val == 0x23) @@ -27,8 +33,12 @@ internal static class BufferedReadStreamExtensions do { 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. val = innerValue; @@ -38,18 +48,31 @@ internal static class BufferedReadStreamExtensions } while (isWhitespace); stream.Seek(-1, SeekOrigin.Current); + return true; } /// - /// Read a decimal text value. + /// Read a decimal text value and signal if EOF has been reached. /// - /// The integer value of the decimal. - public static int ReadDecimal(this BufferedReadStream stream) + /// The buffered read stream. + /// The read value. + /// if EOF has been reached while reading the stream; otherwise. + /// + /// 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. + /// + public static bool ReadDecimal(this BufferedReadStream stream, out int value) { - int value = 0; + value = 0; while (true) { - int current = stream.ReadByte() - 0x30; + int current = stream.ReadByte(); + if (current < 0) + { + return false; + } + + current -= 0x30; if ((uint)current > 9) { break; @@ -58,6 +81,6 @@ internal static class BufferedReadStreamExtensions value = (value * 10) + current; } - return value; + return true; } } diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs index e1bc5be6e8..3fe339865b 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -95,6 +96,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals /// Processes the ppm header. /// /// The input stream. + /// An EOF marker has been read before the image has been decoded. private void ProcessHeader(BufferedReadStream stream) { Span buffer = stackalloc byte[2]; @@ -144,14 +146,22 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals throw new InvalidImageContentException("Unknown of not implemented image type encountered."); } - stream.SkipWhitespaceAndComments(); - int width = stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - int height = stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); + if (!stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int width) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int height) || + !stream.SkipWhitespaceAndComments()) + { + ThrowPrematureEof(); + } + if (this.colorType != PbmColorType.BlackAndWhite) { - this.maxPixelValue = stream.ReadDecimal(); + if (!stream.ReadDecimal(out this.maxPixelValue)) + { + ThrowPrematureEof(); + } + if (this.maxPixelValue > 255) { this.componentType = PbmComponentType.Short; @@ -174,6 +184,9 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals meta.Encoding = this.encoding; meta.ColorType = this.colorType; meta.ComponentType = this.componentType; + + [DoesNotReturn] + static void ThrowPrematureEof() => throw new InvalidImageContentException("Reached EOF while reading the header."); } private void ProcessPixels(BufferedReadStream stream, Buffer2D pixels) diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoder.cs b/src/ImageSharp/Formats/Pbm/PbmEncoder.cs index 0f492fae72..8258c91655 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoder.cs @@ -49,7 +49,7 @@ public sealed class PbmEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - PbmEncoderCore encoder = new(image.GetConfiguration(), this); + PbmEncoderCore encoder = new(image.Configuration, this); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs index 4a07173a14..b6e31a3c28 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs @@ -78,7 +78,7 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals private void SanitizeAndSetEncoderOptions(Image image) where TPixel : unmanaged, IPixel { - this.configuration = image.GetConfiguration(); + this.configuration = image.Configuration; PbmMetadata metadata = image.Metadata.GetPbmMetadata(); this.encoding = this.encoder.Encoding ?? metadata.Encoding; this.colorType = this.encoder.ColorType ?? metadata.ColorType; diff --git a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs index f6d803684c..8748d90fa8 100644 --- a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs @@ -65,13 +65,18 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - byte value = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new L8(value); + stream.ReadDecimal(out int value); + rowSpan[x] = new L8((byte)value); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -79,6 +84,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -91,13 +101,18 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - ushort value = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new L16(value); + stream.ReadDecimal(out int value); + rowSpan[x] = new L16((ushort)value); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -105,6 +120,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -117,17 +137,29 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - byte red = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - byte green = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - byte blue = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new Rgb24(red, green, blue); + if (!stream.ReadDecimal(out int red) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int green) || + !stream.SkipWhitespaceAndComments()) + { + // Reached EOF before reading a full RGB value + 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 pixelSpan = pixels.DangerousGetRowSpan(y); @@ -135,6 +167,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -147,17 +184,29 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - ushort red = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - ushort green = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - ushort blue = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new Rgb48(red, green, blue); + if (!stream.ReadDecimal(out int red) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int green) || + !stream.SkipWhitespaceAndComments()) + { + // Reached EOF before reading a full RGB value + 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 pixelSpan = pixels.DangerousGetRowSpan(y); @@ -165,6 +214,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -177,13 +231,19 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - int value = stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); + stream.ReadDecimal(out int value); + rowSpan[x] = value == 0 ? White : Black; + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -191,6 +251,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } } diff --git a/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs new file mode 100644 index 0000000000..a9f99a9e4a --- /dev/null +++ b/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; + } + + /// + /// Gets the number of frames + /// + public int NumberFrames { get; } + + /// + /// Gets the number of times to loop this APNG. 0 indicates infinite looping. + /// + public int NumberPlays { get; } + + /// + /// Writes the acTL to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span buffer) + { + BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames); + BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays); + } + + /// + /// Parses the APngAnimationControl from the given data buffer. + /// + /// The data to parse. + /// The parsed acTL. + public static AnimationControl Parse(ReadOnlySpan data) + => new( + numberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]), + numberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8])); +} diff --git a/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs b/src/ImageSharp/Formats/Png/Chunks/FrameControl.cs new file mode 100644 index 0000000000..fb2ca473c2 --- /dev/null +++ b/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; + } + + /// + /// Gets the sequence number of the animation chunk, starting from 0 + /// + public uint SequenceNumber { get; } + + /// + /// Gets the width of the following frame + /// + public uint Width { get; } + + /// + /// Gets the height of the following frame + /// + public uint Height { get; } + + /// + /// Gets the X position at which to render the following frame + /// + public uint XOffset { get; } + + /// + /// Gets the Y position at which to render the following frame + /// + public uint YOffset { get; } + + /// + /// Gets the X limit at which to render the following frame + /// + public uint XMax => this.XOffset + this.Width; + + /// + /// Gets the Y limit at which to render the following frame + /// + public uint YMax => this.YOffset + this.Height; + + /// + /// Gets the frame delay fraction numerator + /// + public ushort DelayNumerator { get; } + + /// + /// Gets the frame delay fraction denominator + /// + public ushort DelayDenominator { get; } + + /// + /// Gets the type of frame area disposal to be done after rendering this frame + /// + public PngDisposalMethod DisposeOperation { get; } + + /// + /// Gets the type of frame area rendering for this frame + /// + public PngBlendMethod BlendOperation { get; } + + public Rectangle Bounds => new((int)this.XOffset, (int)this.YOffset, (int)this.Width, (int)this.Height); + + /// + /// Validates the APng fcTL. + /// + /// The header. + /// + /// Thrown if the image does pass validation. + /// + 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)}"); + } + } + + /// + /// Writes the fcTL to the given buffer. + /// + /// The buffer to write to. + public void WriteTo(Span 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; + } + + /// + /// Parses the APngFrameControl from the given data buffer. + /// + /// The data to parse. + /// The parsed fcTL. + public static FrameControl Parse(ReadOnlySpan 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]); +} diff --git a/src/ImageSharp/Formats/Png/PngHeader.cs b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs similarity index 98% rename from src/ImageSharp/Formats/Png/PngHeader.cs rename to src/ImageSharp/Formats/Png/Chunks/PngHeader.cs index 06fec86f30..77fb706f60 100644 --- a/src/ImageSharp/Formats/Png/PngHeader.cs +++ b/src/ImageSharp/Formats/Png/Chunks/PngHeader.cs @@ -4,7 +4,7 @@ using System.Buffers.Binary; -namespace SixLabors.ImageSharp.Formats.Png; +namespace SixLabors.ImageSharp.Formats.Png.Chunks; /// /// Represents the png header chunk. diff --git a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs similarity index 89% rename from src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs rename to src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs index 34d53f00eb..7847882484 100644 --- a/src/ImageSharp/Formats/Png/Chunks/PhysicalChunkData.cs +++ b/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. using System.Buffers.Binary; @@ -10,11 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks; /// /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. /// -internal readonly struct PhysicalChunkData +internal readonly struct PngPhysical { 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.YAxisPixelsPerUnit = y; @@ -44,13 +44,13 @@ internal readonly struct PhysicalChunkData /// /// The data buffer. /// The parsed PhysicalChunkData. - public static PhysicalChunkData Parse(ReadOnlySpan data) + public static PngPhysical Parse(ReadOnlySpan data) { uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]); uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4)); byte unit = data[8]; - return new PhysicalChunkData(hResolution, vResolution, unit); + return new PngPhysical(hResolution, vResolution, unit); } /// @@ -59,7 +59,7 @@ internal readonly struct PhysicalChunkData /// /// The metadata. /// The constructed PngPhysicalChunkData instance. - public static PhysicalChunkData FromMetadata(ImageMetadata meta) + public static PngPhysical FromMetadata(ImageMetadata meta) { byte unitSpecifier = 0; uint x; @@ -92,7 +92,7 @@ internal readonly struct PhysicalChunkData break; } - return new PhysicalChunkData(x, y, unitSpecifier); + return new PngPhysical(x, y, unitSpecifier); } /// diff --git a/src/ImageSharp/Formats/Png/PngTextData.cs b/src/ImageSharp/Formats/Png/Chunks/PngTextData.cs similarity index 99% rename from src/ImageSharp/Formats/Png/PngTextData.cs rename to src/ImageSharp/Formats/Png/Chunks/PngTextData.cs index 8ef4f1821d..077eb46082 100644 --- a/src/ImageSharp/Formats/Png/PngTextData.cs +++ b/src/ImageSharp/Formats/Png/Chunks/PngTextData.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Png; +namespace SixLabors.ImageSharp.Formats.Png.Chunks; /// /// Stores text data contained in the iTXt, tEXt, and zTXt chunks. diff --git a/src/ImageSharp/Formats/Png/MetadataExtensions.cs b/src/ImageSharp/Formats/Png/MetadataExtensions.cs index e05bd5f844..f24b8d1b5c 100644 --- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Png/MetadataExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Metadata; @@ -14,7 +15,22 @@ public static partial class MetadataExtensions /// /// Gets the png format specific metadata for the image. /// - /// The metadata this method extends. + /// The metadata this method extends. /// The . - public static PngMetadata GetPngMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(PngFormat.Instance); + public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Gets the aPng format specific metadata for the image frame. + /// + /// The metadata this method extends. + /// The . + public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + + /// + /// Gets the aPng format specific metadata for the image frame. + /// + /// The metadata this method extends. + /// The metadata. + /// The . + public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata); } diff --git a/src/ImageSharp/Formats/Png/PngBlendMethod.cs b/src/ImageSharp/Formats/Png/PngBlendMethod.cs new file mode 100644 index 0000000000..f71dce8325 --- /dev/null +++ b/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; + +/// +/// 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. +/// +public enum PngBlendMethod +{ + /// + /// All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. + /// + Source, + + /// + /// 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]. + /// + Over +} diff --git a/src/ImageSharp/Formats/Png/PngChunk.cs b/src/ImageSharp/Formats/Png/PngChunk.cs index b514011eb3..e5fa5fbb72 100644 --- a/src/ImageSharp/Formats/Png/PngChunk.cs +++ b/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 /// public bool IsCritical => - this.Type == PngChunkType.Header || - this.Type == PngChunkType.Palette || - this.Type == PngChunkType.Data; + this.Type is PngChunkType.Header or + PngChunkType.Palette or + PngChunkType.Data or + PngChunkType.FrameData; } diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index f47c2e7f86..a008bf8ea2 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/src/ImageSharp/Formats/Png/PngChunkType.cs @@ -9,15 +9,17 @@ namespace SixLabors.ImageSharp.Formats.Png; internal enum PngChunkType : uint { /// - /// 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. /// + /// IDAT (Multiple) Data = 0x49444154U, /// /// This chunk must appear last. It marks the end of the PNG data stream. /// The chunk's data field is empty. /// + /// IEND (Single) End = 0x49454E44U, /// @@ -25,34 +27,40 @@ internal enum PngChunkType : uint /// common information like the width and the height of the image or /// the used compression method. /// + /// IHDR (Single) Header = 0x49484452U, /// /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte /// series in the RGB format. /// + /// PLTE (Single) Palette = 0x504C5445U, /// /// The eXIf data chunk which contains the Exif profile. /// + /// eXIF (Single) Exif = 0x65584966U, /// /// This chunk specifies the relationship between the image samples and the desired /// display output intensity. /// + /// gAMA (Single) Gamma = 0x67414D41U, /// - /// 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. /// + /// pHYs (Single) Physical = 0x70485973U, /// /// 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 (Multiple) Text = 0x74455874U, /// @@ -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 /// a compressed text string. /// + /// zTXt (Multiple) CompressedText = 0x7A545874U, /// - /// 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. /// + /// iTXt (Multiple) InternationalText = 0x69545874U, /// - /// 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) /// or a single transparent color (for grayscale and true color images). /// + /// tRNS (Single) Transparency = 0x74524E53U, /// - /// 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). /// + /// tIME (Single) Time = 0x74494d45, /// - /// 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), /// the bKGD chunk should be ignored. /// + /// bKGD (Single) Background = 0x624b4744, /// - /// 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. /// + /// iCCP (Single) EmbeddedColorProfile = 0x69434350, /// - /// 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. /// + /// sBIT (Single) SignificantBits = 0x73424954, /// - /// 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. /// + /// sRGB (Single) StandardRgbColourSpace = 0x73524742, /// - /// 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. /// + /// hIST (Single) Histogram = 0x68495354, /// - /// The sPLT chunk contains the suggested palette. + /// This chunk contains the suggested palette. /// + /// sPLT (Single) SuggestedPalette = 0x73504c54, /// - /// 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. /// + /// cHRM (Single) Chroma = 0x6348524d, + /// + /// 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. + /// + /// acTL (Single, APNG) + AnimationControl = 0x6163544cU, + + /// + /// 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. + /// + /// fcTL (Multiple, APNG) + FrameControl = 0x6663544cU, + + /// + /// 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. + /// + /// fdAT (Multiple, APNG) + FrameData = 0x66644154U, + /// /// 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 /// + /// CgBI ProprietaryApple = 0x43674249 } diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index b76c73b9f2..43f2b0fb25 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/src/ImageSharp/Formats/Png/PngConstants.cs @@ -28,12 +28,12 @@ internal static class PngConstants /// /// The list of mimetypes that equate to a Png. /// - public static readonly IEnumerable MimeTypes = new[] { "image/png" }; + public static readonly IEnumerable MimeTypes = new[] { "image/png", "image/apng" }; /// /// The list of file extensions that equate to a Png. /// - public static readonly IEnumerable FileExtensions = new[] { "png" }; + public static readonly IEnumerable FileExtensions = new[] { "png", "apng" }; /// /// The header bytes as a big-endian coded ulong. @@ -43,7 +43,7 @@ internal static class PngConstants /// /// The dictionary of available color types. /// - public static readonly Dictionary ColorTypes = new Dictionary + public static readonly Dictionary ColorTypes = new() { [PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 }, [PngColorType.Rgb] = new byte[] { 8, 16 }, @@ -80,7 +80,7 @@ internal static class PngConstants /// /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. /// - public static ReadOnlySpan XmpKeyword => new byte[] + public static ReadOnlySpan XmpKeyword => new[] { (byte)'X', (byte)'M', diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index f273ac2b98..d226451389 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -61,24 +61,24 @@ public sealed class PngDecoder : ImageDecoder case PngColorType.Grayscale: if (bits == PngBitDepth.Bit16) { - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); } - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); case PngColorType.Rgb: if (bits == PngBitDepth.Bit16) { - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); } - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index d1d29dca6b..d8305a3f57 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#nullable disable using System.Buffers; using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO.Compression; using System.Runtime.CompilerServices; @@ -34,12 +34,17 @@ internal sealed class PngDecoderCore : IImageDecoderInternals private readonly Configuration configuration; /// - /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. + /// Whether the metadata should be ignored when the image is being decoded. + /// + private readonly uint maxFrames; + + /// + /// Whether the metadata should be ignored when the image is being decoded. /// private readonly bool skipMetadata; /// - /// Gets or sets a value indicating whether to read the IHDR and tRNS chunks only. + /// Whether to read the IHDR and tRNS chunks only. /// private readonly bool colorMetadataOnly; @@ -51,13 +56,18 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The stream to decode from. /// - private BufferedReadStream currentStream; + private BufferedReadStream currentStream = null!; /// /// The png header. /// private PngHeader header; + /// + /// The png animation control. + /// + private AnimationControl animationControl; + /// /// The number of bytes per pixel. /// @@ -76,32 +86,22 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The palette containing color information for indexed png's. /// - private byte[] palette; + private byte[] palette = null!; /// /// The palette containing alpha channel color information for indexed png's. /// - private byte[] paletteAlpha; + private byte[] paletteAlpha = null!; /// /// Previous scanline processed. /// - private IMemoryOwner previousScanline; + private IMemoryOwner previousScanline = null!; /// /// The current scanline that is being processed. /// - private IMemoryOwner scanline; - - /// - /// The index of the current scanline being processed. - /// - private int currentRow = Adam7.FirstRow[0]; - - /// - /// The current number of bytes read in the current scanline. - /// - private int currentRowBytesRead; + private IMemoryOwner scanline = null!; /// /// Gets or sets the png color type. @@ -121,6 +121,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { this.Options = options; this.configuration = options.Configuration; + this.maxFrames = options.MaxFrames; this.skipMetadata = options.SkipMetadata; this.memoryAllocator = this.configuration.MemoryAllocator; } @@ -129,6 +130,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { this.Options = options; this.colorMetadataOnly = colorMetadataOnly; + this.maxFrames = options.MaxFrames; this.skipMetadata = true; this.configuration = options.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; @@ -144,11 +146,16 @@ internal sealed class PngDecoderCore : IImageDecoderInternals public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + uint frameCount = 0; ImageMetadata metadata = new(); PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; this.currentStream.Skip(8); - Image image = null; + Image? image = null; + FrameControl? previousFrameControl = null; + FrameControl? currentFrameControl = null; + ImageFrame? previousFrame = null; + ImageFrame? currentFrame = null; Span buffer = stackalloc byte[20]; try @@ -160,33 +167,91 @@ internal sealed class PngDecoderCore : IImageDecoderInternals switch (chunk.Type) { case PngChunkType.Header: + if (!Equals(this.header, default(PngHeader))) + { + PngThrowHelper.ThrowInvalidHeader(); + } + this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; + case PngChunkType.AnimationControl: + this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); + break; case PngChunkType.Physical: ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.Gamma: ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; + case PngChunkType.FrameControl: + frameCount++; + if (frameCount == this.maxFrames) + { + break; + } + + currentFrame = null; + currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + break; + case PngChunkType.FrameData: + if (frameCount == this.maxFrames) + { + break; + } + + if (image is null) + { + PngThrowHelper.ThrowMissingDefaultData(); + } + + if (currentFrameControl is null) + { + PngThrowHelper.ThrowMissingFrameControl(); + } + + previousFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height); + this.InitializeFrame(previousFrameControl.Value, currentFrameControl.Value, image, previousFrame, out currentFrame); + + this.currentStream.Position += 4; + this.ReadScanlines( + chunk.Length - 4, + currentFrame, + pngMetadata, + this.ReadNextDataChunkAndSkipSeq, + currentFrameControl.Value, + cancellationToken); + + previousFrame = currentFrame; + previousFrameControl = currentFrameControl; + break; case PngChunkType.Data: + + currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height); if (image is null) { - this.InitializeImage(metadata, out image); + this.InitializeImage(metadata, currentFrameControl.Value, out image); + + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } - this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken); + this.ReadScanlines( + chunk.Length, + image.Frames.RootFrame, + pngMetadata, + this.ReadNextDataChunk, + currentFrameControl.Value, + cancellationToken); + previousFrame = currentFrame; + previousFrameControl = currentFrameControl; break; case PngChunkType.Palette: - byte[] pal = new byte[chunk.Length]; - chunk.Data.GetSpan().CopyTo(pal); - this.palette = pal; + this.palette = chunk.Data.GetSpan().ToArray(); break; case PngChunkType.Transparency: - byte[] alpha = new byte[chunk.Length]; - chunk.Data.GetSpan().CopyTo(alpha); - this.paletteAlpha = alpha; - this.AssignTransparentMarkers(alpha, pngMetadata); + this.paletteAlpha = chunk.Data.GetSpan().ToArray(); + this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata); break; case PngChunkType.Text: this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); @@ -246,9 +311,11 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { + uint frameCount = 0; ImageMetadata metadata = new(); PngMetadata pngMetadata = metadata.GetPngMetadata(); this.currentStream = stream; + FrameControl? lastFrameControl = null; Span buffer = stackalloc byte[20]; this.currentStream.Skip(8); @@ -264,6 +331,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngChunkType.Header: this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; + case PngChunkType.AnimationControl: + this.ReadAnimationControlChunk(pngMetadata, chunk.Data.GetSpan()); + break; case PngChunkType.Physical: if (this.colorMetadataOnly) { @@ -282,8 +352,36 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ReadGammaChunk(pngMetadata, chunk.Data.GetSpan()); break; - case PngChunkType.Data: + case PngChunkType.FrameControl: + ++frameCount; + if (frameCount == this.maxFrames) + { + break; + } + + lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); + break; + case PngChunkType.FrameData: + if (frameCount == this.maxFrames) + { + break; + } + if (this.colorMetadataOnly) + { + goto EOF; + } + + if (lastFrameControl is null) + { + PngThrowHelper.ThrowMissingFrameControl(); + } + + // Skip sequence number + this.currentStream.Skip(4); + this.SkipChunkDataAndCrc(chunk); + break; + case PngChunkType.Data: // Spec says tRNS must be before IDAT so safe to exit. if (this.colorMetadataOnly) { @@ -292,12 +390,15 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.SkipChunkDataAndCrc(chunk); break; + case PngChunkType.Palette: + this.palette = chunk.Data.GetSpan().ToArray(); + break; + case PngChunkType.Transparency: - byte[] alpha = new byte[chunk.Length]; - chunk.Data.GetSpan().CopyTo(alpha); - this.paletteAlpha = alpha; - this.AssignTransparentMarkers(alpha, pngMetadata); + this.paletteAlpha = chunk.Data.GetSpan().ToArray(); + this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata); + // Spec says tRNS must be after PLTE so safe to exit. if (this.colorMetadataOnly) { goto EOF; @@ -367,9 +468,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals EOF: if (this.header.Width == 0 && this.header.Height == 0) { - PngThrowHelper.ThrowNoHeader(); + PngThrowHelper.ThrowInvalidHeader(); } + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); + return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata); } finally @@ -398,7 +502,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// The number of bits per value. /// The new array. /// The resulting array. - private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, out IMemoryOwner buffer) + private bool TryScaleUpTo8BitArray(ReadOnlySpan source, int bytesPerScanline, int bits, [NotNullWhen(true)] out IMemoryOwner? buffer) { if (bits >= 8) { @@ -433,7 +537,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// The data containing physical data. private static void ReadPhysicalChunk(ImageMetadata metadata, ReadOnlySpan data) { - PhysicalChunkData physicalChunk = PhysicalChunkData.Parse(data); + PngPhysical physicalChunk = PngPhysical.Parse(data); metadata.ResolutionUnits = physicalChunk.UnitSpecifier == byte.MinValue ? PixelResolutionUnit.AspectRatio @@ -466,8 +570,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The type the pixels will be /// The metadata information for the image + /// The frame control information for the frame /// The image that we will populate - private void InitializeImage(ImageMetadata metadata, out Image image) + private void InitializeImage(ImageMetadata metadata, FrameControl frameControl, out Image image) where TPixel : unmanaged, IPixel { image = Image.CreateUninitialized( @@ -476,6 +581,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.header.Height, metadata); + PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata(); + frameMetadata.FromChunk(in frameControl); + this.bytesPerPixel = this.CalculateBytesPerPixel(); this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; this.bytesPerSample = 1; @@ -490,6 +598,47 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); } + /// + /// Initializes the image and various buffers needed for processing + /// + /// The type the pixels will be + /// The frame control information for the previous frame. + /// The frame control information for the current frame. + /// The image that we will populate + /// The previous frame. + /// The created frame + private void InitializeFrame( + FrameControl previousFrameControl, + FrameControl currentFrameControl, + Image image, + ImageFrame? previousFrame, + out ImageFrame frame) + where TPixel : unmanaged, IPixel + { + // We create a clone of the previous frame and add it. + // We will overpaint the difference of pixels on the current frame to create a complete image. + // This ensures that we have enough pixel data to process without distortion. #2450 + frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame); + + // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. + if (previousFrameControl.DisposeOperation == PngDisposalMethod.Background + || (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.Previous)) + { + Rectangle restoreArea = previousFrameControl.Bounds; + Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea); + Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest); + pixelRegion.Clear(); + } + + PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata(); + frameMetadata.FromChunk(currentFrameControl); + + this.previousScanline?.Dispose(); + this.scanline?.Dispose(); + this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); + } + /// /// Calculates the correct number of bits per pixel for the given color type. /// @@ -553,24 +702,32 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// Reads the scanlines within the image. /// /// The pixel format. - /// The png chunk containing the compressed scanline data. + /// The length of the chunk that containing the compressed scanline data. /// The pixel data. /// The png metadata + /// A delegate to get more data from the inner stream for . + /// The frame control /// The cancellation token. - private void ReadScanlines(PngChunk chunk, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void ReadScanlines( + int chunkLength, + ImageFrame image, + PngMetadata pngMetadata, + Func getData, + in FrameControl frameControl, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - using ZlibInflateStream deframeStream = new(this.currentStream, this.ReadNextDataChunk); - deframeStream.AllocateNewBytes(chunk.Length, true); - DeflateStream dataStream = deframeStream.CompressedStream; + using ZlibInflateStream inflateStream = new(this.currentStream, getData); + inflateStream.AllocateNewBytes(chunkLength, true); + DeflateStream dataStream = inflateStream.CompressedStream!; - if (this.header.InterlaceMethod == PngInterlaceMode.Adam7) + if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) { - this.DecodeInterlacedPixelData(dataStream, image, pngMetadata, cancellationToken); + this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); } else { - this.DecodePixelData(dataStream, image, pngMetadata, cancellationToken); + this.DecodePixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); } } @@ -578,29 +735,48 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// Decodes the raw pixel data row by row /// /// The pixel format. + /// The frame control /// The compressed pixel data stream. - /// The image to decode to. + /// The image frame to decode to. /// The png metadata /// The CancellationToken - private void DecodePixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void DecodePixelData( + FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame imageFrame, + PngMetadata pngMetadata, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - while (this.currentRow < this.header.Height) + int currentRow = (int)frameControl.YOffset; + int currentRowBytesRead = 0; + int height = (int)frameControl.YMax; + + IMemoryOwner? blendMemory = null; + Span blendRowBuffer = Span.Empty; + if (frameControl.BlendOperation == PngBlendMethod.Over) + { + blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean); + blendRowBuffer = blendMemory.Memory.Span; + } + + while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); - Span scanlineSpan = this.scanline.GetSpan(); - while (this.currentRowBytesRead < this.bytesPerScanline) + int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1; + Span scanlineSpan = this.scanline.GetSpan()[..bytesPerFrameScanline]; + while (currentRowBytesRead < bytesPerFrameScanline) { - int bytesRead = compressedStream.Read(scanlineSpan, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead); + int bytesRead = compressedStream.Read(scanlineSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead); if (bytesRead <= 0) { return; } - this.currentRowBytesRead += bytesRead; + currentRowBytesRead += bytesRead; } - this.currentRowBytesRead = 0; + currentRowBytesRead = 0; switch ((FilterType)scanlineSpan[0]) { @@ -628,28 +804,47 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } - this.ProcessDefilteredScanline(scanlineSpan, image, pngMetadata); - + this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, imageFrame, pngMetadata, blendRowBuffer); this.SwapScanlineBuffers(); - this.currentRow++; + currentRow++; } + + blendMemory?.Dispose(); } /// /// Decodes the raw interlaced pixel data row by row - /// /// /// The pixel format. + /// The frame control /// The compressed pixel data stream. - /// The current image. + /// The current image frame. /// The png metadata. /// The cancellation token. - private void DecodeInterlacedPixelData(DeflateStream compressedStream, ImageFrame image, PngMetadata pngMetadata, CancellationToken cancellationToken) + private void DecodeInterlacedPixelData( + in FrameControl frameControl, + DeflateStream compressedStream, + ImageFrame imageFrame, + PngMetadata pngMetadata, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + int currentRow = Adam7.FirstRow[0] + (int)frameControl.YOffset; + int currentRowBytesRead = 0; int pass = 0; - int width = this.header.Width; - Buffer2D imageBuffer = image.PixelBuffer; + int width = (int)frameControl.Width; + int endRow = (int)frameControl.YMax; + + Buffer2D imageBuffer = imageFrame.PixelBuffer; + + IMemoryOwner? blendMemory = null; + Span blendRowBuffer = Span.Empty; + if (frameControl.BlendOperation == PngBlendMethod.Over) + { + blendMemory = this.memoryAllocator.Allocate(imageFrame.Width, AllocationOptions.Clean); + blendRowBuffer = blendMemory.Memory.Span; + } + while (true) { int numColumns = Adam7.ComputeColumns(width, pass); @@ -664,21 +859,21 @@ internal sealed class PngDecoderCore : IImageDecoderInternals int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; - while (this.currentRow < this.header.Height) + while (currentRow < endRow) { cancellationToken.ThrowIfCancellationRequested(); - while (this.currentRowBytesRead < bytesPerInterlaceScanline) + while (currentRowBytesRead < bytesPerInterlaceScanline) { - int bytesRead = compressedStream.Read(this.scanline.GetSpan(), this.currentRowBytesRead, bytesPerInterlaceScanline - this.currentRowBytesRead); + int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead); if (bytesRead <= 0) { return; } - this.currentRowBytesRead += bytesRead; + currentRowBytesRead += bytesRead; } - this.currentRowBytesRead = 0; + currentRowBytesRead = 0; Span scanSpan = this.scanline.Slice(0, bytesPerInterlaceScanline); Span prevSpan = this.previousScanline.Slice(0, bytesPerInterlaceScanline); @@ -709,12 +904,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } - Span rowSpan = imageBuffer.DangerousGetRowSpan(this.currentRow); - this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]); - + Span rowSpan = imageBuffer.DangerousGetRowSpan(currentRow); + this.ProcessInterlacedDefilteredScanline( + frameControl, + this.scanline.GetSpan(), + rowSpan, + pngMetadata, + blendRowBuffer, + pixelOffset: Adam7.FirstColumn[pass], + increment: Adam7.ColumnIncrement[pass]); + + blendRowBuffer.Clear(); this.SwapScanlineBuffers(); - this.currentRow += Adam7.RowIncrement[pass]; + currentRow += Adam7.RowIncrement[pass]; } pass++; @@ -722,7 +925,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals if (pass < 7) { - this.currentRow = Adam7.FirstRow[pass]; + currentRow = Adam7.FirstRow[pass]; } else { @@ -730,27 +933,44 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } } + + blendMemory?.Dispose(); } /// /// Processes the de-filtered scanline filling the image pixel data /// /// The pixel format. - /// The de-filtered scanline + /// The frame control + /// The index of the current scanline being processed. + /// The de-filtered scanline /// The image /// The png metadata. - private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) + /// A span used to temporarily hold the decoded row pixel data for alpha blending. + private void ProcessDefilteredScanline( + in FrameControl frameControl, + int currentRow, + ReadOnlySpan scanline, + ImageFrame pixels, + PngMetadata pngMetadata, + Span blendRowBuffer) where TPixel : unmanaged, IPixel { - Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow); + Span destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); + + bool blend = frameControl.BlendOperation == PngBlendMethod.Over; + Span rowSpan = blend + ? blendRowBuffer + : destination; // Trim the first marker byte from the buffer - ReadOnlySpan trimmed = defilteredScanline[1..]; + ReadOnlySpan trimmed = scanline[1..]; // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. - IMemoryOwner buffer = null; + IMemoryOwner? buffer = null; try { + // TODO: The allocation here could be per frame, not per scanline. ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( trimmed, this.bytesPerScanline - 1, @@ -763,18 +983,18 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { case PngColorType.Grayscale: PngScanlineProcessor.ProcessGrayscaleScanline( - this.header, + this.header.BitDepth, + in frameControl, scanlineSpan, rowSpan, - pngMetadata.HasTransparency, - pngMetadata.TransparentL16.GetValueOrDefault(), - pngMetadata.TransparentL8.GetValueOrDefault()); + pngMetadata.TransparentColor); break; case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline( - this.header, + this.header.BitDepth, + in frameControl, scanlineSpan, rowSpan, (uint)this.bytesPerPixel, @@ -784,32 +1004,31 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Palette: PngScanlineProcessor.ProcessPaletteScanline( - this.header, + in frameControl, scanlineSpan, rowSpan, - this.palette, - this.paletteAlpha); + pngMetadata.ColorTable); break; case PngColorType.Rgb: PngScanlineProcessor.ProcessRgbScanline( this.configuration, - this.header, + this.header.BitDepth, + frameControl, scanlineSpan, rowSpan, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTransparency, - pngMetadata.TransparentRgb48.GetValueOrDefault(), - pngMetadata.TransparentRgb24.GetValueOrDefault()); + pngMetadata.TransparentColor); break; case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessRgbaScanline( this.configuration, - this.header, + this.header.BitDepth, + in frameControl, scanlineSpan, rowSpan, this.bytesPerPixel, @@ -817,6 +1036,13 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } + + if (blend) + { + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + blender.Blend(this.configuration, destination, destination, rowSpan, 1f); + } } finally { @@ -828,19 +1054,33 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// Processes the interlaced de-filtered scanline filling the image pixel data /// /// The pixel format. - /// The de-filtered scanline - /// The current image row. + /// The frame control + /// The de-filtered scanline + /// The current image row. /// The png metadata. + /// A span used to temporarily hold the decoded row pixel data for alpha blending. /// The column start index. Always 0 for none interlaced images. /// The column increment. Always 1 for none interlaced images. - private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defilteredScanline, Span rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1) + private void ProcessInterlacedDefilteredScanline( + in FrameControl frameControl, + ReadOnlySpan scanline, + Span destination, + PngMetadata pngMetadata, + Span blendRowBuffer, + int pixelOffset = 0, + int increment = 1) where TPixel : unmanaged, IPixel { + bool blend = frameControl.BlendOperation == PngBlendMethod.Over; + Span rowSpan = blend + ? blendRowBuffer + : destination; + // Trim the first marker byte from the buffer - ReadOnlySpan trimmed = defilteredScanline[1..]; + ReadOnlySpan trimmed = scanline[1..]; // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. - IMemoryOwner buffer = null; + IMemoryOwner? buffer = null; try { ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray( @@ -855,20 +1095,20 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { case PngColorType.Grayscale: PngScanlineProcessor.ProcessInterlacedGrayscaleScanline( - this.header, + this.header.BitDepth, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.HasTransparency, - pngMetadata.TransparentL16.GetValueOrDefault(), - pngMetadata.TransparentL8.GetValueOrDefault()); + pngMetadata.TransparentColor); break; case PngColorType.GrayscaleWithAlpha: PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline( - this.header, + this.header.BitDepth, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -880,34 +1120,35 @@ internal sealed class PngDecoderCore : IImageDecoderInternals case PngColorType.Palette: PngScanlineProcessor.ProcessInterlacedPaletteScanline( - this.header, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, (uint)increment, - this.palette, - this.paletteAlpha); + pngMetadata.ColorTable); break; case PngColorType.Rgb: PngScanlineProcessor.ProcessInterlacedRgbScanline( - this.header, + this.configuration, + this.header.BitDepth, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, (uint)increment, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTransparency, - pngMetadata.TransparentRgb48.GetValueOrDefault(), - pngMetadata.TransparentRgb24.GetValueOrDefault()); + pngMetadata.TransparentColor); break; case PngColorType.RgbWithAlpha: PngScanlineProcessor.ProcessInterlacedRgbaScanline( - this.header, + this.configuration, + this.header.BitDepth, + in frameControl, scanlineSpan, rowSpan, (uint)pixelOffset, @@ -917,6 +1158,13 @@ internal sealed class PngDecoderCore : IImageDecoderInternals break; } + + if (blend) + { + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + blender.Blend(this.configuration, destination, destination, rowSpan, 1f); + } } finally { @@ -924,10 +1172,44 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } } + /// + /// Decodes and assigns the color palette to the metadata + /// + /// The palette buffer. + /// The alpha palette buffer. + /// The png metadata. + private static void AssignColorPalette(ReadOnlySpan palette, ReadOnlySpan alpha, PngMetadata pngMetadata) + { + if (palette.Length == 0) + { + return; + } + + Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf()]; + ReadOnlySpan rgbTable = MemoryMarshal.Cast(palette); + for (int i = 0; i < colorTable.Length; i++) + { + colorTable[i] = new Color(rgbTable[i]); + } + + if (alpha.Length > 0) + { + // The alpha chunk may contain as many transparency entries as there are palette entries + // (more than that would not make any sense) or as few as one. + for (int i = 0; i < alpha.Length; i++) + { + ref Color color = ref colorTable[i]; + color = color.WithAlpha(alpha[i] / 255F); + } + } + + pngMetadata.ColorTable = colorTable; + } + /// /// Decodes and assigns marker colors that identify transparent pixels in non indexed images. /// - /// The alpha tRNS array. + /// The alpha tRNS buffer. /// The png metadata. private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngMetadata) { @@ -941,16 +1223,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2)); ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); - pngMetadata.TransparentRgb48 = new Rgb48(rc, gc, bc); - pngMetadata.HasTransparency = true; + pngMetadata.TransparentColor = new(new Rgb48(rc, gc, bc)); return; } byte r = ReadByteLittleEndian(alpha, 0); byte g = ReadByteLittleEndian(alpha, 2); byte b = ReadByteLittleEndian(alpha, 4); - pngMetadata.TransparentRgb24 = new Rgb24(r, g, b); - pngMetadata.HasTransparency = true; + pngMetadata.TransparentColor = new(new Rgb24(r, g, b)); } } else if (this.pngColorType == PngColorType.Grayscale) @@ -959,20 +1239,39 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { if (this.header.BitDepth == 16) { - pngMetadata.TransparentL16 = new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])); + pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2]))); } else { - pngMetadata.TransparentL8 = new L8(ReadByteLittleEndian(alpha, 0)); + pngMetadata.TransparentColor = Color.FromPixel(new L8(ReadByteLittleEndian(alpha, 0))); } - - pngMetadata.HasTransparency = true; } } - else if (this.pngColorType == PngColorType.Palette && alpha.Length > 0) - { - pngMetadata.HasTransparency = true; - } + } + + /// + /// Reads a animation control chunk from the data. + /// + /// The png metadata. + /// The containing data. + private void ReadAnimationControlChunk(PngMetadata pngMetadata, ReadOnlySpan data) + { + this.animationControl = AnimationControl.Parse(data); + + pngMetadata.RepeatCount = this.animationControl.NumberPlays; + } + + /// + /// Reads a header chunk from the data. + /// + /// The containing data. + private FrameControl ReadFrameControlChunk(ReadOnlySpan data) + { + FrameControl fcTL = FrameControl.Parse(data); + + fcTL.Validate(this.header); + + return fcTL; } /// @@ -1062,7 +1361,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ReadOnlySpan compressedData = data[(zeroIndex + 2)..]; - if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string uncompressed) + if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed) && !TryReadTextChunkMetadata(baseMetadata, name, uncompressed)) { metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty)); @@ -1355,7 +1654,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { ReadOnlySpan compressedData = data[dataStartIdx..]; - if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string uncompressed)) + if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string? uncompressed)) { pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword)); } @@ -1378,7 +1677,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// The string encoding to use. /// The uncompressed value. /// The . - private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value) + private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, [NotNullWhen(true)] out string? value) { if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData)) { @@ -1403,11 +1702,11 @@ internal sealed class PngDecoderCore : IImageDecoderInternals Span buffer = stackalloc byte[20]; - this.currentStream.Read(buffer, 0, 4); + _ = this.currentStream.Read(buffer, 0, 4); if (this.TryReadChunk(buffer, out PngChunk chunk)) { - if (chunk.Type == PngChunkType.Data) + if (chunk.Type is PngChunkType.Data or PngChunkType.FrameData) { chunk.Data?.Dispose(); return chunk.Length; @@ -1419,6 +1718,22 @@ internal sealed class PngDecoderCore : IImageDecoderInternals return 0; } + /// + /// Reads the next data chunk and skip sequence number. + /// + /// Count of bytes in the next data chunk, or 0 if there are no more data chunks left. + private int ReadNextDataChunkAndSkipSeq() + { + int length = this.ReadNextDataChunk(); + if (this.ReadNextDataChunk() is 0) + { + return length; + } + + this.currentStream.Position += 4; // Skip sequence number + return length - 4; + } + /// /// Reads a chunk from the stream. /// @@ -1461,7 +1776,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals // If we're reading color metadata only we're only interested in the IHDR and tRNS chunks. // We can skip all other chunk data in the stream for better performance. - if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency) + if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency && type != PngChunkType.Palette) { chunk = new PngChunk(length, type); @@ -1476,9 +1791,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.ValidateChunk(chunk, buffer); - // Restore the stream position for IDAT chunks, because it will be decoded later and + // Restore the stream position for IDAT and fdAT chunks, because it will be decoded later and // was only read to verifying the CRC is correct. - if (type == PngChunkType.Data) + if (type is PngChunkType.Data or PngChunkType.FrameData) { this.currentStream.Position = pos; } diff --git a/src/ImageSharp/Formats/Png/PngDisposalMethod.cs b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs new file mode 100644 index 0000000000..17391de95c --- /dev/null +++ b/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; + +/// +/// Specifies how the output buffer should be changed at the end of the delay (before rendering the next frame). +/// +public enum PngDisposalMethod +{ + /// + /// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. + /// + None, + + /// + /// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. + /// + Background, + + /// + /// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. + /// + Previous +} diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 595601522e..dcbaf3140d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. #nullable disable -using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Png; @@ -11,6 +11,16 @@ namespace SixLabors.ImageSharp.Formats.Png; /// public class PngEncoder : QuantizingImageEncoder { + /// + /// Initializes a new instance of the class. + /// + 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(); + /// /// Gets the number of bits per sample or per palette index (not per pixel). /// Not all values are allowed for all values. @@ -68,7 +78,7 @@ public class PngEncoder : QuantizingImageEncoder /// protected override void Encode(Image 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); } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 175a9f777d..04e3b1d840 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -1,12 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#nullable disable using System.Buffers; using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -101,29 +99,34 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// The raw data of previous scanline. /// - private IMemoryOwner previousScanline; + private IMemoryOwner previousScanline = null!; /// /// The raw data of current scanline. /// - private IMemoryOwner currentScanline; + private IMemoryOwner currentScanline = null!; /// /// The color profile name. /// private const string ColorProfileName = "ICC Profile"; + /// + /// The encoder quantizer, if present. + /// + private IQuantizer? quantizer; + /// /// Initializes a new instance of the class. /// - /// The to use for buffer allocations. /// The configuration. /// The encoder with options. - public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoder encoder) + public PngEncoderCore(Configuration configuration, PngEncoder encoder) { - this.memoryAllocator = memoryAllocator; this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; this.encoder = encoder; + this.quantizer = encoder.Quantizer; } /// @@ -143,20 +146,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.height = image.Height; ImageMetadata metadata = image.Metadata; - PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); - Image clonedImage = null; - bool clearTransparency = this.encoder.TransparentColorMode == PngTransparentColorMode.Clear; + + stream.Write(PngConstants.HeaderBytes); + + ImageFrame? clonedFrame = null; + ImageFrame currentFrame = image.Frames.RootFrame; + + bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear; if (clearTransparency) { - clonedImage = image.Clone(); - ClearTransparentPixels(clonedImage); + currentFrame = clonedFrame = currentFrame.Clone(); + ClearTransparentPixels(currentFrame); } - IndexedImageFrame quantized = this.CreateQuantizedImageAndUpdateBitDepth(image, clonedImage); - - stream.Write(PngConstants.HeaderBytes); + // Do not move this. We require an accurate bit depth for the header chunk. + IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null); this.WriteHeaderChunk(stream); this.WriteGammaChunk(stream); @@ -167,13 +173,58 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.WriteExifChunk(stream, metadata); this.WriteXmpChunk(stream, metadata); 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? 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); stream.Flush(); + // Dispose of allocations from final frame. + clonedFrame?.Dispose(); quantized?.Dispose(); - clonedImage?.Dispose(); } /// @@ -181,18 +232,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { this.previousScanline?.Dispose(); this.currentScanline?.Dispose(); - this.previousScanline = null; - this.currentScanline = null; } /// /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases. /// /// The type of the pixel. - /// The cloned image where the transparent pixels will be changed. - private static void ClearTransparentPixels(Image image) - where TPixel : unmanaged, IPixel => - image.ProcessPixelRows(accessor => + /// The cloned image frame where the transparent pixels will be changed. + private static void ClearTransparentPixels(ImageFrame clone) + where TPixel : unmanaged, IPixel + => clone.ProcessPixelRows(accessor => { // TODO: We should be able to speed this up with SIMD and masking. Rgba32 rgba32 = default; @@ -204,7 +253,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { span[x].ToRgba32(ref rgba32); - if (rgba32.A == 0) + if (rgba32.A is 0) { 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. /// /// The type of the pixel. - /// The image to quantize. - /// Cloned image with transparent pixels are changed to black. + /// The image metadata. + /// The frame to quantize. + /// Any previously derived palette. /// The quantized image. - private IndexedImageFrame CreateQuantizedImageAndUpdateBitDepth( - Image image, - Image clonedImage) + private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth( + PngMetadata metadata, + ImageFrame frame, + ReadOnlyMemory? previousPalette) where TPixel : unmanaged, IPixel { - IndexedImageFrame quantized; - 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); - } - + IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, previousPalette); this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); return quantized; } @@ -244,9 +286,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private void CollectGrayscaleBytes(ReadOnlySpan rowSpan) where TPixel : unmanaged, IPixel { - ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); Span rawScanlineSpan = this.currentScanline.GetSpan(); - ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan); if (this.colorType == PngColorType.Grayscale) { @@ -402,20 +442,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The row span. /// The quantized pixels. Can be null. /// The row. - private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row) + private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame? quantized, int row) where TPixel : unmanaged, IPixel { switch (this.colorType) { case PngColorType.Palette: - 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 { - quantized.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan()); + quantized?.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan()); } break; @@ -476,7 +515,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable ReadOnlySpan rowSpan, ref Span filter, ref Span attempt, - IndexedImageFrame quantized, + IndexedImageFrame? quantized, int row) where TPixel : unmanaged, IPixel { @@ -576,6 +615,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size); } + /// + /// Writes the animation control chunk to the stream. + /// + /// The containing image data. + /// The number of frames. + /// The number of times to loop this APNG. + 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); + } + /// /// Writes the palette chunk to the stream. /// Should be written before the first IDAT chunk. @@ -583,7 +637,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The pixel format. /// The containing image data. /// The quantized frame. - private void WritePaletteChunk(Stream stream, IndexedImageFrame quantized) + private void WritePaletteChunk(Stream stream, IndexedImageFrame? quantized) where TPixel : unmanaged, IPixel { if (quantized is null) @@ -642,14 +696,14 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The image metadata. private void WritePhysicalChunk(Stream stream, ImageMetadata meta) { - if ((this.chunkFilter & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk) + if (this.chunkFilter.HasFlag(PngChunkFilter.ExcludePhysicalChunk)) { 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); } /// @@ -691,9 +745,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable return; } - byte[] xmpData = meta.XmpProfile.Data; + byte[]? xmpData = meta.XmpProfile.Data; - if (xmpData.Length == 0) + if (xmpData?.Length is 0 or null) { return; } @@ -760,18 +814,9 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } 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 = false; - foreach (char c in textData.Value) - { - if (c > maxLatinCode) - { - hasUnicodeCharacters = true; - break; - } - } + bool hasUnicodeCharacters = textData.Value.Any(c => c > maxLatinCode); 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. 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); } @@ -889,7 +934,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The image metadata. private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) { - if (!pngMetadata.HasTransparency) + if (pngMetadata.TransparentColor is null) { return; } @@ -897,19 +942,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span alpha = this.chunkDataBuffer.Span; 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(); BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); } - else if (pngMetadata.TransparentRgb24.HasValue) + else { alpha.Clear(); - Rgb24 rgb = pngMetadata.TransparentRgb24.Value; + Rgb24 rgb = pngMetadata.TransparentColor.Value.ToRgb24(); alpha[1] = rgb.R; alpha[3] = rgb.G; alpha[5] = rgb.B; @@ -918,28 +963,61 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } 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(); + BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); } - else if (pngMetadata.TransparentL8.HasValue) + else { + L8 l8 = pngMetadata.TransparentColor.Value.ToPixel(); alpha.Clear(); - alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; + alpha[1] = l8.PackedValue; this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); } } } + /// + /// Writes the animation control chunk to the stream. + /// + /// The containing image data. + /// The image frame. + /// The frame sequence number. + 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; + } + /// /// Writes the pixel information to the stream. /// /// The pixel format. - /// The image. + /// The frame control + /// The frame. /// The quantized pixel data. Can be null. /// The stream. - private void WriteDataChunks(Image pixels, IndexedImageFrame quantized, Stream stream) + /// Is writing fdAT or IDAT. + private uint WriteDataChunks(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame) where TPixel : unmanaged, IPixel { byte[] buffer; @@ -949,20 +1027,20 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { 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 { - this.EncodeAdam7Pixels(pixels, deflateStream); + this.EncodeAdam7Pixels(frameControl, pixels, deflateStream); } } 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. // 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++; } 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; } /// @@ -1009,13 +1105,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Encodes the pixels. /// /// The type of the pixel. + /// The frame control /// The pixels. /// The quantized pixels span. /// The deflate stream. - private void EncodePixels(Image pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) + private void EncodePixels(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - 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; this.AllocateScanlineBuffers(bytesPerScanline); @@ -1026,7 +1126,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { Span filter = filterBuffer.GetSpan(); Span 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); deflateStream.Write(filter); @@ -1039,18 +1139,19 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Interlaced encoding the pixels. /// /// The type of the pixel. - /// The image. + /// The frame control + /// The image frame. /// The deflate stream. - private void EncodeAdam7Pixels(Image image, ZlibDeflateStream deflateStream) + private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame frame, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - Buffer2D pixelBuffer = image.Frames.RootFrame.PixelBuffer; + int width = (int)frameControl.XMax; + int height = (int)frameControl.YMax; + Buffer2D pixelBuffer = frame.PixelBuffer; for (int pass = 0; pass < 7; pass++) { - int startRow = Adam7.FirstRow[pass]; - int startCol = Adam7.FirstColumn[pass]; + int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset; + int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset; int blockWidth = Adam7.ComputeBlockWidth(width, pass); int bytesPerScanline = this.bytesPerPixel <= 1 @@ -1072,7 +1173,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { // Collect pixel data Span 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]; } @@ -1092,17 +1193,18 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// Interlaced encoding the quantized (indexed, with palette) pixels. /// /// The type of the pixel. + /// The frame control /// The quantized. /// The deflate stream. - private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream) + private void EncodeAdam7IndexedPixels(FrameControl frameControl, IndexedImageFrame quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = quantized.Width; - int height = quantized.Height; + int width = (int)frameControl.Width; + int endRow = (int)frameControl.YMax; for (int pass = 0; pass < 7; pass++) { - int startRow = Adam7.FirstRow[pass]; - int startCol = Adam7.FirstColumn[pass]; + int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset; + int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset; int blockWidth = Adam7.ComputeBlockWidth(width, pass); int bytesPerScanline = this.bytesPerPixel <= 1 @@ -1121,17 +1223,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span filter = filterBuffer.GetSpan(); Span attempt = attemptBuffer.GetSpan(); - for (int row = startRow; - row < height; - row += Adam7.RowIncrement[pass]) + for (int row = startRow; row < endRow; row += Adam7.RowIncrement[pass]) { // Collect data ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row); for (int col = startCol, i = 0; - col < width; + col < frameControl.XMax; col += Adam7.ColumnIncrement[pass]) { - block[i++] = srcRow[col]; + block[i] = srcRow[col]; + i++; } // Encode data @@ -1163,7 +1264,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// The to write to. /// The type of chunk to write. - /// The containing data. + /// The containing data. /// The position to offset the data at. /// The of the data to write. private void WriteChunk(Stream stream, PngChunkType type, Span data, int offset, int length) @@ -1175,7 +1276,39 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable 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 + } + + /// + /// Writes a frame data chunk of a specified length to the stream at the given offset. + /// + /// The to write to. + /// The frame sequence number. + /// The containing data. + /// The position to offset the data at. + /// The of the data to write. + private void WriteFrameDataChunk(Stream stream, uint sequenceNumber, Span data, int offset, int length) + { + Span 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) { @@ -1198,7 +1331,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// 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 amount = scanlineLength % mod; @@ -1242,14 +1375,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable if (!encoder.FilterMethod.HasValue) { // Specification recommends default filter method None for paletted images and Paeth for others. - if (this.colorType == PngColorType.Palette) - { - this.filterMethod = PngFilterMethod.None; - } - else - { - this.filterMethod = PngFilterMethod.Paeth; - } + this.filterMethod = this.colorType is PngColorType.Palette ? PngFilterMethod.None : PngFilterMethod.Paeth; } // 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; 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; } @@ -1276,28 +1402,50 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The png encoder. /// The color type. /// The bits per component. - /// The image. - private static IndexedImageFrame CreateQuantizedFrame( + /// The image metadata. + /// The frame to quantize. + /// Any previously derived palette. + private IndexedImageFrame? CreateQuantizedFrame( QuantizingImageEncoder encoder, PngColorType colorType, byte bitDepth, - Image image) + PngMetadata metadata, + ImageFrame frame, + ReadOnlyMemory? previousPalette) where TPixel : unmanaged, IPixel { - if (colorType != PngColorType.Palette) + if (colorType is not PngColorType.Palette) { 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 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. - IQuantizer quantizer = encoder.Quantizer - ?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); + if (this.quantizer is null) + { + 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. - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(image.GetConfiguration()); + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration); - frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image); - return frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); + frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); + return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } /// @@ -1311,25 +1459,23 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable private static byte CalculateBitDepth( PngColorType colorType, byte bitDepth, - IndexedImageFrame quantizedFrame) + IndexedImageFrame? quantizedFrame) where TPixel : unmanaged, IPixel { - 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); // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk // We check again for the bit depth as the bit depth of the color palette from a given quantizer might not // be within the acceptable range. - if (bits == 3) + bits = bits switch { - bits = 4; - } - else if (bits is >= 5 and <= 7) - { - bits = 8; - } + 3 => 4, + >= 5 and <= 7 => 8, + _ => bits + }; bitDepth = bits; } @@ -1367,21 +1513,21 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The type of pixel format. private static PngColorType SuggestColorType() where TPixel : unmanaged, IPixel - => typeof(TPixel) switch + => default(TPixel) switch { - Type t when t == typeof(A8) => PngColorType.GrayscaleWithAlpha, - Type t when t == typeof(Argb32) => PngColorType.RgbWithAlpha, - Type t when t == typeof(Bgr24) => PngColorType.Rgb, - Type t when t == typeof(Bgra32) => PngColorType.RgbWithAlpha, - Type t when t == typeof(L8) => PngColorType.Grayscale, - Type t when t == typeof(L16) => PngColorType.Grayscale, - Type t when t == typeof(La16) => PngColorType.GrayscaleWithAlpha, - Type t when t == typeof(La32) => PngColorType.GrayscaleWithAlpha, - Type t when t == typeof(Rgb24) => PngColorType.Rgb, - Type t when t == typeof(Rgba32) => PngColorType.RgbWithAlpha, - Type t when t == typeof(Rgb48) => PngColorType.Rgb, - Type t when t == typeof(Rgba64) => PngColorType.RgbWithAlpha, - Type t when t == typeof(RgbaVector) => PngColorType.RgbWithAlpha, + A8 => PngColorType.GrayscaleWithAlpha, + Argb32 => PngColorType.RgbWithAlpha, + Bgr24 => PngColorType.Rgb, + Bgra32 => PngColorType.RgbWithAlpha, + L8 => PngColorType.Grayscale, + L16 => PngColorType.Grayscale, + La16 => PngColorType.GrayscaleWithAlpha, + La32 => PngColorType.GrayscaleWithAlpha, + Rgb24 => PngColorType.Rgb, + Rgba32 => PngColorType.RgbWithAlpha, + Rgb48 => PngColorType.Rgb, + Rgba64 => PngColorType.RgbWithAlpha, + RgbaVector => PngColorType.RgbWithAlpha, _ => PngColorType.RgbWithAlpha }; @@ -1392,27 +1538,27 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The type of pixel format. private static PngBitDepth SuggestBitDepth() where TPixel : unmanaged, IPixel - => typeof(TPixel) switch + => default(TPixel) switch { - Type t when t == typeof(A8) => PngBitDepth.Bit8, - Type t when t == typeof(Argb32) => PngBitDepth.Bit8, - Type t when t == typeof(Bgr24) => PngBitDepth.Bit8, - Type t when t == typeof(Bgra32) => PngBitDepth.Bit8, - Type t when t == typeof(L8) => PngBitDepth.Bit8, - Type t when t == typeof(L16) => PngBitDepth.Bit16, - Type t when t == typeof(La16) => PngBitDepth.Bit8, - Type t when t == typeof(La32) => PngBitDepth.Bit16, - Type t when t == typeof(Rgb24) => PngBitDepth.Bit8, - Type t when t == typeof(Rgba32) => PngBitDepth.Bit8, - Type t when t == typeof(Rgb48) => PngBitDepth.Bit16, - Type t when t == typeof(Rgba64) => PngBitDepth.Bit16, - Type t when t == typeof(RgbaVector) => PngBitDepth.Bit16, + A8 => PngBitDepth.Bit8, + Argb32 => PngBitDepth.Bit8, + Bgr24 => PngBitDepth.Bit8, + Bgra32 => PngBitDepth.Bit8, + L8 => PngBitDepth.Bit8, + L16 => PngBitDepth.Bit16, + La16 => PngBitDepth.Bit8, + La32 => PngBitDepth.Bit16, + Rgb24 => PngBitDepth.Bit8, + Rgba32 => PngBitDepth.Bit8, + Rgb48 => PngBitDepth.Bit16, + Rgba64 => PngBitDepth.Bit16, + RgbaVector => PngBitDepth.Bit16, _ => PngBitDepth.Bit8 }; private unsafe struct ScratchBuffer { - private const int Size = 16; + private const int Size = 26; private fixed byte scratch[Size]; public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); diff --git a/src/ImageSharp/Formats/Png/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs index 2d1f2dcc7d..e5852affa9 100644 --- a/src/ImageSharp/Formats/Png/PngFormat.cs +++ b/src/ImageSharp/Formats/Png/PngFormat.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Registers the image encoders, decoders and mime type detectors for the png format. /// -public sealed class PngFormat : IImageFormat +public sealed class PngFormat : IImageFormat { private PngFormat() { @@ -31,4 +31,7 @@ public sealed class PngFormat : IImageFormat /// public PngMetadata CreateDefaultFormatMetadata() => new(); + + /// + public PngFrameMetadata CreateDefaultFormatFrameMetadata() => new(); } diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs new file mode 100644 index 0000000000..ca4d8c1f45 --- /dev/null +++ b/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; + +/// +/// Provides APng specific metadata information for the image frame. +/// +public class PngFrameMetadata : IDeepCloneable +{ + /// + /// Initializes a new instance of the class. + /// + public PngFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private PngFrameMetadata(PngFrameMetadata other) + { + this.FrameDelay = other.FrameDelay; + this.DisposalMethod = other.DisposalMethod; + this.BlendMethod = other.BlendMethod; + } + + /// + /// 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. + /// + public Rational FrameDelay { get; set; } + + /// + /// Gets or sets the type of frame area disposal to be done after rendering this frame + /// + public PngDisposalMethod DisposalMethod { get; set; } + + /// + /// Gets or sets the type of frame area rendering for this frame + /// + public PngBlendMethod BlendMethod { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The chunk to create an instance from. + internal void FromChunk(in FrameControl frameControl) + { + this.FrameDelay = new Rational(frameControl.DelayNumerator, frameControl.DelayDenominator); + this.DisposalMethod = frameControl.DisposeOperation; + this.BlendMethod = frameControl.BlendOperation; + } + + /// + public IDeepCloneable DeepClone() => new PngFrameMetadata(this); +} diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 9ff3905fe1..b113dbfc17 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; @@ -27,11 +28,13 @@ public class PngMetadata : IDeepCloneable this.ColorType = other.ColorType; this.Gamma = other.Gamma; this.InterlaceMethod = other.InterlaceMethod; - this.HasTransparency = other.HasTransparency; - this.TransparentL8 = other.TransparentL8; - this.TransparentL16 = other.TransparentL16; - this.TransparentRgb24 = other.TransparentRgb24; - this.TransparentRgb48 = other.TransparentRgb48; + this.TransparentColor = other.TransparentColor; + this.RepeatCount = other.RepeatCount; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } for (int i = 0; i < other.TextData.Count; i++) { @@ -61,33 +64,14 @@ public class PngMetadata : IDeepCloneable public float Gamma { get; set; } /// - /// Gets or sets the Rgb24 transparent color. - /// This represents any color in an 8 bit Rgb24 encoded png that should be transparent. - /// - public Rgb24? TransparentRgb24 { get; set; } - - /// - /// Gets or sets the Rgb48 transparent color. - /// This represents any color in a 16 bit Rgb24 encoded png that should be transparent. + /// Gets or sets the color table, if any. /// - public Rgb48? TransparentRgb48 { get; set; } + public ReadOnlyMemory? ColorTable { get; set; } /// - /// Gets or sets the 8 bit grayscale transparent color. - /// This represents any color in an 8 bit grayscale encoded png that should be transparent. + /// Gets or sets the transparent color used with non palette based images, if a transparency chunk and markers were decoded. /// - public L8? TransparentL8 { get; set; } - - /// - /// Gets or sets the 16 bit grayscale transparent color. - /// This represents any color in a 16 bit grayscale encoded png that should be transparent. - /// - public L16? TransparentL16 { get; set; } - - /// - /// Gets or sets a value indicating whether the image contains a transparency chunk and markers were decoded. - /// - public bool HasTransparency { get; set; } + public Color? TransparentColor { get; set; } /// /// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks. @@ -95,6 +79,11 @@ public class PngMetadata : IDeepCloneable /// public IList TextData { get; set; } = new List(); + /// + /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. + /// + public int RepeatCount { get; set; } + /// public IDeepCloneable DeepClone() => new PngMetadata(this); } diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 04a23308cc..f217515e3c 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; @@ -15,96 +16,43 @@ namespace SixLabors.ImageSharp.Formats.Png; internal static class PngScanlineProcessor { public static void ProcessGrayscaleScanline( - in PngHeader header, + int bitDepth, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - bool hasTrans, - L16 luminance16Trans, - L8 luminanceTrans) - where TPixel : unmanaged, IPixel - { - TPixel pixel = default; - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1); - - 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(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(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; - } - } - } + Color? transparentColor) + where TPixel : unmanaged, IPixel => + ProcessInterlacedGrayscaleScanline( + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + transparentColor); public static void ProcessInterlacedGrayscaleScanline( - in PngHeader header, + int bitDepth, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, uint increment, - bool hasTrans, - L16 luminance16Trans, - L8 luminanceTrans) + Color? transparentColor) where TPixel : unmanaged, IPixel { + uint offset = pixelOffset + frameControl.XOffset; TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); 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; - 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)); pixel.FromL16(Unsafe.As(ref luminance)); @@ -113,7 +61,7 @@ internal static class PngScanlineProcessor } 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); pixel.FromL8(Unsafe.As(ref luminance)); @@ -124,15 +72,16 @@ internal static class PngScanlineProcessor return; } - if (header.BitDepth == 16) + if (bitDepth == 16) { + L16 transparent = transparentColor.Value.ToPixel(); La32 source = default; 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)); 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); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -140,13 +89,13 @@ internal static class PngScanlineProcessor } else { + byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor); La16 source = default; - byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); - 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); source.L = luminance; - source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue; + source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue; pixel.FromLa16(source); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -155,47 +104,26 @@ internal static class PngScanlineProcessor } public static void ProcessGrayscaleWithAlphaScanline( - in PngHeader header, + int bitDepth, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint bytesPerPixel, uint bytesPerSample) - where TPixel : unmanaged, IPixel - { - TPixel pixel = default; - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - - if (header.BitDepth == 16) - { - La32 source = default; - int o = 0; - 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; - } - } - } + where TPixel : unmanaged, IPixel => + ProcessInterlacedGrayscaleWithAlphaScanline( + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample); public static void ProcessInterlacedGrayscaleWithAlphaScanline( - in PngHeader header, + int bitDepth, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -204,15 +132,16 @@ internal static class PngScanlineProcessor uint bytesPerSample) where TPixel : unmanaged, IPixel { + uint offset = pixelOffset + frameControl.XOffset; TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (header.BitDepth == 16) + if (bitDepth == 16) { La32 source = default; 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.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); @@ -224,135 +153,108 @@ internal static class PngScanlineProcessor else { La16 source = default; - nuint offset = 0; - for (nuint x = pixelOffset; x < (uint)header.Width; x += increment) + nuint offset2 = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment) { - source.L = Unsafe.Add(ref scanlineSpanRef, offset); - source.A = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample); + source.L = Unsafe.Add(ref scanlineSpanRef, offset2); + source.A = Unsafe.Add(ref scanlineSpanRef, offset2 + bytesPerSample); pixel.FromLa16(source); Unsafe.Add(ref rowSpanRef, x) = pixel; - offset += bytesPerPixel; + offset2 += bytesPerPixel; } } } public static void ProcessPaletteScanline( - in PngHeader header, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - ReadOnlySpan palette, - byte[] paletteAlpha) - where TPixel : unmanaged, IPixel - { - if (palette.IsEmpty) - { - PngThrowHelper.ThrowMissingPalette(); - } - - TPixel pixel = default; - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - ReadOnlySpan palettePixels = MemoryMarshal.Cast(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; - } - } - } + ReadOnlyMemory? palette) + where TPixel : unmanaged, IPixel => + ProcessInterlacedPaletteScanline( + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + palette); public static void ProcessInterlacedPaletteScanline( - in PngHeader header, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, uint increment, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel { + if (palette is null) + { + PngThrowHelper.ThrowMissingPalette(); + } + TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - ReadOnlySpan palettePixels = MemoryMarshal.Cast(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 = 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); + ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span); - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else + for (nuint x = pixelOffset, o = 0; x < frameControl.XMax; x += increment, o++) { - for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) - { - int index = Unsafe.Add(ref scanlineSpanRef, o); - Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + uint index = Unsafe.Add(ref scanlineSpanRef, o); + pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32()); + Unsafe.Add(ref rowSpanRef, x) = pixel; } } public static void ProcessRgbScanline( Configuration configuration, - in PngHeader header, + int bitDepth, + in FrameControl frameControl, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int bytesPerPixel, + int bytesPerSample, + Color? transparentColor) + where TPixel : unmanaged, IPixel => + ProcessInterlacedRgbScanline( + configuration, + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample, + transparentColor); + + public static void ProcessInterlacedRgbScanline( + Configuration configuration, + int bitDepth, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, + uint pixelOffset, + uint increment, int bytesPerPixel, int bytesPerSample, - bool hasTrans, - Rgb48 rgb48Trans, - Rgb24 rgb24Trans) + Color? transparentColor) where TPixel : unmanaged, IPixel { + uint offset = pixelOffset + frameControl.XOffset; + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (!hasTrans) + if (transparentColor is null) { - if (header.BitDepth == 16) + if (bitDepth == 16) { Rgb48 rgb48 = default; 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.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -362,27 +264,47 @@ internal static class PngScanlineProcessor Unsafe.Add(ref rowSpanRef, x) = pixel; } } + else if (pixelOffset == 0 && increment == 1) + { + PixelOperations.Instance.FromRgb24Bytes( + configuration, + scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], + rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), + (int)frameControl.Width); + } else { - PixelOperations.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; } - if (header.BitDepth == 16) + if (bitDepth == 16) { + Rgb48 transparent = transparentColor.Value.ToPixel(); + Rgb48 rgb48 = default; Rgba64 rgba64 = default; 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.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; + rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue; pixel.FromRgba64(rgba64); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -390,142 +312,47 @@ internal static class PngScanlineProcessor } else { - Rgba32 rgba32 = default; - ReadOnlySpan rgb24Span = MemoryMarshal.Cast(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( - in PngHeader header, - ReadOnlySpan scanlineSpan, - Span rowSpan, - uint pixelOffset, - uint increment, - int bytesPerPixel, - int bytesPerSample, - bool hasTrans, - Rgb48 rgb48Trans, - Rgb24 rgb24Trans) - where TPixel : unmanaged, IPixel - { - 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; + Rgb24 transparent = transparentColor.Value.ToPixel(); - 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; 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.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + 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); 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( Configuration configuration, - in PngHeader header, + int bitDepth, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, int bytesPerSample) - where TPixel : unmanaged, IPixel - { - TPixel pixel = default; - ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - - if (header.BitDepth == 16) - { - Rgba64 rgba64 = default; - int o = 0; - for (nuint x = 0; x < (uint)header.Width; x++, o += bytesPerPixel) - { - rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); - rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); - rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); - rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); - - pixel.FromRgba64(rgba64); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - PixelOperations.Instance.FromRgba32Bytes(configuration, scanlineSpan, rowSpan, header.Width); - } - } + where TPixel : unmanaged, IPixel => + ProcessInterlacedRgbaScanline( + configuration, + bitDepth, + frameControl, + scanlineSpan, + rowSpan, + 0, + 1, + bytesPerPixel, + bytesPerSample); public static void ProcessInterlacedRgbaScanline( - in PngHeader header, + Configuration configuration, + int bitDepth, + in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, uint pixelOffset, @@ -534,15 +361,15 @@ internal static class PngScanlineProcessor int bytesPerSample) where TPixel : unmanaged, IPixel { + uint offset = pixelOffset + frameControl.XOffset; TPixel pixel = default; - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (header.BitDepth == 16) + if (bitDepth == 16) { Rgba64 rgba64 = default; 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.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); @@ -553,11 +380,20 @@ internal static class PngScanlineProcessor Unsafe.Add(ref rowSpanRef, x) = pixel; } } + else if (pixelOffset == 0 && increment == 1) + { + PixelOperations.Instance.FromRgba32Bytes( + configuration, + scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], + rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), + (int)frameControl.Width); + } else { + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); Rgba32 rgba = default; 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.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 67da78e45b..0552e9a79e 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Png; @@ -12,13 +13,22 @@ internal static class PngThrowHelper => throw new InvalidImageContentException(errorMessage, innerException); [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] - 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] - 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] 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!"); [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] public static void ThrowUnknownFilter() => throw new InvalidImageContentException("Unknown filter type."); diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs index b3769d45cb..b9c2078b3f 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs +++ b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; - namespace SixLabors.ImageSharp.Formats.Qoi; /// @@ -27,7 +25,7 @@ public class QoiEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - QoiEncoderCore encoder = new(this, image.GetMemoryAllocator(), image.GetConfiguration()); + QoiEncoderCore encoder = new(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs index 40b246faf2..53f67e765d 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs @@ -33,13 +33,12 @@ internal class QoiEncoderCore : IImageEncoderInternals /// Initializes a new instance of the class. /// /// The encoder with options. - /// The to use for buffer allocations. /// The configuration of the Encoder. - public QoiEncoderCore(QoiEncoder encoder, MemoryAllocator memoryAllocator, Configuration configuration) + public QoiEncoderCore(QoiEncoder encoder, Configuration configuration) { this.encoder = encoder; - this.memoryAllocator = memoryAllocator; this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; } /// diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index 71acf3ae83..09b12e6081 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -1,12 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; - namespace SixLabors.ImageSharp.Formats.Tga; /// -/// 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. /// public sealed class TgaEncoder : ImageEncoder { @@ -23,7 +21,7 @@ public sealed class TgaEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - TgaEncoderCore encoder = new(this, image.GetMemoryAllocator()); + TgaEncoderCore encoder = new(this, image.Configuration.MemoryAllocator); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index ad63bd356d..bbb476c017 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -112,7 +112,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals } else { - this.WriteImage(image.GetConfiguration(), stream, image.Frames.RootFrame); + this.WriteImage(image.Configuration, stream, image.Frames.RootFrame); } stream.Flush(); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs index a5ce4f8426..416472e830 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs @@ -32,7 +32,7 @@ internal class WebpTiffCompression : TiffBaseDecompressor /// protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) { - using WebpDecoderCore decoder = new(this.options); + using WebpDecoderCore decoder = new(new WebpDecoderOptions()); using Image image = decoder.Decode(stream, cancellationToken); CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index b9a1f31553..720e376b9d 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Tiff.Compression; diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs index 755e79e42e..086eef0585 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs @@ -40,7 +40,7 @@ internal class DirectoryReader public IList Read() { this.ByteOrder = ReadByteOrder(this.stream); - var headerReader = new HeaderReader(this.stream, this.ByteOrder); + HeaderReader headerReader = new(this.stream, this.ByteOrder); headerReader.ReadFileHeader(); this.nextIfdOffset = headerReader.FirstIfdOffset; @@ -52,7 +52,12 @@ internal class DirectoryReader private static ByteOrder ReadByteOrder(Stream stream) { Span headerBytes = stackalloc byte[2]; - stream.Read(headerBytes); + + if (stream.Read(headerBytes) != 2) + { + throw TiffThrowHelper.ThrowInvalidHeader(); + } + if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) { return ByteOrder.LittleEndian; @@ -68,10 +73,10 @@ internal class DirectoryReader private IList ReadIfds(bool isBigTiff) { - var readers = new List(); + List readers = new(); 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); 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; readers.Add(reader); @@ -94,11 +104,11 @@ internal class DirectoryReader } } - var list = new List(readers.Count); + List list = new(readers.Count); foreach (EntryReader reader in readers) { reader.ReadBigValues(); - var profile = new ExifProfile(reader.Values, reader.InvalidTags); + ExifProfile profile = new(reader.Values, reader.InvalidTags); list.Add(profile); } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index fb5b9f2ed7..ea64e82899 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Processing; @@ -48,7 +47,7 @@ public class TiffEncoder : QuantizingImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - TiffEncoderCore encode = new(this, image.GetMemoryAllocator()); + TiffEncoderCore encode = new(this, image.Configuration.MemoryAllocator); encode.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index d0634cf259..149f23f1bf 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -128,7 +128,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - this.configuration = image.GetConfiguration(); + this.configuration = image.Configuration; ImageFrameMetadata rootFrameMetaData = image.Frames.RootFrame.Metadata; TiffFrameMetadata rootFrameTiffMetaData = rootFrameMetaData.GetTiffMetadata(); diff --git a/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs b/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs new file mode 100644 index 0000000000..5be8f6a296 --- /dev/null +++ b/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; + +/// +/// Enum to decide how to handle the background color of the Animation chunk during decoding. +/// +public enum BackgroundColorHandling +{ + /// + /// 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. + /// + Standard = 0, + + /// + /// The background color of the ANIM chunk is ignored and instead the canvas is initialized with transparent, BGRA(0, 0, 0, 0). + /// + Ignore = 1 +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 1f7c7586eb..469e4c9ab0 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -502,7 +502,7 @@ internal class Vp8LEncoder : IDisposable doNotCache = true; // Go brute force on all transforms. - foreach (EntropyIx entropyIx in Enum.GetValues(typeof(EntropyIx)).Cast()) + foreach (EntropyIx entropyIx in Enum.GetValues()) { // We can only apply kPalette or kPaletteAndSpatial if we can indeed use a palette. if ((entropyIx != EntropyIx.Palette && entropyIx != EntropyIx.PaletteAndSpatial) || usePalette) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 21337ce6c8..90c9c70b26 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -52,17 +52,24 @@ internal class WebpAnimationDecoder : IDisposable /// private IMemoryOwner? alphaData; + /// + /// The flag to decide how to handle the background color in the Animation Chunk. + /// + private readonly BackgroundColorHandling backgroundColorHandling; + /// /// Initializes a new instance of the class. /// /// The memory allocator. /// The global configuration. /// The maximum number of frames to decode. Inclusive. - public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames) + /// The flag to decide how to handle the background color in the Animation Chunk. + public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames, BackgroundColorHandling backgroundColorHandling) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; this.maxFrames = maxFrames; + this.backgroundColorHandling = backgroundColorHandling; } /// @@ -94,7 +101,10 @@ internal class WebpAnimationDecoder : IDisposable switch (chunkType) { 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; break; case WebpChunkType.Xmp: diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs index daa5eaf4fe..e23b817ccd 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Image decoder for generating an image out of a webp stream. /// -public sealed class WebpDecoder : ImageDecoder +public sealed class WebpDecoder : SpecializedImageDecoder { private WebpDecoder() { @@ -25,25 +25,33 @@ public sealed class WebpDecoder : ImageDecoder Guard.NotNull(options, nameof(options)); 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); } /// - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(WebpDecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); using WebpDecoderCore decoder = new(options); - Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + Image image = decoder.Decode(options.GeneralOptions.Configuration, stream, cancellationToken); - ScaleToTargetSize(options, image); + ScaleToTargetSize(options.GeneralOptions, image); return image; } + /// + protected override Image Decode(WebpDecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + /// protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); + + /// + protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) + => new() { GeneralOptions = options }; } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 223e15a0e7..8832ac1068 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -48,16 +48,22 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable /// private WebpImageInfo? webImageInfo; + /// + /// The flag to decide how to handle the background color in the Animation Chunk. + /// + private BackgroundColorHandling backgroundColorHandling; + /// /// Initializes a new instance of the class. /// /// The decoder options. - public WebpDecoderCore(DecoderOptions options) + public WebpDecoderCore(WebpDecoderOptions options) { - this.Options = options; - this.configuration = options.Configuration; - this.skipMetadata = options.SkipMetadata; - this.maxFrames = options.MaxFrames; + this.Options = options.GeneralOptions; + this.backgroundColorHandling = options.BackgroundColorHandling; + this.configuration = options.GeneralOptions.Configuration; + this.skipMetadata = options.GeneralOptions.SkipMetadata; + this.maxFrames = options.GeneralOptions.MaxFrames; this.memoryAllocator = this.configuration.MemoryAllocator; } @@ -83,7 +89,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { 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(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs new file mode 100644 index 0000000000..6fb15acbb4 --- /dev/null +++ b/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; + +/// +/// Configuration options for decoding webp images. +/// +public sealed class WebpDecoderOptions : ISpecializedDecoderOptions +{ + /// + public DecoderOptions GeneralOptions { get; init; } = new(); + + /// + /// 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. + /// + /// + public BackgroundColorHandling BackgroundColorHandling { get; init; } = BackgroundColorHandling.Standard; +} diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index bd8303f1c8..29d0c9e3b0 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -82,7 +82,7 @@ public sealed class WebpEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - WebpEncoderCore encoder = new(this, image.GetConfiguration()); + WebpEncoderCore encoder = new(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index efa8f6f4be..1aa53d65e1 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -68,6 +68,11 @@ internal sealed class BufferedReadStream : Stream this.readBufferIndex = int.MinValue; } + /// + /// Gets the number indicating the EOF hits occured while reading from this instance. + /// + public int EofHitCount { get; private set; } + /// /// Gets the size, in bytes, of the underlying buffer. /// @@ -142,6 +147,7 @@ internal sealed class BufferedReadStream : Stream { if (this.readerPosition >= this.Length) { + this.EofHitCount++; return -1; } @@ -294,7 +300,7 @@ internal sealed class BufferedReadStream : Stream this.readerPosition += n; this.readBufferIndex += n; - + this.CheckEof(n); return n; } @@ -352,6 +358,7 @@ internal sealed class BufferedReadStream : Stream this.Position += n; + this.CheckEof(n); return n; } @@ -418,4 +425,13 @@ internal sealed class BufferedReadStream : Stream Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckEof(int read) + { + if (read == 0) + { + this.EofHitCount++; + } + } } diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index cba32cb782..03a19a4be8 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -17,7 +17,6 @@ namespace SixLabors.ImageSharp; public abstract partial class Image : IDisposable, IConfigurationProvider { private bool isDisposed; - private readonly Configuration configuration; /// /// Initializes a new instance of the class. @@ -26,12 +25,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// The pixel type information. /// The image metadata. /// The size in px units. - 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.Size = size; - this.Metadata = metadata ?? new ImageMetadata(); + this.Metadata = metadata; } /// @@ -45,7 +44,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider internal Image( Configuration configuration, PixelTypeInfo pixelType, - ImageMetadata? metadata, + ImageMetadata metadata, int width, int height) : this(configuration, pixelType, metadata, new Size(width, height)) @@ -53,7 +52,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider } /// - Configuration IConfigurationProvider.Configuration => this.configuration; + public Configuration Configuration { get; } /// /// Gets information about the image pixels. @@ -147,7 +146,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// The pixel format. /// The public Image CloneAs() - where TPixel2 : unmanaged, IPixel => this.CloneAs(this.GetConfiguration()); + where TPixel2 : unmanaged, IPixel => this.CloneAs(this.Configuration); /// /// Returns a copy of the image in the given pixel format. diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index 75e4f13257..6c769a9d17 100644 --- a/src/ImageSharp/ImageExtensions.cs +++ b/src/ImageSharp/ImageExtensions.cs @@ -47,7 +47,7 @@ public static partial class ImageExtensions { Guard.NotNull(path, nameof(path)); 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); } @@ -70,7 +70,7 @@ public static partial class ImageExtensions Guard.NotNull(path, nameof(path)); 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); } @@ -94,14 +94,14 @@ public static partial class ImageExtensions 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) { StringBuilder sb = new(); sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); - foreach (KeyValuePair val in source.GetConfiguration().ImageFormatsManager.ImageEncoders) + foreach (KeyValuePair val in source.Configuration.ImageFormatsManager.ImageEncoders) { 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."); } - IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.GetEncoder(format); + IImageEncoder encoder = source.Configuration.ImageFormatsManager.GetEncoder(format); if (encoder is null) { StringBuilder sb = new(); sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); - foreach (KeyValuePair val in source.GetConfiguration().ImageFormatsManager.ImageEncoders) + foreach (KeyValuePair val in source.Configuration.ImageFormatsManager.ImageEncoders) { sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); } diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index 1e5d40385c..2558e1a13a 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -15,8 +15,6 @@ namespace SixLabors.ImageSharp; /// public abstract partial class ImageFrame : IConfigurationProvider, IDisposable { - private readonly Configuration configuration; - /// /// Initializes a new instance of the class. /// @@ -26,10 +24,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// The . protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata) { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(metadata, nameof(metadata)); - - this.configuration = configuration ?? Configuration.Default; + this.Configuration = configuration; this.Width = width; this.Height = height; this.Metadata = metadata; @@ -51,19 +46,19 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable public ImageFrameMetadata Metadata { get; } /// - Configuration IConfigurationProvider.Configuration => this.configuration; + public Configuration Configuration { get; } /// /// Gets the size of the frame. /// /// The - public Size Size() => new Size(this.Width, this.Height); + public Size Size() => new(this.Width, this.Height); /// /// Gets the bounds of the frame. /// /// The - public Rectangle Bounds() => new Rectangle(0, 0, this.Width, this.Height); + public Rectangle Bounds() => new(0, 0, this.Width, this.Height); /// public void Dispose() @@ -84,6 +79,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// /// Updates the size of the image frame. /// + /// The size. internal void UpdateSize(Size size) { this.Width = size.Width; diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs index faa83b59e2..e927fb0fac 100644 --- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs +++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Collections; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -24,7 +23,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); // Frames are already cloned within the caller - this.frames.Add(new ImageFrame(parent.GetConfiguration(), width, height, backgroundColor)); + this.frames.Add(new ImageFrame(parent.Configuration, width, height, backgroundColor)); } internal ImageFrameCollection(Image parent, int width, int height, MemoryGroup memorySource) @@ -32,7 +31,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); // Frames are already cloned within the caller - this.frames.Add(new ImageFrame(parent.GetConfiguration(), width, height, memorySource)); + this.frames.Add(new ImageFrame(parent.Configuration, width, height, memorySource)); } internal ImageFrameCollection(Image parent, IEnumerable> frames) @@ -138,7 +137,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.EnsureNotDisposed(); this.ValidateFrame(source); - ImageFrame clonedFrame = source.Clone(this.parent.GetConfiguration()); + ImageFrame clonedFrame = source.Clone(this.parent.Configuration); this.frames.Insert(index, clonedFrame); return clonedFrame; } @@ -153,7 +152,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.EnsureNotDisposed(); this.ValidateFrame(source); - ImageFrame clonedFrame = source.Clone(this.parent.GetConfiguration()); + ImageFrame clonedFrame = source.Clone(this.parent.Configuration); this.frames.Add(clonedFrame); return clonedFrame; } @@ -169,7 +168,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.EnsureNotDisposed(); ImageFrame frame = ImageFrame.LoadPixelData( - this.parent.GetConfiguration(), + this.parent.Configuration, source, this.RootFrame.Width, this.RootFrame.Height); @@ -270,7 +269,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.frames.Remove(frame); - return new Image(this.parent.GetConfiguration(), this.parent.Metadata.DeepClone(), new[] { frame }); + return new Image(this.parent.Configuration, this.parent.Metadata.DeepClone(), new[] { frame }); } /// @@ -285,7 +284,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer ImageFrame frame = this[index]; ImageFrame clonedFrame = frame.Clone(); - return new Image(this.parent.GetConfiguration(), this.parent.Metadata.DeepClone(), new[] { clonedFrame }); + return new Image(this.parent.Configuration, this.parent.Metadata.DeepClone(), new[] { clonedFrame }); } /// @@ -299,7 +298,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.EnsureNotDisposed(); ImageFrame frame = new( - this.parent.GetConfiguration(), + this.parent.Configuration, this.RootFrame.Width, this.RootFrame.Height); this.frames.Add(frame); @@ -365,7 +364,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer public ImageFrame CreateFrame(TPixel backgroundColor) { ImageFrame frame = new( - this.parent.GetConfiguration(), + this.parent.Configuration, this.RootFrame.Width, this.RootFrame.Height, backgroundColor); @@ -414,7 +413,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer private ImageFrame CopyNonCompatibleFrame(ImageFrame source) { ImageFrame result = new( - this.parent.GetConfiguration(), + this.parent.Configuration, source.Size(), source.Metadata.DeepClone()); source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup); diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 0e7eef11e9..0b6354d05d 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -66,7 +66,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D( + this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D( width, height, configuration.PreferContiguousImageBuffers, @@ -99,7 +99,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D( + this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D( width, height, configuration.PreferContiguousImageBuffers); @@ -146,7 +146,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(source, nameof(source)); - this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D( + this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D( source.PixelBuffer.Width, source.PixelBuffer.Height, configuration.PreferContiguousImageBuffers); @@ -371,7 +371,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource } this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) - => PixelOperations.Instance.To(this.GetConfiguration(), s, d)); + => PixelOperations.Instance.To(this.Configuration, s, d)); } /// @@ -381,7 +381,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// Clones the current instance. /// /// The - internal ImageFrame Clone() => this.Clone(this.GetConfiguration()); + internal ImageFrame Clone() => this.Clone(this.Configuration); /// /// Clones the current instance. @@ -396,7 +396,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// The pixel format. /// The internal ImageFrame? CloneAs() - where TPixel2 : unmanaged, IPixel => this.CloneAs(this.GetConfiguration()); + where TPixel2 : unmanaged, IPixel => this.CloneAs(this.Configuration); /// /// Returns a copy of the image frame in the given pixel format. diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 75d4b173c8..b08c27c41b 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -22,8 +22,8 @@ - - 3.0 + + 3.1 diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 69654329c4..c24014e698 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -78,12 +78,12 @@ public sealed class Image : Image /// The height of the image in pixels. /// The images metadata. internal Image(Configuration configuration, int width, int height, ImageMetadata? metadata) - : base(configuration, PixelTypeInfo.Create(), metadata, width, height) + : base(configuration, PixelTypeInfo.Create(), metadata ?? new(), width, height) => this.frames = new ImageFrameCollection(this, width, height, default(TPixel)); /// /// Initializes a new instance of the class - /// wrapping an external pixel bufferx. + /// wrapping an external pixel buffer. /// /// The configuration providing initialization code which allows extending the library. /// Pixel buffer. @@ -129,7 +129,7 @@ public sealed class Image : Image int height, TPixel backgroundColor, ImageMetadata? metadata) - : base(configuration, PixelTypeInfo.Create(), metadata, width, height) + : base(configuration, PixelTypeInfo.Create(), metadata ?? new(), width, height) => this.frames = new ImageFrameCollection(this, width, height, backgroundColor); /// @@ -328,7 +328,7 @@ public sealed class Image : Image /// Clones the current image /// /// Returns a new image with all the same metadata as the original. - public Image Clone() => this.Clone(this.GetConfiguration()); + public Image Clone() => this.Clone(this.Configuration); /// /// Clones the current image with the given configuration. diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs index 798edf9b22..0bae193632 100644 --- a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs @@ -117,6 +117,11 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato AllocationOptions options = AllocationOptions.None) { long totalLengthInBytes = totalLength * Unsafe.SizeOf(); + if (totalLengthInBytes < 0) + { + throw new InvalidMemoryOperationException("Attempted to allocate a MemoryGroup of a size that is not representable."); + } + if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes) { var buffer = new SharedArrayPoolBuffer((int)totalLength); diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index f4b2dfc08c..39c6e62e15 100644 --- a/src/ImageSharp/Memory/Buffer2D{T}.cs +++ b/src/ImageSharp/Memory/Buffer2D{T}.cs @@ -9,9 +9,6 @@ namespace SixLabors.ImageSharp.Memory; /// Represents a buffer of value type objects /// interpreted as a 2D region of x elements. /// -/// -/// Before RC1, this class might be target of API changes, use it on your own risk! -/// /// The value type. public sealed class Buffer2D : IDisposable where T : struct diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs index 741df50f2d..4767ca852e 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs @@ -56,11 +56,6 @@ public abstract partial class ExifTag /// public static ExifTag IntergraphRegisters { get; } = new ExifTag(ExifTagValue.IntergraphRegisters); - /// - /// Gets the TimeZoneOffset exif tag. - /// - public static ExifTag TimeZoneOffset { get; } = new ExifTag(ExifTagValue.TimeZoneOffset); - /// /// Gets the offset to child IFDs exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs index 96603b182a..e4fe13fe57 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs @@ -165,4 +165,9 @@ public abstract partial class ExifTag /// Gets the GPSDestDistance exif tag. /// public static ExifTag GPSDestDistance { get; } = new ExifTag(ExifTagValue.GPSDestDistance); + + /// + /// Gets the GPSHPositioningError exif tag. + /// + public static ExifTag GPSHPositioningError { get; } = new ExifTag(ExifTagValue.GPSHPositioningError); } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs new file mode 100644 index 0000000000..d6a9205143 --- /dev/null +++ b/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; + +/// +public abstract partial class ExifTag +{ + /// + /// Gets the TimeZoneOffset exif tag. + /// + public static ExifTag TimeZoneOffset { get; } = new ExifTag(ExifTagValue.TimeZoneOffset); +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs index 3788a1296f..56e8a3ffd1 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs @@ -1691,6 +1691,11 @@ internal enum ExifTagValue /// GPSDifferential = 0x001E, + /// + /// GPSHPositioningError + /// + GPSHPositioningError = 0x001F, + /// /// Used in the Oce scanning process. /// Identifies the scanticket used in the scanning process. diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs index 8023fb8bca..206417f667 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs @@ -5,6 +5,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif; internal sealed class ExifSignedShortArray : ExifArrayValue { + public ExifSignedShortArray(ExifTag tag) + : base(tag) + { + } + public ExifSignedShortArray(ExifTagValue tag) : base(tag) { diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index 8aa54c3a40..93f67d46ad 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -144,8 +144,6 @@ internal static partial class ExifValues return new ExifLongArray(ExifTag.StripRowCounts); case ExifTagValue.IntergraphRegisters: return new ExifLongArray(ExifTag.IntergraphRegisters); - case ExifTagValue.TimeZoneOffset: - return new ExifLongArray(ExifTag.TimeZoneOffset); case ExifTagValue.SubIFDs: return new ExifLongArray(ExifTag.SubIFDs); @@ -243,6 +241,8 @@ internal static partial class ExifValues return new ExifRational(ExifTag.GPSDestBearing); case ExifTagValue.GPSDestDistance: return new ExifRational(ExifTag.GPSDestDistance); + case ExifTagValue.GPSHPositioningError: + return new ExifRational(ExifTag.GPSHPositioningError); case ExifTagValue.WhitePoint: return new ExifRationalArray(ExifTag.WhitePoint); @@ -417,6 +417,9 @@ internal static partial class ExifValues case ExifTagValue.Decode: return new ExifSignedRationalArray(ExifTag.Decode); + case ExifTagValue.TimeZoneOffset: + return new ExifSignedShortArray(ExifTag.TimeZoneOffset); + case ExifTagValue.ImageDescription: return new ExifString(ExifTag.ImageDescription); case ExifTagValue.Make: diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs index 074712d302..7898ca7a9f 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs @@ -87,15 +87,21 @@ internal sealed class IccReader foreach (IccTagTableEntry tag in tagTable) { IccTagDataEntry entry; - - try + if (store.TryGetValue(tag.Offset, out IccTagDataEntry? value)) { - entry = reader.ReadTagDataEntry(tag); + entry = value; } - catch + else { - // Ignore tags that could not be read - continue; + try + { + entry = reader.ReadTagDataEntry(tag); + } + catch + { + // Ignore tags that could not be read + continue; + } } entry.TagSignature = tag.Signature; diff --git a/src/ImageSharp/Primitives/Rational.cs b/src/ImageSharp/Primitives/Rational.cs index 59f34331a7..201219f7e0 100644 --- a/src/ImageSharp/Primitives/Rational.cs +++ b/src/ImageSharp/Primitives/Rational.cs @@ -70,7 +70,7 @@ public readonly struct Rational : IEquatable /// Whether to use the best possible precision when parsing the value. 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.Denominator = (uint)rational.Denominator; @@ -109,7 +109,7 @@ public readonly struct Rational : IEquatable /// /// The . /// - public static Rational FromDouble(double value) => new Rational(value, false); + public static Rational FromDouble(double value) => new(value, false); /// /// Converts the specified to an instance of this type. @@ -119,24 +119,19 @@ public readonly struct Rational : IEquatable /// /// The . /// - public static Rational FromDouble(double value, bool bestPrecision) => new Rational(value, bestPrecision); + public static Rational FromDouble(double value, bool bestPrecision) => new(value, bestPrecision); /// public override bool Equals(object? obj) => obj is Rational other && this.Equals(other); /// public bool Equals(Rational other) - { - var left = new LongRational(this.Numerator, this.Denominator); - var right = new LongRational(other.Numerator, other.Denominator); - - return left.Equals(right); - } + => this.Numerator == other.Numerator && this.Denominator == other.Denominator; /// public override int GetHashCode() { - var self = new LongRational(this.Numerator, this.Denominator); + LongRational self = new(this.Numerator, this.Denominator); return self.GetHashCode(); } @@ -169,7 +164,7 @@ public readonly struct Rational : IEquatable /// The public string ToString(IFormatProvider provider) { - var rational = new LongRational(this.Numerator, this.Denominator); + LongRational rational = new(this.Numerator, this.Denominator); return rational.ToString(provider); } } diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs index fdf0967c53..713d4d5b77 100644 --- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs +++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs @@ -54,7 +54,7 @@ public static partial class ProcessingExtensions public static Buffer2D CalculateIntegralImage(this ImageFrame source, Rectangle bounds) where TPixel : unmanaged, IPixel { - Configuration configuration = source.GetConfiguration(); + Configuration configuration = source.Configuration; var interest = Rectangle.Intersect(bounds, source.Bounds()); int startY = interest.Y; diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs index f830ddfd09..784258aa51 100644 --- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs @@ -22,7 +22,7 @@ public static partial class ProcessingExtensions /// The source has been disposed. /// The processing operation failed. public static void Mutate(this Image source, Action operation) - => Mutate(source, source.GetConfiguration(), operation); + => Mutate(source, source.Configuration, operation); /// /// Mutates the source image by applying the image operation to it. @@ -57,7 +57,7 @@ public static partial class ProcessingExtensions /// The processing operation failed. public static void Mutate(this Image source, Action operation) where TPixel : unmanaged, IPixel - => Mutate(source, source.GetConfiguration(), operation); + => Mutate(source, source.Configuration, operation); /// /// Mutates the source image by applying the image operation to it. @@ -97,7 +97,7 @@ public static partial class ProcessingExtensions /// The processing operation failed. public static void Mutate(this Image source, params IImageProcessor[] operations) where TPixel : unmanaged, IPixel - => Mutate(source, source.GetConfiguration(), operations); + => Mutate(source, source.Configuration, operations); /// /// Mutates the source image by applying the operations to it. @@ -135,7 +135,7 @@ public static partial class ProcessingExtensions /// The source has been disposed. /// The processing operation failed. public static Image Clone(this Image source, Action operation) - => Clone(source, source.GetConfiguration(), operation); + => Clone(source, source.Configuration, 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 /// The new . public static Image Clone(this Image source, Action operation) where TPixel : unmanaged, IPixel - => Clone(source, source.GetConfiguration(), operation); + => Clone(source, source.Configuration, 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 /// The new public static Image Clone(this Image source, params IImageProcessor[] operations) where TPixel : unmanaged, IPixel - => Clone(source, source.GetConfiguration(), operations); + => Clone(source, source.Configuration, operations); /// /// Creates a deep clone of the current image. The clone is then mutated by the given operations. diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs index a231d6dee7..524153804c 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -111,7 +111,7 @@ internal struct WuQuantizer : IQuantizer public QuantizerOptions Options { get; } /// - public ReadOnlyMemory Palette + public readonly ReadOnlyMemory Palette { get { @@ -362,7 +362,7 @@ internal struct WuQuantizer : IQuantizer /// /// The source data. /// The bounds within the source image to quantize. - private void Build3DHistogram(Buffer2D source, Rectangle bounds) + private readonly void Build3DHistogram(Buffer2D source, Rectangle bounds) { Span momentSpan = this.momentsOwner.GetSpan(); @@ -393,7 +393,7 @@ internal struct WuQuantizer : IQuantizer /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// /// The memory allocator used for allocating buffers. - private void Get3DMoments(MemoryAllocator allocator) + private readonly void Get3DMoments(MemoryAllocator allocator) { using IMemoryOwner volume = allocator.Allocate(IndexCount * IndexAlphaCount); using IMemoryOwner area = allocator.Allocate(IndexAlphaCount); @@ -462,7 +462,7 @@ internal struct WuQuantizer : IQuantizer /// /// The cube. /// The . - private double Variance(ref Box cube) + private readonly double Variance(ref Box cube) { ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); @@ -503,7 +503,7 @@ internal struct WuQuantizer : IQuantizer /// The cutting point. /// The whole moment. /// The . - 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 momentSpan = this.momentsOwner.GetSpan(); Moment bottom = Bottom(ref cube, direction, momentSpan); @@ -634,7 +634,7 @@ internal struct WuQuantizer : IQuantizer /// /// The cube. /// A label. - private void Mark(ref Box cube, byte label) + private readonly void Mark(ref Box cube, byte label) { Span tagSpan = this.tagsOwner.GetSpan(); diff --git a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs index 9afc852b58..2eda61e41d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs @@ -38,7 +38,7 @@ internal class EntropyCropProcessor : ImageProcessor // All frames have be the same size so we only need to calculate the correct dimensions for the first frame using (Image 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. new EdgeDetector2DProcessor(KnownEdgeDetectorKernels.Sobel, false).Execute(this.Configuration, temp, this.SourceRectangle); diff --git a/tests/ImageSharp.Tests/Color/ColorTests.cs b/tests/ImageSharp.Tests/Color/ColorTests.cs index 85e6eba78c..f7e2092176 100644 --- a/tests/ImageSharp.Tests/Color/ColorTests.cs +++ b/tests/ImageSharp.Tests/Color/ColorTests.cs @@ -18,25 +18,42 @@ public partial class ColorTests Assert.Equal(expected, (Rgba32)c2); } - [Fact] - public void Equality_WhenTrue() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Equality_WhenTrue(bool highPrecision) { Color c1 = new Rgba64(100, 2000, 3000, 40000); Color c2 = new Rgba64(100, 2000, 3000, 40000); + if (highPrecision) + { + c1 = Color.FromPixel(c1.ToPixel()); + c2 = Color.FromPixel(c2.ToPixel()); + } + Assert.True(c1.Equals(c2)); Assert.True(c1 == c2); Assert.False(c1 != c2); Assert.True(c1.GetHashCode() == c2.GetHashCode()); } - [Fact] - public void Equality_WhenFalse() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Equality_WhenFalse(bool highPrecision) { Color c1 = new Rgba64(100, 2000, 3000, 40000); Color c2 = new Rgba64(101, 2000, 3000, 40000); Color c3 = new Rgba64(100, 2000, 3000, 40001); + if (highPrecision) + { + c1 = Color.FromPixel(c1.ToPixel()); + c2 = Color.FromPixel(c2.ToPixel()); + c3 = Color.FromPixel(c3.ToPixel()); + } + Assert.False(c1.Equals(c2)); Assert.False(c2.Equals(c3)); Assert.False(c3.Equals(c1)); @@ -47,13 +64,20 @@ public partial class ColorTests Assert.False(c1.Equals(null)); } - [Fact] - public void ToHex() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ToHex(bool highPrecision) { string expected = "ABCD1234"; - var color = Color.ParseHex(expected); - string actual = color.ToHex(); + Color color = Color.ParseHex(expected); + if (highPrecision) + { + color = Color.FromPixel(color.ToPixel()); + } + + string actual = color.ToHex(); Assert.Equal(expected, actual); } diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs index 1b57663f3a..11dd1cd58c 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs @@ -1,9 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Pbm; @@ -120,4 +122,23 @@ public class PbmDecoderTests testOutputDetails: details, 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); + } } diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs index c40ec7318a..a69d9d9ba7 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs @@ -83,12 +83,9 @@ public class PbmMetadataTests } [Fact] - public void Identify_HandlesCraftedDenialOfServiceString() + public void Identify_EofInHeader_ThrowsInvalidImageContentException() { byte[] bytes = Convert.FromBase64String("UDEjWAAACQAAAAA="); - ImageInfo info = Image.Identify(bytes); - Assert.Equal(default, info.Size); - Configuration.Default.ImageFormatsManager.TryFindFormatByFileExtension("pbm", out IImageFormat format); - Assert.Equal(format!, info.Metadata.DecodedImageFormat); + Assert.Throws(() => Image.Identify(bytes)); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 2dfd99439a..2e11093db6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -7,7 +7,6 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; @@ -79,6 +78,18 @@ public partial class PngDecoderTests { TestImages.Png.Rgba64Bpp, typeof(Image) }, }; + 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] [MemberData(nameof(PixelFormatRange))] public void Decode_NonGeneric_CreatesCorrectImageType(string path, Type type) @@ -107,6 +118,19 @@ public partial class PngDecoderTests image.CompareToOriginal(provider, ImageComparer.Exact); } + [Theory] + [WithFileCollection(nameof(MultiFrameTestFiles), PixelTypes.Rgba32)] + public void Decode_VerifyAllFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image 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] [WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)] public void PngDecoder_Decode_Resize(TestImageProvider provider) @@ -539,7 +563,8 @@ public partial class PngDecoderTests { using Image image = provider.GetImage(PngDecoder.Instance); 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 @@ -551,7 +576,8 @@ public partial class PngDecoderTests using MemoryStream stream = new(testFile.Bytes, false); ImageInfo imageInfo = Image.Identify(stream); 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 diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index b20ec0675a..92c07a27a6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image 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 output = Image.Load(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] [MemberData(nameof(PngTrnsFiles))] public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType) @@ -449,44 +483,17 @@ public partial class PngEncoderTests TestFile testFile = TestFile.Create(imagePath); using Image input = testFile.CreateRgba32Image(); PngMetadata inMeta = input.Metadata.GetPngMetadata(); - Assert.True(inMeta.HasTransparency); + Assert.True(inMeta.TransparentColor.HasValue); using MemoryStream memStream = new(); input.Save(memStream, PngEncoder); memStream.Position = 0; using Image output = Image.Load(memStream); PngMetadata outMeta = output.Metadata.GetPngMetadata(); - Assert.True(outMeta.HasTransparency); - - switch (pngColorType) - { - 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; - } + Assert.True(outMeta.TransparentColor.HasValue); + Assert.Equal(inMeta.TransparentColor, outMeta.TransparentColor); + Assert.Equal(pngBitDepth, outMeta.BitDepth); + Assert.Equal(pngColorType, outMeta.ColorType); } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs new file mode 100644 index 0000000000..e29585c2dc --- /dev/null +++ b/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)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index d7a353665a..b3c122a7a8 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -30,15 +31,25 @@ public class PngMetadataTests ColorType = PngColorType.GrayscaleWithAlpha, InterlaceMethod = PngInterlaceMode.Adam7, Gamma = 2, - TextData = new List { new PngTextData("name", "value", "foo", "bar") } + TextData = new List { new PngTextData("name", "value", "foo", "bar") }, + RepeatCount = 123 }; 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.ColorType = PngColorType.Palette; clone.InterlaceMethod = PngInterlaceMode.None; clone.Gamma = 1; + clone.RepeatCount = 321; Assert.False(meta.BitDepth == clone.BitDepth); Assert.False(meta.ColorType == clone.ColorType); @@ -46,6 +57,7 @@ public class PngMetadataTests Assert.False(meta.Gamma.Equals(clone.Gamma)); Assert.False(meta.TextData.Equals(clone.TextData)); Assert.True(meta.TextData.SequenceEqual(clone.TextData)); + Assert.False(meta.RepeatCount == clone.RepeatCount); } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs index 04341a2419..878f3fb8d4 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Chunks; namespace SixLabors.ImageSharp.Tests.Formats.Png; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 2c8268d413..decc0069a5 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. // ReSharper disable InconsistentNaming -using System.Runtime.InteropServices; using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Tiff; @@ -666,6 +665,33 @@ public class TiffDecoderTests : TiffDecoderBaseTester public void TiffDecoder_CanDecode_TiledWithNonEqualWidthAndHeight(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(JpegCompressedGray0000539558, PixelTypes.Rgba32)] + public void TiffDecoder_ThrowsException_WithCircular_IFD_Offsets(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => Assert.Throws( + () => + { + using (provider.GetImage(TiffDecoder.Instance)) + { + } + }); + + [Theory] + [WithFile(Tiled0000023664, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_TiledWithBadZlib(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + + // ImageMagick cannot decode this image. + image.DebugSave(provider); + image.CompareToReferenceOutput( + ImageComparer.Exact, + provider, + appendPixelTypeToFileName: false); + } + [Theory] [WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)] public void DecodeMultiframe(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index f95b003d0a..c0fc00b82d 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.InteropServices; using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Webp; @@ -340,6 +339,24 @@ public class WebpDecoderTests Assert.Equal(1, image.Frames.Count); } + [Theory] + [WithFile(Lossy.AnimatedIssue2528, PixelTypes.Rgba32)] + public void Decode_AnimatedLossy_IgnoreBackgroundColor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + WebpDecoderOptions options = new() + { + BackgroundColorHandling = BackgroundColorHandling.Ignore, + GeneralOptions = new DecoderOptions() + { + MaxFrames = 1 + } + }; + using Image image = provider.GetImage(WebpDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + [Theory] [WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)] [WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs index 258ee5b9f7..9b03a447a9 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs @@ -42,7 +42,7 @@ public class YuvConversionTests { // arrange using Image image = provider.GetImage(); - Configuration config = image.GetConfiguration(); + Configuration config = image.Configuration; MemoryAllocator memoryAllocator = config.MemoryAllocator; int pixels = image.Width * image.Height; int uvWidth = (image.Width + 1) >> 1; @@ -158,7 +158,7 @@ public class YuvConversionTests { // arrange using Image image = provider.GetImage(); - Configuration config = image.GetConfiguration(); + Configuration config = image.Configuration; MemoryAllocator memoryAllocator = config.MemoryAllocator; int pixels = image.Width * image.Height; int uvWidth = (image.Width + 1) >> 1; diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs index 9cfd393906..f9ff6ba69e 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs @@ -20,7 +20,7 @@ public abstract partial class ImageFrameCollectionTests public void AddFrame_OfDifferentPixelType() { using (Image sourceImage = new( - this.Image.GetConfiguration(), + this.Image.Configuration, this.Image.Width, this.Image.Height, Color.Blue)) @@ -41,7 +41,7 @@ public abstract partial class ImageFrameCollectionTests public void InsertFrame_OfDifferentPixelType() { using (Image sourceImage = new( - this.Image.GetConfiguration(), + this.Image.Configuration, this.Image.Width, this.Image.Height, Color.Blue)) @@ -278,8 +278,8 @@ public abstract partial class ImageFrameCollectionTests where TPixel : unmanaged, IPixel { using Image source = provider.GetImage(); - using Image dest = new(source.GetConfiguration(), source.Width, source.Height); - + using Image dest = new(source.Configuration, source.Width, source.Height); + // Giphy.gif has 5 frames ImportFrameAs(source.Frames, dest.Frames, 0); ImportFrameAs(source.Frames, dest.Frames, 1); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs index 87794f3357..5fc58a752b 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs @@ -70,7 +70,7 @@ public partial class ImageTests { using Image image = new(5, 5); 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); using MemoryStream stream = new(); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 3239c57b1a..9aaefa41ef 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -136,7 +136,7 @@ public partial class ImageTests ref Rgba32 pixel0 = ref imageMem.Span[0]; Assert.True(Unsafe.AreSame(ref array[0], ref pixel0)); - Assert.Equal(cfg, image.GetConfiguration()); + Assert.Equal(cfg, image.Configuration); Assert.Equal(metaData, image.Metadata); } } @@ -239,7 +239,7 @@ public partial class ImageTests ref Rgba32 pixel0 = ref imageMem.Span[0]; Assert.True(Unsafe.AreSame(ref Unsafe.As(ref array[0]), ref pixel0)); - Assert.Equal(cfg, image.GetConfiguration()); + Assert.Equal(cfg, image.Configuration); Assert.Equal(metaData, image.Metadata); } } @@ -336,7 +336,7 @@ public partial class ImageTests ref Rgba32 pixel_1 = ref imageSpan[imageSpan.Length - 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); } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.cs b/tests/ImageSharp.Tests/Image/ImageTests.cs index eefa81835e..ca51f7f5cb 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Memory; @@ -31,10 +32,14 @@ public partial class ImageTests Assert.Equal(11 * 23, imageMem.Length); 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(() => new Image(int.MaxValue, int.MaxValue)); + [Fact] public void Configuration_Width_Height() { @@ -48,7 +53,7 @@ public partial class ImageTests Assert.Equal(11 * 23, imageMem.Length); 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); 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(22, image.Height); - Assert.Same(configuration, image.GetConfiguration()); + Assert.Same(configuration, image.Configuration); Assert.Same(metadata, image.Metadata); Assert.Equal(dirtyValue, image[5, 5].PackedValue); diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs index 3403e3f17e..7e7c136635 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs +++ b/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(() => allocator.AllocateGroup(int.MaxValue * (long)int.MaxValue, int.MaxValue)); + } + [Fact] public unsafe void Allocate_MemoryIsPinnableMultipleTimes() { diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs index 1adb7bd556..99cafa8960 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs @@ -70,8 +70,7 @@ public class ExifValuesTests { ExifTag.JPEGDCTables }, { ExifTag.JPEGACTables }, { ExifTag.StripRowCounts }, - { ExifTag.IntergraphRegisters }, - { ExifTag.TimeZoneOffset } + { ExifTag.IntergraphRegisters } }; public static TheoryData NumberTags => new TheoryData @@ -129,6 +128,7 @@ public class ExifValuesTests { ExifTag.GPSImgDirection }, { ExifTag.GPSDestBearing }, { ExifTag.GPSDestDistance }, + { ExifTag.GPSHPositioningError }, }; public static TheoryData RationalArrayTags => new TheoryData @@ -235,6 +235,11 @@ public class ExifValuesTests { ExifTag.Decode } }; + public static TheoryData SignedShortArrayTags => new TheoryData + { + { ExifTag.TimeZoneOffset } + }; + public static TheoryData StringTags => new TheoryData { { ExifTag.ImageDescription }, @@ -559,6 +564,21 @@ public class ExifValuesTests 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] [MemberData(nameof(StringTags))] public void ExifStringTests(ExifTag tag) diff --git a/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs b/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs index 98b5c8e980..403865e662 100644 --- a/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs +++ b/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs @@ -22,7 +22,7 @@ public abstract class BaseImageOperationsExtensionTest : IDisposable this.options = new GraphicsOptions { Antialias = false }; this.source = new Image(91 + 324, 123 + 56); this.rect = new Rectangle(91, 123, 324, 56); // make this random? - this.internalOperations = new FakeImageOperationsProvider.FakeImageOperations(this.source.GetConfiguration(), this.source, false); + this.internalOperations = new FakeImageOperationsProvider.FakeImageOperations(this.source.Configuration, this.source, false); this.internalOperations.SetGraphicsOptions(this.options); this.operations = this.internalOperations; } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs index 0e53856dac..c94983ecd5 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs @@ -65,7 +65,7 @@ public class BokehBlurTest // Make sure the kernel components are the same using Image image = new(1, 1); - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; BokehBlurProcessor definition = new(10, BokehBlurProcessor.DefaultComponents, BokehBlurProcessor.DefaultGamma); using BokehBlurProcessor processor = (BokehBlurProcessor)definition.CreatePixelSpecificProcessor(configuration, image, image.Bounds); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a976201126..b370f69fe3 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -62,6 +62,17 @@ public static class TestImages public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.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 public const string Filter0 = "Png/filter0.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 BikeSmall = "Webp/bike_lossy_small.webp"; public const string Animated = "Webp/leo_animated_lossy.webp"; + public const string AnimatedIssue2528 = "Webp/issues/Issue2528.webp"; // Lossy images without macroblock filtering. 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 Issues2255 = "Tiff/Issues/Issue2255.png"; 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 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 GrayscalePlainMagick = "Pbm/grayscale_plain_magick.pgm"; 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 RgbPlainNormalized = "Pbm/rgb_plain_normalized.ppm"; public const string RgbPlainMagick = "Pbm/rgb_plain_magick.ppm"; diff --git a/tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs b/tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs new file mode 100644 index 0000000000..d949ce0f2d --- /dev/null +++ b/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(); + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs index 52f160dedc..aa8ab397d2 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs @@ -28,7 +28,7 @@ public class ExactImageComparer : ImageComparer var bBuffer = new Rgba64[width]; var differences = new List(); - Configuration configuration = expected.GetConfiguration(); + Configuration configuration = expected.Configuration; Buffer2D expectedBuffer = expected.PixelBuffer; Buffer2D actualBuffer = actual.PixelBuffer; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs index c541133079..93ed4c6fff 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs @@ -72,7 +72,7 @@ public class TolerantImageComparer : ImageComparer float totalDifference = 0F; var differences = new List(); - Configuration configuration = expected.GetConfiguration(); + Configuration configuration = expected.Configuration; Buffer2D expectedBuffer = expected.PixelBuffer; Buffer2D actualBuffer = actual.PixelBuffer; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index 65fa736dcf..24faed69cc 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -179,7 +179,7 @@ public class ImagingTestCaseUtility return path; } - public IEnumerable GetTestOutputFileNamesMultiFrame( + public IEnumerable<(int Index, string FileName)> GetTestOutputFileNamesMultiFrame( int frameCount, string extension = null, object testOutputDetails = null, @@ -201,11 +201,11 @@ public class ImagingTestCaseUtility continue; } - yield return $"{baseDir}/{i:D2}.{extension}"; + yield return (i, $"{baseDir}/{i:D2}.{extension}"); } } - public string[] SaveTestOutputFileMultiFrame( + public (int Index, string FileName)[] SaveTestOutputFileMultiFrame( Image image, string extension = "png", IImageEncoder encoder = null, @@ -216,27 +216,17 @@ public class ImagingTestCaseUtility { encoder ??= TestEnvironment.GetReferenceEncoder($"foo.{extension}"); - string[] files = this.GetTestOutputFileNamesMultiFrame( + (int Index, string FileName)[] files = this.GetTestOutputFileNamesMultiFrame( image.Frames.Count, extension, testOutputDetails, appendPixelTypeToFileName, predicate: predicate).ToArray(); - for (int i = 0; i < image.Frames.Count; i++) + foreach ((int Index, string FileName) file in files) { - if (predicate != null && !predicate(i, image.Frames.Count)) - { - continue; - } - - if (i >= files.Length) - { - break; - } - - using Image frameImage = image.Frames.CloneFrame(i); - string filePath = files[i]; + using Image frameImage = image.Frames.CloneFrame(file.Index); + string filePath = file.FileName; using FileStream stream = File.OpenWrite(filePath); frameImage.Save(stream, encoder); } @@ -252,14 +242,14 @@ public class ImagingTestCaseUtility => TestEnvironment.GetReferenceOutputFileName( this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription)); - public string[] GetReferenceOutputFileNamesMultiFrame( + public (int Index, string FileName)[] GetReferenceOutputFileNamesMultiFrame( int frameCount, string extension, object testOutputDetails, bool appendPixelTypeToFileName = true, Func predicate = null) => this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails, appendPixelTypeToFileName, predicate: predicate) - .Select(TestEnvironment.GetReferenceOutputFileName).ToArray(); + .Select(x => (x.Index, TestEnvironment.GetReferenceOutputFileName(x.FileName))).ToArray(); internal void Init(string typeName, string methodName, string outputSubfolderName) { diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs index a4d305d97f..d32a6c93f1 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; @@ -15,10 +14,7 @@ public sealed class ImageSharpPngEncoderWithDefaultConfiguration : PngEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - Configuration configuration = Configuration.Default; - MemoryAllocator allocator = configuration.MemoryAllocator; - - using PngEncoderCore encoder = new(allocator, configuration, this); + using PngEncoderCore encoder = new(Configuration.Default, this); encoder.Encode(image, stream, cancellationToken); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs index e57da55895..04f59979f7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs @@ -45,7 +45,7 @@ public static class SystemDrawingBridge long sourceRowByteCount = data.Stride; long destRowByteCount = w * sizeof(Bgra32); - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; image.ProcessPixelRows(accessor => { using IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w); @@ -104,7 +104,7 @@ public static class SystemDrawingBridge long sourceRowByteCount = data.Stride; long destRowByteCount = w * sizeof(Bgr24); - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; using (IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w)) @@ -134,7 +134,7 @@ public static class SystemDrawingBridge internal static unsafe Bitmap To32bppArgbSystemDrawingBitmap(Image image) where TPixel : unmanaged, IPixel { - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; int w = image.Width; int h = image.Height; @@ -148,7 +148,7 @@ public static class SystemDrawingBridge long sourceRowByteCount = w * sizeof(Bgra32); image.ProcessPixelRows(accessor => { - using IMemoryOwner workBuffer = image.GetConfiguration().MemoryAllocator.Allocate(w); + using IMemoryOwner workBuffer = image.Configuration.MemoryAllocator.Allocate(w); fixed (Bgra32* sourcePtr = &workBuffer.GetReference()) { for (int y = 0; y < h; y++) diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 9951ecfa90..3c74b48938 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -336,7 +336,7 @@ public static class TestImageExtensions Func predicate = null) where TPixel : unmanaged, IPixel { - string[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame( + (int Index, string FileName)[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame( frameCount, extension, testOutputDetails, @@ -345,10 +345,11 @@ public static class TestImageExtensions List> temporaryFrameImages = new(); - IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]); + IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0].FileName); - foreach (string path in frameFiles) + for (int i = 0; i < frameFiles.Length; i++) { + string path = frameFiles[i].FileName; if (!File.Exists(path)) { throw new FileNotFoundException("Reference output file missing: " + path); @@ -536,10 +537,8 @@ public static class TestImageExtensions referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); using MemoryStream stream = new(testFile.Bytes); - using (Image original = referenceDecoder.Decode(referenceDecoderOptions ?? DecoderOptions.Default, stream)) - { - comparer.VerifySimilarity(original, image); - } + using Image original = referenceDecoder.Decode(referenceDecoderOptions ?? DecoderOptions.Default, stream); + comparer.VerifySimilarity(original, image); return image; } @@ -562,10 +561,8 @@ public static class TestImageExtensions referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); using MemoryStream stream = new(testFile.Bytes); - using (Image original = referenceDecoder.Decode(DecoderOptions.Default, stream)) - { - comparer.VerifySimilarity(original, image); - } + using Image original = referenceDecoder.Decode(DecoderOptions.Default, stream); + comparer.VerifySimilarity(original, image); return image; } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index cbce961103..3dceaf2524 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -200,13 +200,13 @@ public class TestImageProviderTests where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - string[] files = provider.Utility.SaveTestOutputFileMultiFrame(image); + (int Index, string FileName)[] files = provider.Utility.SaveTestOutputFileMultiFrame(image); Assert.True(files.Length > 2); - foreach (string path in files) + foreach ((int Index, string FileName) file in files) { - this.Output.WriteLine(path); - Assert.True(File.Exists(path)); + this.Output.WriteLine(file.FileName); + Assert.True(File.Exists(file.FileName)); } } @@ -342,8 +342,8 @@ public class TestImageProviderTests using Image image2 = provider.GetImage(); using Image image3 = provider.GetImage(); - Assert.Same(customConfiguration, image2.GetConfiguration()); - Assert.Same(customConfiguration, image3.GetConfiguration()); + Assert.Same(customConfiguration, image2.Configuration); + Assert.Same(customConfiguration, image3.Configuration); } } diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png new file mode 100644 index 0000000000..a695681b0f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_12-dispose-prev-first.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ea3f66d081c07c2eeefccae69084dbd0eabb824ace03280cb58a39b818de556 +size 102 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png new file mode 100644 index 0000000000..a695681b0f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_14-dispose-background-before-region.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ea3f66d081c07c2eeefccae69084dbd0eabb824ace03280cb58a39b818de556 +size 102 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png new file mode 100644 index 0000000000..7f10ed9664 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02c81691db45508be3fe8c6051e8b09937eaa347f332f1097026e00a0e084b38 +size 99 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png new file mode 100644 index 0000000000..7f10ed9664 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02c81691db45508be3fe8c6051e8b09937eaa347f332f1097026e00a0e084b38 +size 99 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png new file mode 100644 index 0000000000..de47c015c9 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_15-dispose-background-region.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1322aa335ad845cacfa20266bc0ffc31db117376373c15bcdb222abcf4b8f83 +size 113 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/04.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/05.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/06.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/07.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/08.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/104.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/112.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/120.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/24.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/40.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/48.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/48.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/48.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/56.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/56.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/56.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/64.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/64.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/72.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/72.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/72.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/80.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/80.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/80.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/88.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/88.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/88.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/96.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/96.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_21-blend-over-multiple.png/96.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_4-split-idat-zero-length.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_4-split-idat-zero-length.png/00.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_4-split-idat-zero-length.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/01.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/02.png new file mode 100644 index 0000000000..e544ca74e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_7-dispose-none.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11558f68c1a1c3ad32832c7fc91ae093b7351bef68222e4d28ea44f6f2d6511a +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/00.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/01.png new file mode 100644 index 0000000000..8fcbcb492a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2d35aad4996610f754a166ae30906b49f98979c14a71143f99911e465755a8 +size 89 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/02.png new file mode 100644 index 0000000000..a695681b0f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_8-dispose-background.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ea3f66d081c07c2eeefccae69084dbd0eabb824ace03280cb58a39b818de556 +size 102 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/00.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/00.png new file mode 100644 index 0000000000..7b8766bdc5 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6118abf41302696bfe4a62baa32a7798b3833ca49fc3854dcde4a810905fc457 +size 1012 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/01.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/01.png new file mode 100644 index 0000000000..097c9b76f9 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8d11d84cab8580efc7397870116ff3ddde4c3a5da9c2c2baa473eb463326072 +size 915 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/02.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/02.png new file mode 100644 index 0000000000..47148a78e6 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f36ea3ed9e652fe005c2767d758da268feb444e90833e02ab3fb15d1155037fd +size 971 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/03.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/03.png new file mode 100644 index 0000000000..ff550fcfbb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb61715535a98977f4a3cb89ac85bc56826a54b4bdd4393d89ca445f50865d22 +size 990 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/04.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/04.png new file mode 100644 index 0000000000..12233f3015 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_VerifyAllFrames_Rgba32_apng.png/04.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0648a06346a6ccca69503da187bc5901c7275ade03834030a8f3895ad03ff58a +size 941 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_TiledWithBadZlib_tiled-0000023664.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_TiledWithBadZlib_tiled-0000023664.png new file mode 100644 index 0000000000..d93f6ef3cd --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_TiledWithBadZlib_tiled-0000023664.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:456f0699fbba95953fbdba0164168583cc7d2efe1f858a6570938e8797b398cd +size 15586 diff --git a/tests/Images/Input/Pbm/00000_00000_premature_eof.ppm b/tests/Images/Input/Pbm/00000_00000_premature_eof.ppm new file mode 100644 index 0000000000..445cd0059a --- /dev/null +++ b/tests/Images/Input/Pbm/00000_00000_premature_eof.ppm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39cf6ca5b2f9d428c0c33e0fc7ab5e92c31e0c8a7d9e0276b9285f51a8ff547c +size 69 diff --git a/tests/Images/Input/Png/animated/12-dispose-prev-first.png b/tests/Images/Input/Png/animated/12-dispose-prev-first.png new file mode 100644 index 0000000000..7d6c9db25d --- /dev/null +++ b/tests/Images/Input/Png/animated/12-dispose-prev-first.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28138dd4a4ad56f86c18216b051b96a1bb353b69ebd85ce272928b085bb84400 +size 371 diff --git a/tests/Images/Input/Png/animated/14-dispose-background-before-region.png b/tests/Images/Input/Png/animated/14-dispose-background-before-region.png new file mode 100644 index 0000000000..3411044e6d --- /dev/null +++ b/tests/Images/Input/Png/animated/14-dispose-background-before-region.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3d4ba499c333a600dd1e42f374a9a68fb783b0f3274091ab34f5b395462eae8 +size 327 diff --git a/tests/Images/Input/Png/animated/15-dispose-background-region.png b/tests/Images/Input/Png/animated/15-dispose-background-region.png new file mode 100644 index 0000000000..8e684686c9 --- /dev/null +++ b/tests/Images/Input/Png/animated/15-dispose-background-region.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6db2a90911b40067b7f35b01869115f081858ee15b28374e57c51c7e5c0cb524 +size 492 diff --git a/tests/Images/Input/Png/animated/21-blend-over-multiple.png b/tests/Images/Input/Png/animated/21-blend-over-multiple.png new file mode 100644 index 0000000000..4c088bacc4 --- /dev/null +++ b/tests/Images/Input/Png/animated/21-blend-over-multiple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b571f7034ef1fb355182cf00fa6ccd7d784720709f229e3bcc5948abf2f81ee +size 28791 diff --git a/tests/Images/Input/Png/animated/4-split-idat-zero-length.png b/tests/Images/Input/Png/animated/4-split-idat-zero-length.png new file mode 100644 index 0000000000..d2d6567462 --- /dev/null +++ b/tests/Images/Input/Png/animated/4-split-idat-zero-length.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0ffdbe7dc6dad05dfc4cacd712b76c1121cd7378671212ae000d76c07b1a4e +size 273 diff --git a/tests/Images/Input/Png/animated/7-dispose-none.png b/tests/Images/Input/Png/animated/7-dispose-none.png new file mode 100644 index 0000000000..d0ef09b852 --- /dev/null +++ b/tests/Images/Input/Png/animated/7-dispose-none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1abab0c7de5252a16da34777ff34c4a29c6000493d23ac1777cd17415e6aab33 +size 617 diff --git a/tests/Images/Input/Png/animated/8-dispose-background.png b/tests/Images/Input/Png/animated/8-dispose-background.png new file mode 100644 index 0000000000..89052b655d --- /dev/null +++ b/tests/Images/Input/Png/animated/8-dispose-background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f26f544d5f7f0c8d4448ca020c93f79b64e1d607c7c561082bc989ca2e91fad +size 572 diff --git a/tests/Images/Input/Png/animated/apng.png b/tests/Images/Input/Png/animated/apng.png new file mode 100644 index 0000000000..7def301ae6 --- /dev/null +++ b/tests/Images/Input/Png/animated/apng.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c15e4670da1826d1cc25555bd6cbe287ecc70327cd029a7613334a39a283021 +size 2508 diff --git a/tests/Images/Input/Tiff/Issues/JpegCompressedGray-0000539558.tiff b/tests/Images/Input/Tiff/Issues/JpegCompressedGray-0000539558.tiff new file mode 100644 index 0000000000..934bf3c9a3 --- /dev/null +++ b/tests/Images/Input/Tiff/Issues/JpegCompressedGray-0000539558.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f1ca630b5e46c7b5f21100fa8c0fbf27b79ca9da8cd95897667b64aedccf6e5 +size 539558 diff --git a/tests/Images/Input/Tiff/Issues/tiled-0000023664.tiff b/tests/Images/Input/Tiff/Issues/tiled-0000023664.tiff new file mode 100644 index 0000000000..5106a027cc --- /dev/null +++ b/tests/Images/Input/Tiff/Issues/tiled-0000023664.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb28a028b2467b9b42451d9cb30d8170fd91ff4c4046b69cc1ae7f123bf7ba6f +size 23664 diff --git a/tests/Images/Input/Webp/issues/Issue2528.webp b/tests/Images/Input/Webp/issues/Issue2528.webp new file mode 100644 index 0000000000..c7ff62ec3d --- /dev/null +++ b/tests/Images/Input/Webp/issues/Issue2528.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4aa2ba2e6ef0263b5b657e4d15241d497721a0461250b1d942751812b96de71 +size 60214