From a486558ed19a8fda1c60bf50e9e2e5d258b8bcbd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 17 Nov 2023 13:19:02 +1000 Subject: [PATCH] Complete Webp and add tests --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 20 +++- .../Formats/Gif/MetadataExtensions.cs | 13 ++- .../Formats/Png/MetadataExtensions.cs | 18 ++- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 4 +- .../Formats/Png/PngDisposalMethod.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 59 +++++++++- .../Formats/Png/PngFrameMetadata.cs | 18 ++- src/ImageSharp/Formats/Png/PngMetadata.cs | 33 ++++++ .../Formats/Webp/Chunks/WebpFrameData.cs | 2 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 4 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +- .../Formats/Webp/MetadataExtensions.cs | 2 +- .../Formats/Webp/WebpCommonUtils.cs | 56 ++++++++- .../Formats/Webp/WebpDisposalMethod.cs | 2 +- .../Formats/Webp/WebpEncoderCore.cs | 2 +- .../Formats/Webp/WebpFrameMetadata.cs | 8 ++ src/ImageSharp/Formats/Webp/WebpMetadata.cs | 8 ++ .../Quantization/EuclideanPixelMap{TPixel}.cs | 6 +- .../Formats/Gif/GifEncoderTests.cs | 92 ++++++++++++++- .../Formats/Png/PngEncoderTests.cs | 109 +++++++++++++++++- .../Formats/WebP/WebpEncoderTests.cs | 93 +++++++++++++++ 21 files changed, 515 insertions(+), 40 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 33942ce54..d22b960ec 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -190,19 +190,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals return GifMetadata.FromAnimatedMetadata(ani); } + // Return explicit new instance so we do not mutate the original metadata. return new(); } private static GifFrameMetadata? GetGifFrameMetadata(ImageFrame frame, int transparencyIndex) where TPixel : unmanaged, IPixel { - if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata? gif)) + if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif)) { return gif; } GifFrameMetadata? metadata = null; - if (frame.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? png)) + if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png)) { AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata(); metadata = GifFrameMetadata.FromAnimatedMetadata(ani); @@ -342,7 +343,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals } } - this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement); + // We can't deduplicate here as we need the background pixels to be present in the buffer. + if (metadata?.DisposalMethod == GifDisposalMethod.RestoreToBackground) + { + for (int y = 0; y < currentFrame.PixelBuffer.Height; y++) + { + Span sourceRow = currentFrame.PixelBuffer.DangerousGetRowSpan(y); + Span destinationRow = encodingFrame.PixelBuffer.DangerousGetRowSpan(y); + sourceRow.CopyTo(destinationRow); + } + } + else + { + this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement); + } IndexedImageFrame quantized; if (useLocal) diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index f4eaffe6b..c7f9f84c8 100644 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs @@ -56,16 +56,25 @@ public static partial class MetadataExtensions /// /// if the gif frame metadata exists; otherwise, . /// - public static bool TryGetGifFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata) + public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata) => source.TryGetFormatMetadata(GifFormat.Instance, out metadata); internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source) - => new() + { + Color background = Color.Transparent; + if (source.GlobalColorTable != null) + { + background = source.GlobalColorTable.Value.Span[source.BackgroundColorIndex]; + } + + return new() { ColorTable = source.GlobalColorTable, ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local, RepeatCount = source.RepeatCount, + BackgroundColor = background, }; + } internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source) => new() diff --git a/src/ImageSharp/Formats/Png/MetadataExtensions.cs b/src/ImageSharp/Formats/Png/MetadataExtensions.cs index 4a606d3a4..b6313bffe 100644 --- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Png/MetadataExtensions.cs @@ -36,7 +36,7 @@ public static partial class MetadataExtensions /// /// The metadata this method extends. /// The . - public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance); + public static PngFrameMetadata GetPngMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance); /// /// Gets the png format specific metadata for the image frame. @@ -46,7 +46,7 @@ public static partial class MetadataExtensions /// /// if the png frame metadata exists; otherwise, . /// - public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) + public static bool TryGetPngMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata); internal static AnimatedImageMetadata ToAnimatedImageMetadata(this PngMetadata source) @@ -58,17 +58,25 @@ public static partial class MetadataExtensions }; internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this PngFrameMetadata source) - => new() + { + double delay = source.FrameDelay.ToDouble(); + if (double.IsNaN(delay)) + { + delay = 0; + } + + return new() { ColorTableMode = FrameColorTableMode.Global, - Duration = TimeSpan.FromMilliseconds(source.FrameDelay.ToDouble() * 1000), + Duration = TimeSpan.FromMilliseconds(delay * 1000), DisposalMode = GetMode(source.DisposalMethod), BlendMode = source.BlendMethod == PngBlendMethod.Source ? FrameBlendMode.Source : FrameBlendMode.Over, }; + } private static FrameDisposalMode GetMode(PngDisposalMethod method) => method switch { - PngDisposalMethod.None => FrameDisposalMode.DoNotDispose, + PngDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose, PngDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground, PngDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious, _ => FrameDisposalMode.Unspecified, diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 7d573efb6..b0706b14c 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -581,7 +581,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.header.Height, metadata); - PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata(); + PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngMetadata(); frameMetadata.FromChunk(in frameControl); this.bytesPerPixel = this.CalculateBytesPerPixel(); @@ -630,7 +630,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals pixelRegion.Clear(); } - PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata(); + PngFrameMetadata frameMetadata = frame.Metadata.GetPngMetadata(); frameMetadata.FromChunk(currentFrameControl); this.previousScanline?.Dispose(); diff --git a/src/ImageSharp/Formats/Png/PngDisposalMethod.cs b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs index a431e8941..1537c5ced 100644 --- a/src/ImageSharp/Formats/Png/PngDisposalMethod.cs +++ b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs @@ -11,7 +11,7 @@ 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, + DoNotDispose, /// /// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index be6991bab..a779718a0 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -7,8 +7,10 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -137,7 +139,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The to encode the image data to. /// The token to request cancellation. public void Encode(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); @@ -146,7 +148,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.height = image.Height; ImageMetadata metadata = image.Metadata; - PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance); + PngMetadata pngMetadata = GetPngMetadata(image); this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel); stream.Write(PngConstants.HeaderBytes); @@ -234,6 +236,54 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable this.currentScanline?.Dispose(); } + private static PngMetadata GetPngMetadata(Image image) + where TPixel : unmanaged, IPixel + { + if (image.Metadata.TryGetPngMetadata(out PngMetadata? png)) + { + return png; + } + + if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif)) + { + AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata(); + return PngMetadata.FromAnimatedMetadata(ani); + } + + if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp)) + { + AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata(); + return PngMetadata.FromAnimatedMetadata(ani); + } + + // Return explicit new instance so we do not mutate the original metadata. + return new(); + } + + private static PngFrameMetadata GetPngFrameMetadata(ImageFrame frame) + where TPixel : unmanaged, IPixel + { + if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png)) + { + return png; + } + + if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif)) + { + AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata(); + return PngFrameMetadata.FromAnimatedMetadata(ani); + } + + if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp)) + { + AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata(); + return PngFrameMetadata.FromAnimatedMetadata(ani); + } + + // Return explicit new instance so we do not mutate the original metadata. + return new(); + } + /// /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases. /// @@ -985,9 +1035,10 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The containing image data. /// The image frame. /// The frame sequence number. - private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber) + private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber) + where TPixel : unmanaged, IPixel { - PngFrameMetadata frameMetadata = imageFrame.Metadata.GetPngFrameMetadata(); + PngFrameMetadata frameMetadata = GetPngFrameMetadata(imageFrame); // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. FrameControl fcTL = new( diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs index ca4d8c1f4..dbda4d73c 100644 --- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs @@ -34,7 +34,7 @@ public class PngFrameMetadata : IDeepCloneable /// 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; } + public Rational FrameDelay { get; set; } = new(0); /// /// Gets or sets the type of frame area disposal to be done after rendering this frame @@ -59,4 +59,20 @@ public class PngFrameMetadata : IDeepCloneable /// public IDeepCloneable DeepClone() => new PngFrameMetadata(this); + + internal static PngFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) + => new() + { + FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000), + DisposalMethod = GetMode(metadata.DisposalMode), + BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? PngBlendMethod.Source : PngBlendMethod.Over, + }; + + private static PngDisposalMethod GetMode(FrameDisposalMode mode) => mode switch + { + FrameDisposalMode.RestoreToBackground => PngDisposalMethod.RestoreToBackground, + FrameDisposalMode.RestoreToPrevious => PngDisposalMethod.RestoreToPrevious, + FrameDisposalMode.DoNotDispose => PngDisposalMethod.DoNotDispose, + _ => PngDisposalMethod.DoNotDispose, + }; } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 6110cdd0c..8e2691c10 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -85,4 +85,37 @@ public class PngMetadata : IDeepCloneable /// public IDeepCloneable DeepClone() => new PngMetadata(this); + + internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) + { + // Should the conversion be from a format that uses a 24bit palette entries (gif) + // we need to clone and adjust the color table to allow for transparency. + ReadOnlyMemory? colorTable = metadata.ColorTable; + if (metadata.ColorTable.HasValue) + { + Color[] clone = metadata.ColorTable.Value.ToArray(); + for (int i = 0; i < clone.Length; i++) + { + ref Color c = ref clone[i]; + if (c == metadata.BackgroundColor) + { + // Png treats background as fully empty + c = default; + break; + } + } + + colorTable = clone; + } + + return new() + { + ColorType = colorTable.HasValue ? PngColorType.Palette : null, + BitDepth = colorTable.HasValue + ? (PngBitDepth)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(colorTable.Value.Length), 1, 8) + : null, + ColorTable = colorTable, + RepeatCount = metadata.RepeatCount, + }; + } } diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs index aee518326..230f69c32 100644 --- a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs @@ -33,7 +33,7 @@ internal readonly struct WebpFrameData height, duration, (flags & 2) == 0 ? WebpBlendingMethod.Over : WebpBlendingMethod.Source, - (flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.None) + (flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose) { } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 0821be577..b9e2519fa 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -259,7 +259,7 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); } } @@ -307,7 +307,7 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(frame); // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = new WebpFrameData( diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 3fea72c07..e6148a066 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -333,7 +333,7 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); } } @@ -477,7 +477,7 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(frame); // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = new WebpFrameData( diff --git a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs index 44da191d2..10c72a3d9 100644 --- a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs @@ -69,7 +69,7 @@ public static partial class MetadataExtensions private static FrameDisposalMode GetMode(WebpDisposalMethod method) => method switch { WebpDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground, - WebpDisposalMethod.None => FrameDisposalMode.DoNotDispose, + WebpDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose, _ => FrameDisposalMode.DoNotDispose, }; } diff --git a/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs index 1a8fcbafc..bb7dd6f27 100644 --- a/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs @@ -4,6 +4,8 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp; @@ -13,6 +15,54 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// internal static class WebpCommonUtils { + public static WebpMetadata GetWebpMetadata(Image image) + where TPixel : unmanaged, IPixel + { + if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp)) + { + return webp; + } + + if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif)) + { + AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata(); + return WebpMetadata.FromAnimatedMetadata(ani); + } + + if (image.Metadata.TryGetPngMetadata(out PngMetadata? png)) + { + AnimatedImageMetadata ani = png.ToAnimatedImageMetadata(); + return WebpMetadata.FromAnimatedMetadata(ani); + } + + // Return explicit new instance so we do not mutate the original metadata. + return new(); + } + + public static WebpFrameMetadata GetWebpFrameMetadata(ImageFrame frame) + where TPixel : unmanaged, IPixel + { + if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp)) + { + return webp; + } + + if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif)) + { + AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata(); + return WebpFrameMetadata.FromAnimatedMetadata(ani); + } + + if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png)) + { + AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata(); + return WebpFrameMetadata.FromAnimatedMetadata(ani); + } + + // Return explicit new instance so we do not mutate the original metadata. + return new(); + } + /// /// Checks if the pixel row is not opaque. /// @@ -27,7 +77,7 @@ internal static class WebpCommonUtils int length = (row.Length * 4) - 3; fixed (byte* src = rowBytes) { - var alphaMaskVector256 = Vector256.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); + Vector256 alphaMaskVector256 = Vector256.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); Vector256 all0x80Vector256 = Vector256.Create((byte)0x80).AsByte(); for (; i + 128 <= length; i += 128) @@ -124,7 +174,7 @@ internal static class WebpCommonUtils private static unsafe bool IsNoneOpaque64Bytes(byte* src, int i) { - var alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); + Vector128 alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); Vector128 a0 = Sse2.LoadVector128(src + i).AsByte(); Vector128 a1 = Sse2.LoadVector128(src + i + 16).AsByte(); @@ -144,7 +194,7 @@ internal static class WebpCommonUtils private static unsafe bool IsNoneOpaque32Bytes(byte* src, int i) { - var alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); + Vector128 alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); Vector128 a0 = Sse2.LoadVector128(src + i).AsByte(); Vector128 a1 = Sse2.LoadVector128(src + i + 16).AsByte(); diff --git a/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs index 397c2ee50..47cc83951 100644 --- a/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs +++ b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs @@ -11,7 +11,7 @@ public enum WebpDisposalMethod /// /// Do not dispose. Leave the canvas as is. /// - None = 0, + DoNotDispose = 0, /// /// Dispose to background color. Fill the rectangle on the canvas covered by the current frame with background color specified in the ANIM chunk. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 47712071b..837487047 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -123,7 +123,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); + WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); lossless = webpMetadata.FileFormat == WebpFileFormatType.Lossless; } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index ef21d8b6f..667b8f8f4 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -44,4 +44,12 @@ public class WebpFrameMetadata : IDeepCloneable /// public IDeepCloneable DeepClone() => new WebpFrameMetadata(this); + + internal static WebpFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) + => new() + { + FrameDelay = (uint)metadata.Duration.Milliseconds, + BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? WebpBlendingMethod.Source : WebpBlendingMethod.Over, + DisposalMethod = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose + }; } diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index 9d0d8d08d..536ea0929 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -46,4 +46,12 @@ public class WebpMetadata : IDeepCloneable /// public IDeepCloneable DeepClone() => new WebpMetadata(this); + + internal static WebpMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) + => new() + { + FileFormat = WebpFileFormatType.Lossless, + BackgroundColor = metadata.BackgroundColor, + RepeatCount = metadata.RepeatCount + }; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index f75664903..8aa166d16 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -200,7 +200,7 @@ internal sealed class EuclideanPixelMap : IDisposable } [MethodImpl(InliningOptions.ShortMethod)] - public void Add(Rgba32 rgba, byte index) + public readonly void Add(Rgba32 rgba, byte index) { int r = rgba.R >> RgbShift; int g = rgba.G >> RgbShift; @@ -211,7 +211,7 @@ internal sealed class EuclideanPixelMap : IDisposable } [MethodImpl(InliningOptions.ShortMethod)] - public bool TryGetValue(Rgba32 rgba, out short match) + public readonly bool TryGetValue(Rgba32 rgba, out short match) { int r = rgba.R >> RgbShift; int g = rgba.G >> RgbShift; @@ -226,7 +226,7 @@ internal sealed class EuclideanPixelMap : IDisposable /// Clears the cache resetting each entry to empty. /// [MethodImpl(InliningOptions.ShortMethod)] - public void Clear() => this.table.GetSpan().Fill(-1); + public readonly void Clear() => this.table.GetSpan().Fill(-1); [MethodImpl(InliningOptions.ShortMethod)] private static int GetPaletteIndex(int r, int g, int b, int a) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 65d186c91..cd485b5fa 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -2,6 +2,8 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -245,7 +247,7 @@ public class GifEncoderTests int count = 0; foreach (ImageFrame frame in image.Frames) { - if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata _)) + if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _)) { count++; } @@ -261,7 +263,7 @@ public class GifEncoderTests count = 0; foreach (ImageFrame frame in image2.Frames) { - if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata _)) + if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _)) { count++; } @@ -269,4 +271,90 @@ public class GifEncoderTests Assert.Equal(image2.Frames.Count, count); } + + [Theory] + [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)] + public void Encode_AnimatedFormatTransform_FromPng(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + + using MemoryStream memStream = new(); + image.Save(memStream, new GifEncoder()); + memStream.Position = 0; + + using Image output = Image.Load(memStream); + + // TODO: Find a better way to compare. + // The image has been visually checked but the quantization and frame trimming pattern used in the gif encoder + // means we cannot use an exact comparison nor replicate using the quantizing processor. + ImageComparer.TolerantPercentage(1.51f).VerifySimilarity(output, image); + + PngMetadata png = image.Metadata.GetPngMetadata(); + GifMetadata gif = output.Metadata.GetGifMetadata(); + + Assert.Equal(png.RepeatCount, gif.RepeatCount); + + for (int i = 0; i < image.Frames.Count; i++) + { + PngFrameMetadata pngF = image.Frames[i].Metadata.GetPngMetadata(); + GifFrameMetadata gifF = output.Frames[i].Metadata.GetGifMetadata(); + + Assert.Equal((int)(pngF.FrameDelay.ToDouble() * 100), gifF.FrameDelay); + + switch (pngF.DisposalMethod) + { + case PngDisposalMethod.RestoreToBackground: + Assert.Equal(GifDisposalMethod.RestoreToBackground, gifF.DisposalMethod); + break; + case PngDisposalMethod.DoNotDispose: + default: + Assert.Equal(GifDisposalMethod.NotDispose, gifF.DisposalMethod); + break; + } + } + } + + [Theory] + [WithFile(TestImages.Webp.Lossless.Animated, PixelTypes.Rgba32)] + public void Encode_AnimatedFormatTransform_FromWebp(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(WebpDecoder.Instance); + + using MemoryStream memStream = new(); + image.Save(memStream, new GifEncoder()); + memStream.Position = 0; + + using Image output = Image.Load(memStream); + + // TODO: Find a better way to compare. + // The image has been visually checked but the quantization and frame trimming pattern used in the gif encoder + // means we cannot use an exact comparison nor replicate using the quantizing processor. + ImageComparer.TolerantPercentage(0.776f).VerifySimilarity(output, image); + + WebpMetadata webp = image.Metadata.GetWebpMetadata(); + GifMetadata gif = output.Metadata.GetGifMetadata(); + + Assert.Equal(webp.RepeatCount, gif.RepeatCount); + + for (int i = 0; i < image.Frames.Count; i++) + { + WebpFrameMetadata webpF = image.Frames[i].Metadata.GetWebpMetadata(); + GifFrameMetadata gifF = output.Frames[i].Metadata.GetGifMetadata(); + + Assert.Equal(webpF.FrameDelay, (uint)(gifF.FrameDelay * 10)); + + switch (webpF.DisposalMethod) + { + case WebpDisposalMethod.RestoreToBackground: + Assert.Equal(GifDisposalMethod.RestoreToBackground, gifF.DisposalMethod); + break; + case WebpDisposalMethod.DoNotDispose: + default: + Assert.Equal(GifDisposalMethod.NotDispose, gifF.DisposalMethod); + break; + } + } + } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index a6840b33e..45dd30b3b 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -454,10 +455,6 @@ public partial class PngEncoderTests memStream.Position = 0; image.DebugSave(provider: provider, encoder: PngEncoder, null, false); - image.DebugSave(provider: provider, encoder: new GifEncoder(), "gif", false); - - string path = provider.Utility.GetTestOutputFileName("gif"); - image.Save(path); using Image output = Image.Load(memStream); ImageComparer.Exact.VerifySimilarity(output, image); @@ -472,8 +469,8 @@ public partial class PngEncoderTests for (int i = 0; i < image.Frames.Count; i++) { - PngFrameMetadata originalFrameMetadata = image.Frames[i].Metadata.GetPngFrameMetadata(); - PngFrameMetadata outputFrameMetadata = output.Frames[i].Metadata.GetPngFrameMetadata(); + PngFrameMetadata originalFrameMetadata = image.Frames[i].Metadata.GetPngMetadata(); + PngFrameMetadata outputFrameMetadata = output.Frames[i].Metadata.GetPngMetadata(); Assert.Equal(originalFrameMetadata.FrameDelay, outputFrameMetadata.FrameDelay); Assert.Equal(originalFrameMetadata.BlendMethod, outputFrameMetadata.BlendMethod); @@ -481,6 +478,106 @@ public partial class PngEncoderTests } } + [Theory] + [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] + public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(GifDecoder.Instance); + using MemoryStream memStream = new(); + + image.Save(memStream, PngEncoder); + memStream.Position = 0; + + image.Save(provider.Utility.GetTestOutputFileName("png"), new PngEncoder()); + image.Save(provider.Utility.GetTestOutputFileName("gif"), new GifEncoder()); + + using Image output = Image.Load(memStream); + + // TODO: Find a better way to compare. + // The image has been visually checked but the quantization pattern used in the png encoder + // means we cannot use an exact comparison nor replicate using the quantizing processor. + ImageComparer.TolerantPercentage(0.12f).VerifySimilarity(output, image); + + GifMetadata gif = image.Metadata.GetGifMetadata(); + PngMetadata png = output.Metadata.GetPngMetadata(); + + Assert.Equal(gif.RepeatCount, png.RepeatCount); + + for (int i = 0; i < image.Frames.Count; i++) + { + GifFrameMetadata gifF = image.Frames[i].Metadata.GetGifMetadata(); + PngFrameMetadata pngF = output.Frames[i].Metadata.GetPngMetadata(); + + Assert.Equal(gifF.FrameDelay, (int)(pngF.FrameDelay.ToDouble() * 100)); + + switch (gifF.DisposalMethod) + { + case GifDisposalMethod.RestoreToBackground: + Assert.Equal(PngDisposalMethod.RestoreToBackground, pngF.DisposalMethod); + break; + case GifDisposalMethod.RestoreToPrevious: + Assert.Equal(PngDisposalMethod.RestoreToPrevious, pngF.DisposalMethod); + break; + case GifDisposalMethod.Unspecified: + case GifDisposalMethod.NotDispose: + default: + Assert.Equal(PngDisposalMethod.DoNotDispose, pngF.DisposalMethod); + break; + } + } + } + + [Theory] + [WithFile(TestImages.Webp.Lossless.Animated, PixelTypes.Rgba32)] + public void Encode_AnimatedFormatTransform_FromWebp(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(WebpDecoder.Instance); + + using MemoryStream memStream = new(); + image.Save(memStream, PngEncoder); + memStream.Position = 0; + + using Image output = Image.Load(memStream); + ImageComparer.Exact.VerifySimilarity(output, image); + + WebpMetadata webp = image.Metadata.GetWebpMetadata(); + PngMetadata png = output.Metadata.GetPngMetadata(); + + Assert.Equal(webp.RepeatCount, png.RepeatCount); + + for (int i = 0; i < image.Frames.Count; i++) + { + WebpFrameMetadata webpF = image.Frames[i].Metadata.GetWebpMetadata(); + PngFrameMetadata pngF = output.Frames[i].Metadata.GetPngMetadata(); + + Assert.Equal(webpF.FrameDelay, (uint)(pngF.FrameDelay.ToDouble() * 1000)); + + switch (webpF.BlendMethod) + { + case WebpBlendingMethod.Source: + Assert.Equal(PngBlendMethod.Source, pngF.BlendMethod); + break; + case WebpBlendingMethod.Over: + default: + Assert.Equal(PngBlendMethod.Over, pngF.BlendMethod); + break; + } + + switch (webpF.DisposalMethod) + { + case WebpDisposalMethod.RestoreToBackground: + Assert.Equal(PngDisposalMethod.RestoreToBackground, pngF.DisposalMethod); + break; + case WebpDisposalMethod.DoNotDispose: + default: + Assert.Equal(PngDisposalMethod.DoNotDispose, pngF.DisposalMethod); + break; + } + } + } + [Theory] [MemberData(nameof(PngTrnsFiles))] public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 0ad684b27..0fafdbe16 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -2,6 +2,8 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -60,6 +62,97 @@ public class WebpEncoderTests encoded.CompareToReferenceOutput(ImageComparer.Tolerant(0.01f), provider, null, "webp"); } + [Theory] + [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] + public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(GifDecoder.Instance); + using MemoryStream memStream = new(); + + image.Save(memStream, new WebpEncoder()); + memStream.Position = 0; + + using Image output = Image.Load(memStream); + + ImageComparer.Exact.VerifySimilarity(output, image); + + GifMetadata gif = image.Metadata.GetGifMetadata(); + WebpMetadata webp = output.Metadata.GetWebpMetadata(); + + Assert.Equal(gif.RepeatCount, webp.RepeatCount); + + for (int i = 0; i < image.Frames.Count; i++) + { + GifFrameMetadata gifF = image.Frames[i].Metadata.GetGifMetadata(); + WebpFrameMetadata webpF = output.Frames[i].Metadata.GetWebpMetadata(); + + Assert.Equal(gifF.FrameDelay, (int)(webpF.FrameDelay / 10)); + + switch (gifF.DisposalMethod) + { + case GifDisposalMethod.RestoreToBackground: + Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod); + break; + case GifDisposalMethod.RestoreToPrevious: + case GifDisposalMethod.Unspecified: + case GifDisposalMethod.NotDispose: + default: + Assert.Equal(WebpDisposalMethod.DoNotDispose, webpF.DisposalMethod); + break; + } + } + } + + [Theory] + [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)] + public void Encode_AnimatedFormatTransform_FromPng(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + + using MemoryStream memStream = new(); + image.Save(memStream, new WebpEncoder()); + memStream.Position = 0; + + using Image output = Image.Load(memStream); + ImageComparer.Exact.VerifySimilarity(output, image); + PngMetadata png = image.Metadata.GetPngMetadata(); + WebpMetadata webp = output.Metadata.GetWebpMetadata(); + + Assert.Equal(png.RepeatCount, webp.RepeatCount); + + for (int i = 0; i < image.Frames.Count; i++) + { + PngFrameMetadata pngF = image.Frames[i].Metadata.GetPngMetadata(); + WebpFrameMetadata webpF = output.Frames[i].Metadata.GetWebpMetadata(); + + Assert.Equal((uint)(pngF.FrameDelay.ToDouble() * 1000), webpF.FrameDelay); + + switch (pngF.BlendMethod) + { + case PngBlendMethod.Source: + Assert.Equal(WebpBlendingMethod.Source, webpF.BlendMethod); + break; + case PngBlendMethod.Over: + default: + Assert.Equal(WebpBlendingMethod.Over, webpF.BlendMethod); + break; + } + + switch (pngF.DisposalMethod) + { + case PngDisposalMethod.RestoreToBackground: + Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod); + break; + case PngDisposalMethod.DoNotDispose: + default: + Assert.Equal(WebpDisposalMethod.DoNotDispose, webpF.DisposalMethod); + break; + } + } + } + [Theory] [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy. [WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]