From cc0727b286ba760003e719b07b3a0c84fd6eafa7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 23 Nov 2023 23:40:10 +1000 Subject: [PATCH] Add dedup to webp --- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 37 +++++----- .../Formats/Webp/BitWriter/BitWriterBase.cs | 5 +- .../Formats/Webp/Chunks/WebpFrameData.cs | 10 +-- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 69 ++++++++---------- .../Formats/Webp/Lossy/Vp8Encoder.cs | 60 ++++++++------- .../Formats/Webp/Lossy/YuvConversion.cs | 15 ++-- .../Formats/Webp/WebpAnimationDecoder.cs | 2 +- .../Formats/Webp/WebpEncoderCore.cs | 73 +++++++++++++++---- .../Formats/WebP/YuvConversionTests.cs | 5 +- 9 files changed, 154 insertions(+), 122 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index cbd2aa8e7f..46030dde32 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -27,7 +27,7 @@ internal static class AlphaEncoder /// The size in bytes of the alpha data. /// The encoded alpha data. public static IMemoryOwner EncodeAlpha( - ImageFrame frame, + Buffer2DRegion frame, Configuration configuration, MemoryAllocator memoryAllocator, bool skipMetadata, @@ -35,8 +35,6 @@ internal static class AlphaEncoder out int size) where TPixel : unmanaged, IPixel { - int width = frame.Width; - int height = frame.Height; IMemoryOwner alphaData = ExtractAlphaChannel(frame, configuration, memoryAllocator); if (compress) @@ -46,8 +44,8 @@ internal static class AlphaEncoder using Vp8LEncoder lossLessEncoder = new( memoryAllocator, configuration, - width, - height, + frame.Width, + frame.Height, quality, skipMetadata, effort, @@ -58,14 +56,14 @@ internal static class AlphaEncoder // The transparency information will be stored in the green channel of the ARGB quadruplet. // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, // that can improve compression. - using ImageFrame alphaAsFrame = DispatchAlphaToGreen(frame, alphaData.GetSpan()); + using ImageFrame alphaAsFrame = DispatchAlphaToGreen(configuration, frame, alphaData.GetSpan()); - size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame, alphaData); + size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame.PixelBuffer.GetRegion(), alphaData); return alphaData; } - size = width * height; + size = frame.Width * frame.Height; return alphaData; } @@ -73,25 +71,28 @@ internal static class AlphaEncoder /// Store the transparency in the green channel. /// /// The pixel format. - /// The to encode from. + /// The configuration. + /// The pixel buffer to encode from. /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. /// The transparency frame. - private static ImageFrame DispatchAlphaToGreen(ImageFrame frame, Span alphaData) + private static ImageFrame DispatchAlphaToGreen(Configuration configuration, Buffer2DRegion frame, Span alphaData) where TPixel : unmanaged, IPixel { int width = frame.Width; int height = frame.Height; - ImageFrame alphaAsFrame = new ImageFrame(Configuration.Default, width, height); + ImageFrame alphaAsFrame = new(configuration, width, height); for (int y = 0; y < height; y++) { - Memory rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y); - Span pixelRow = rowBuffer.Span; + Memory rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y); + Span pixelRow = rowBuffer.Span; Span alphaRow = alphaData.Slice(y * width, width); + + // TODO: This can be probably simd optimized. for (int x = 0; x < width; x++) { // Leave A/R/B channels zero'd. - pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0); + pixelRow[x] = new Bgra32(0, alphaRow[x], 0, 0); } } @@ -106,12 +107,12 @@ internal static class AlphaEncoder /// The global configuration. /// The memory manager. /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. - private static IMemoryOwner ExtractAlphaChannel(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator) + private static IMemoryOwner ExtractAlphaChannel(Buffer2DRegion frame, Configuration configuration, MemoryAllocator memoryAllocator) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = frame.PixelBuffer; - int height = frame.Height; int width = frame.Width; + int height = frame.Height; + IMemoryOwner alphaDataBuffer = memoryAllocator.Allocate(width * height); Span alphaData = alphaDataBuffer.GetSpan(); @@ -120,7 +121,7 @@ internal static class AlphaEncoder for (int y = 0; y < height; y++) { - Span rowSpan = imageBuffer.DangerousGetRowSpan(y); + Span rowSpan = frame.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgba32(configuration, rowSpan, rgbaRow); int offset = y * width; for (int x = 0; x < width; x++) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 49b059b078..9ffda0f51f 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -100,9 +99,7 @@ internal abstract class BitWriterBase bool hasAnimation) { // Write file size later - long pos = RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc); - - Debug.Assert(pos is 4, "Stream should be written from position 0."); + RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc); // Write VP8X, header if necessary. bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation; diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs index 230f69c32d..c8c4a74a00 100644 --- a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs @@ -83,7 +83,7 @@ internal readonly struct WebpFrameData /// public WebpDisposalMethod DisposalMethod { get; } - public Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); + public Rectangle Bounds => new((int)this.X, (int)this.Y, (int)this.Width, (int)this.Height); /// /// Writes the animation frame() to the stream. @@ -107,8 +107,8 @@ internal readonly struct WebpFrameData long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.FrameData); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.X); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Y); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.X / 2); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Y / 2); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Duration); @@ -128,8 +128,8 @@ internal readonly struct WebpFrameData WebpFrameData data = new( dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer), - x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), - y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, + y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2, width: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index b9e2519fa4..518c09ff4d 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -240,7 +240,7 @@ internal class Vp8LEncoder : IDisposable public void EncodeHeader(Image image, Stream stream, bool hasAnimation) where TPixel : unmanaged, IPixel { - // Write bytes from the bitwriter buffer to the stream. + // Write bytes from the bit-writer buffer to the stream. ImageMetadata metadata = image.Metadata; metadata.SyncProfiles(); @@ -267,7 +267,7 @@ internal class Vp8LEncoder : IDisposable public void EncodeFooter(Image image, Stream stream) where TPixel : unmanaged, IPixel { - // Write bytes from the bitwriter buffer to the stream. + // Write bytes from the bit-writer buffer to the stream. ImageMetadata metadata = image.Metadata; ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; @@ -280,26 +280,25 @@ internal class Vp8LEncoder : IDisposable /// Encodes the image as lossless webp to the specified stream. /// /// The pixel format. - /// The to encode from. + /// The image frame to encode from. + /// The region of interest within the frame to encode. + /// The frame metadata. /// The to encode the image data to. /// Flag indicating, if an animation parameter is present. - public void Encode(ImageFrame frame, Stream stream, bool hasAnimation) + public void Encode(ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation) where TPixel : unmanaged, IPixel { - int width = frame.Width; - int height = frame.Height; - // Convert image pixels to bgra array. - bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height); + bool hasAlpha = this.ConvertPixelsToBgra(frame.PixelBuffer.GetRegion(bounds)); // Write the image size. - this.WriteImageSize(width, height); + this.WriteImageSize(bounds.Width, bounds.Height); // Write the non-trivial Alpha flag and lossless version. this.WriteAlphaAndVersion(hasAlpha); // Encode the main image stream. - this.EncodeStream(frame); + this.EncodeStream(bounds.Width, bounds.Height); this.bitWriter.Finish(); @@ -307,21 +306,18 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(frame); - - // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = new WebpFrameData( - 0, - 0, - (uint)frame.Width, - (uint)frame.Height, + (uint)bounds.Left, + (uint)bounds.Top, + (uint)bounds.Width, + (uint)bounds.Height, frameMetadata.FrameDelay, frameMetadata.BlendMethod, frameMetadata.DisposalMethod) .WriteHeaderTo(stream); } - // Write bytes from the bitwriter buffer to the stream. + // Write bytes from the bit-writer buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream); if (hasAnimation) @@ -334,12 +330,12 @@ internal class Vp8LEncoder : IDisposable /// Encodes the alpha image data using the webp lossless compression. /// /// The type of the pixel. - /// The to encode from. + /// The alpha-pixel data to encode from. /// The destination buffer to write the encoded alpha data to. /// The size of the compressed data in bytes. /// If the size of the data is the same as the pixel count, the compression would not yield in smaller data and is left uncompressed. /// - public int EncodeAlphaImageData(ImageFrame frame, IMemoryOwner alphaData) + public int EncodeAlphaImageData(Buffer2DRegion frame, IMemoryOwner alphaData) where TPixel : unmanaged, IPixel { int width = frame.Width; @@ -347,10 +343,10 @@ internal class Vp8LEncoder : IDisposable int pixelCount = width * height; // Convert image pixels to bgra array. - this.ConvertPixelsToBgra(frame, width, height); + this.ConvertPixelsToBgra(frame); // The image-stream will NOT contain any headers describing the image dimension, the dimension is already known. - this.EncodeStream(frame); + this.EncodeStream(width, height); this.bitWriter.Finish(); int size = this.bitWriter.NumBytes; if (size >= pixelCount) @@ -364,7 +360,7 @@ internal class Vp8LEncoder : IDisposable } /// - /// Writes the image size to the bitwriter buffer. + /// Writes the image size to the bit writer buffer. /// /// The input image width. /// The input image height. @@ -381,7 +377,7 @@ internal class Vp8LEncoder : IDisposable } /// - /// Writes a flag indicating if alpha channel is used and the VP8L version to the bitwriter buffer. + /// Writes a flag indicating if alpha channel is used and the VP8L version to the bit-writer buffer. /// /// Indicates if a alpha channel is present. private void WriteAlphaAndVersion(bool hasAlpha) @@ -393,14 +389,10 @@ internal class Vp8LEncoder : IDisposable /// /// Encodes the image stream using lossless webp format. /// - /// The pixel type. - /// The frame to encode. - private void EncodeStream(ImageFrame frame) - where TPixel : unmanaged, IPixel + /// The image frame width. + /// The image frame height. + private void EncodeStream(int width, int height) { - int width = frame.Width; - int height = frame.Height; - Span bgra = this.Bgra.GetSpan(); Span encodedData = this.EncodedData.GetSpan(); bool lowEffort = this.method == 0; @@ -508,23 +500,20 @@ internal class Vp8LEncoder : IDisposable /// Converts the pixels of the image to bgra. /// /// The type of the pixels. - /// The frame to convert. - /// The width of the image. - /// The height of the image. + /// The frame pixel buffer to convert. /// true, if the image is non opaque. - private bool ConvertPixelsToBgra(ImageFrame frame, int width, int height) + private bool ConvertPixelsToBgra(Buffer2DRegion pixels) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = frame.PixelBuffer; bool nonOpaque = false; Span bgra = this.Bgra.GetSpan(); Span bgraBytes = MemoryMarshal.Cast(bgra); - int widthBytes = width * 4; - for (int y = 0; y < height; y++) + int widthBytes = pixels.Width * 4; + for (int y = 0; y < pixels.Height; y++) { - Span rowSpan = imageBuffer.DangerousGetRowSpan(y); + Span rowSpan = pixels.DangerousGetRowSpan(y); Span rowBytes = bgraBytes.Slice(y * widthBytes, widthBytes); - PixelOperations.Instance.ToBgra32Bytes(this.configuration, rowSpan, rowBytes, width); + PixelOperations.Instance.ToBgra32Bytes(this.configuration, rowSpan, rowBytes, pixels.Width); if (!nonOpaque) { Span rowBgra = MemoryMarshal.Cast(rowBytes); diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index e6148a0660..2b74c300a4 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -351,44 +351,53 @@ internal class Vp8Encoder : IDisposable } /// - /// Encodes the image to the specified stream from the . + /// Encodes the animated image frame to the specified stream. /// /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void EncodeAnimation(ImageFrame frame, Stream stream) + /// The image frame to encode from. + /// The stream to encode the image data to. + /// The region of interest within the frame to encode. + /// The frame metadata. + public void EncodeAnimation(ImageFrame frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata) where TPixel : unmanaged, IPixel => - this.Encode(frame, stream, true, null); + this.Encode(stream, frame, bounds, frameMetadata, true, null); /// - /// Encodes the image to the specified stream from the . + /// Encodes the static image frame to the specified stream. /// /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void EncodeStatic(Image image, Stream stream) - where TPixel : unmanaged, IPixel => - this.Encode(image.Frames.RootFrame, stream, false, image); + /// The stream to encode the image data to. + /// The image to encode from. + public void EncodeStatic(Stream stream, Image image) + where TPixel : unmanaged, IPixel + { + ImageFrame frame = image.Frames.RootFrame; + this.Encode(stream, frame, image.Bounds, WebpCommonUtils.GetWebpFrameMetadata(frame), false, image); + } /// - /// Encodes the image to the specified stream from the . + /// Encodes the image to the specified stream. /// /// The pixel format. - /// The to encode from. - /// The to encode the image data to. + /// The stream to encode the image data to. + /// The image frame to encode from. + /// The region of interest within the frame to encode. + /// The frame metadata. /// Flag indicating, if an animation parameter is present. - /// The to encode from. - private void Encode(ImageFrame frame, Stream stream, bool hasAnimation, Image image) + /// The image to encode from. + private void Encode(Stream stream, ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image image) where TPixel : unmanaged, IPixel { - int width = frame.Width; - int height = frame.Height; + int width = bounds.Width; + int height = bounds.Height; int pixelCount = width * height; Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - bool hasAlpha = YuvConversion.ConvertRgbToYuv(frame, this.configuration, this.memoryAllocator, y, u, v); + + Buffer2DRegion pixels = frame.PixelBuffer.GetRegion(bounds); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(pixels, this.configuration, this.memoryAllocator, y, u, v); if (!hasAnimation) { @@ -456,7 +465,7 @@ internal class Vp8Encoder : IDisposable { // TODO: This can potentially run in an separate task. encodedAlphaData = AlphaEncoder.EncodeAlpha( - frame, + pixels, this.configuration, this.memoryAllocator, this.skipMetadata, @@ -477,14 +486,11 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(frame); - - // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = new WebpFrameData( - 0, - 0, - (uint)frame.Width, - (uint)frame.Height, + (uint)bounds.X, + (uint)bounds.Y, + (uint)bounds.Width, + (uint)bounds.Height, frameMetadata.FrameDelay, frameMetadata.BlendMethod, frameMetadata.DisposalMethod) diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index d669a37b74..f8e664ed03 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -259,7 +259,7 @@ internal static class YuvConversion } /// - /// Converts the RGB values of the image to YUV. + /// Converts the pixel values of the image to YUV. /// /// The pixel type of the image. /// The frame to convert. @@ -269,12 +269,11 @@ internal static class YuvConversion /// Span to store the u component of the image. /// Span to store the v component of the image. /// true, if the image contains alpha data. - public static bool ConvertRgbToYuv(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) + public static bool ConvertRgbToYuv(Buffer2DRegion frame, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = frame.PixelBuffer; - int width = imageBuffer.Width; - int height = imageBuffer.Height; + int width = frame.Width; + int height = frame.Height; int uvWidth = (width + 1) >> 1; // Temporary storage for accumulated R/G/B values during conversion to U/V. @@ -289,8 +288,8 @@ internal static class YuvConversion bool hasAlpha = false; for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2) { - Span rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex); - Span nextRowSpan = imageBuffer.DangerousGetRowSpan(rowIndex + 1); + Span rowSpan = frame.DangerousGetRowSpan(rowIndex); + Span nextRowSpan = frame.DangerousGetRowSpan(rowIndex + 1); PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0); PixelOperations.Instance.ToBgra32(configuration, nextRowSpan, bgraRow1); @@ -320,7 +319,7 @@ internal static class YuvConversion // Extra last row. if ((height & 1) != 0) { - Span rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex); + Span rowSpan = frame.DangerousGetRowSpan(rowIndex); PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0); ConvertRgbaToY(bgraRow0, y[(rowIndex * width)..], width); diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index f081cfcd89..d85096c2e8 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -253,7 +253,7 @@ internal class WebpAnimationDecoder : IDisposable private Buffer2D DecodeImageFrameData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - ImageFrame decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height); + ImageFrame decodedFrame = new(this.configuration, (int)frameData.Width, (int)frameData.Height); try { diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 8374870473..7357e097c9 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Memory; @@ -129,6 +130,8 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals if (lossless) { + bool hasAnimation = image.Frames.Count > 1; + using Vp8LEncoder encoder = new( this.memoryAllocator, this.configuration, @@ -141,17 +144,34 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.nearLossless, this.nearLosslessQuality); - bool hasAnimation = image.Frames.Count > 1; encoder.EncodeHeader(image, stream, hasAnimation); + + // Encode the first frame. + ImageFrame previousFrame = image.Frames.RootFrame; + WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame); + encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation); + if (hasAnimation) { - foreach (ImageFrame imageFrame in image.Frames) + WebpDisposalMethod previousDisposal = frameMetadata.DisposalMethod; + + // Encode additional frames + // This frame is reused to store de-duplicated pixel buffers. + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + + for (int i = 1; i < image.Frames.Count; i++) { - using Vp8LEncoder enc = new( + ImageFrame currentFrame = image.Frames[i]; + frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame); + + ImageFrame? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame; + (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero); + + using Vp8LEncoder animatedEncoder = new( this.memoryAllocator, this.configuration, - image.Width, - image.Height, + bounds.Width, + bounds.Height, this.quality, this.skipMetadata, this.method, @@ -159,13 +179,12 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.nearLossless, this.nearLosslessQuality); - enc.Encode(imageFrame, stream, true); + animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation); + + previousFrame = currentFrame; + previousDisposal = frameMetadata.DisposalMethod; } } - else - { - encoder.Encode(image.Frames.RootFrame, stream, false); - } encoder.EncodeFooter(image, stream); } @@ -183,17 +202,36 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); + if (image.Frames.Count > 1) { + // TODO: What about alpha here? encoder.EncodeHeader(image, stream, false, true); - foreach (ImageFrame imageFrame in image.Frames) + // Encode the first frame. + ImageFrame previousFrame = image.Frames.RootFrame; + WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame); + WebpDisposalMethod previousDisposal = frameMetadata.DisposalMethod; + + encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata); + + // Encode additional frames + // This frame is reused to store de-duplicated pixel buffers. + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + + for (int i = 1; i < image.Frames.Count; i++) { - using Vp8Encoder enc = new( + ImageFrame currentFrame = image.Frames[i]; + frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame); + + ImageFrame? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame; + (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero); + + using Vp8Encoder animatedEncoder = new( this.memoryAllocator, this.configuration, - image.Width, - image.Height, + bounds.Width, + bounds.Height, this.quality, this.skipMetadata, this.method, @@ -202,12 +240,15 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.spatialNoiseShaping, this.alphaCompression); - enc.EncodeAnimation(imageFrame, stream); + animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata); + + previousFrame = currentFrame; + previousDisposal = frameMetadata.DisposalMethod; } } else { - encoder.EncodeStatic(image, stream); + encoder.EncodeStatic(stream, image); } encoder.EncodeFooter(image, stream); diff --git a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs index 433b280bc3..3ae6601b18 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Memory; @@ -143,7 +142,7 @@ public class YuvConversionTests }; // act - YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v); + YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame.PixelBuffer.GetRegion(), config, memoryAllocator, y, u, v); // assert Assert.True(expectedY.AsSpan().SequenceEqual(y)); @@ -249,7 +248,7 @@ public class YuvConversionTests }; // act - YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v); + YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame.PixelBuffer.GetRegion(), config, memoryAllocator, y, u, v); // assert Assert.True(expectedY.AsSpan().SequenceEqual(y));