From a80ae33e4cac6acd60c0303f6740ce85c85cccad Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Oct 2023 18:31:25 +0200 Subject: [PATCH 1/6] Add webp decoder option BackgroundColorHandling to decide howto handle the background color in the ANIM chunk --- .../Decompressors/WebpTiffCompression.cs | 2 +- .../Compression/TiffDecompressorsFactory.cs | 1 + .../Formats/Webp/BackgroundColorHandling.cs | 21 +++++++++++++++++++ .../Formats/Webp/WebpAnimationDecoder.cs | 14 +++++++++++-- src/ImageSharp/Formats/Webp/WebpDecoder.cs | 18 +++++++++++----- .../Formats/Webp/WebpDecoderCore.cs | 18 ++++++++++------ .../Formats/Webp/WebpDecoderOptions.cs | 21 +++++++++++++++++++ 7 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs create mode 100644 src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs 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 @@ public WebpTiffCompression(DecoderOptions options, MemoryAllocator memoryAllocat /// 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/Webp/BackgroundColorHandling.cs b/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs new file mode 100644 index 0000000000..c843311c7f --- /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 black, BGRA(0, 0, 0, 0). + /// + Ignore = 1 +} 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 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat 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 @@ protected override ImageInfo Identify(DecoderOptions options, Stream stream, Can 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 @@ public Image Decode(BufferedReadStream stream, CancellationToken { 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; +} From 22935fd25f6cec3b4f6d935484dc504717e64ee5 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 12 Oct 2023 20:10:13 +0200 Subject: [PATCH 2/6] Add test for Issue 2528 --- .../Formats/WebP/WebpDecoderTests.cs | 19 ++++++++++++++++++- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Webp/issues/Issue2528.webp | 3 +++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/Images/Input/Webp/issues/Issue2528.webp 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 void Decode_AnimatedLossless_WithFrameDecodingModeFirst_OnlyDecodesOneFra 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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 180a8594c0..ad1104a072 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -677,6 +677,7 @@ public static class Lossy 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"; 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 From d14494c780674d67ef7d951e52ff387293568c6e Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Thu, 12 Oct 2023 20:10:44 +0200 Subject: [PATCH 3/6] Update src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs Co-authored-by: James Jackson-South --- src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs b/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs index c843311c7f..5be8f6a296 100644 --- a/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs +++ b/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs @@ -15,7 +15,7 @@ public enum BackgroundColorHandling Standard = 0, /// - /// The background color of the ANIM chunk is ignored and instead the canvas is initialized with black, BGRA(0, 0, 0, 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 } From fa4febf7fe3264a440c4d4e135dd027882e24fb0 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 12 Oct 2023 20:30:06 +0200 Subject: [PATCH 4/6] Fix path to test file --- tests/ImageSharp.Tests/TestImages.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ad1104a072..cb7ef7f376 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -677,7 +677,7 @@ public static class Lossy 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"; + public const string AnimatedIssue2528 = "Webp/Issues/issue2528.webp"; // Lossy images without macroblock filtering. public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp"; From 1fd440484312604304fd997d691c0134da9c11e6 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 12 Oct 2023 20:48:19 +0200 Subject: [PATCH 5/6] Another attempt fixing the file path: path should be lower case --- tests/ImageSharp.Tests/TestImages.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index cb7ef7f376..83d1bf406a 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -677,7 +677,7 @@ public static class Lossy 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"; + public const string AnimatedIssue2528 = "Webp/issues/issue2528.webp"; // Lossy images without macroblock filtering. public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp"; From f4f3a2279ae16d45f1861a7a011381f40c58d312 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 12 Oct 2023 21:48:49 +0200 Subject: [PATCH 6/6] Revert fa4febf7 --- tests/ImageSharp.Tests/TestImages.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 83d1bf406a..470a670a4d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -677,7 +677,7 @@ public static class Lossy 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"; + public const string AnimatedIssue2528 = "Webp/issues/Issue2528.webp"; // Lossy images without macroblock filtering. public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp";