From 6f52a0d13c38bceecb9187534996cf21b83f1483 Mon Sep 17 00:00:00 2001 From: Poker Date: Sat, 21 Oct 2023 10:40:50 +0800 Subject: [PATCH 01/19] Preparation --- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 42 ++++----- .../Formats/Webp/AnimationFrameData.cs | 43 +++++++++ .../Formats/Webp/BitWriter/BitWriterBase.cs | 93 +++++++++++++------ .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 11 +-- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 11 ++- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 80 ++++++++-------- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +- .../Formats/Webp/Lossy/YuvConversion.cs | 6 +- .../Formats/Webp/WebpAnimationDecoder.cs | 42 +-------- .../Formats/Webp/WebpAnimationEncoder.cs | 12 +++ .../Formats/Webp/WebpChunkParsingUtils.cs | 52 +++++++---- src/ImageSharp/Formats/Webp/WebpChunkType.cs | 9 ++ src/ImageSharp/Formats/Webp/WebpConstants.cs | 33 ------- .../Formats/Webp/WebpDecoderCore.cs | 5 - .../Formats/WebP/YuvConversionTests.cs | 4 +- 15 files changed, 248 insertions(+), 199 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index 596715b205..a18d44fde4 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -19,7 +19,7 @@ internal static class AlphaEncoder /// Data is either compressed as lossless webp image or uncompressed. /// /// The pixel format. - /// The to encode from. + /// The to encode from. /// The global configuration. /// The memory manager. /// Whether to skip metadata encoding. @@ -27,7 +27,7 @@ internal static class AlphaEncoder /// The size in bytes of the alpha data. /// The encoded alpha data. public static IMemoryOwner EncodeAlpha( - Image image, + ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator, bool skipMetadata, @@ -35,9 +35,9 @@ public static IMemoryOwner EncodeAlpha( out int size) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - IMemoryOwner alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator); + int width = frame.Width; + int height = frame.Height; + IMemoryOwner alphaData = ExtractAlphaChannel(frame, configuration, memoryAllocator); if (compress) { @@ -58,9 +58,9 @@ public static IMemoryOwner EncodeAlpha( // 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 Image alphaAsImage = DispatchAlphaToGreen(image, alphaData.GetSpan()); + using ImageFrame alphaAsFrame = DispatchAlphaToGreen(frame, alphaData.GetSpan()); - size = lossLessEncoder.EncodeAlphaImageData(alphaAsImage, alphaData); + size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame, alphaData); return alphaData; } @@ -73,45 +73,45 @@ public static IMemoryOwner EncodeAlpha( /// Store the transparency in the green channel. /// /// The pixel format. - /// The to encode from. + /// The to encode from. /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. - /// The transparency image. - private static Image DispatchAlphaToGreen(Image image, Span alphaData) + /// The transparency frame. + private static ImageFrame DispatchAlphaToGreen(ImageFrame frame, Span alphaData) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - Image alphaAsImage = new(width, height); + int width = frame.Width; + int height = frame.Height; + ImageFrame alphaAsFrame = new(Configuration.Default, width, height); for (int y = 0; y < height; y++) { - Memory rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y); + Memory rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y); Span pixelRow = rowBuffer.Span; Span alphaRow = alphaData.Slice(y * width, width); 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(0, alphaRow[x], 0, 0); } } - return alphaAsImage; + return alphaAsFrame; } /// /// Extract the alpha data of the image. /// /// The pixel format. - /// The to encode from. + /// The to encode from. /// 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(Image image, Configuration configuration, MemoryAllocator memoryAllocator) + private static IMemoryOwner ExtractAlphaChannel(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; - int height = image.Height; - int width = image.Width; + Buffer2D imageBuffer = frame.PixelBuffer; + int height = frame.Height; + int width = frame.Width; IMemoryOwner alphaDataBuffer = memoryAllocator.Allocate(width * height); Span alphaData = alphaDataBuffer.GetSpan(); diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs index 714ec428ec..3400fef17d 100644 --- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs +++ b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; + namespace SixLabors.ImageSharp.Formats.Webp; internal struct AnimationFrameData @@ -10,6 +12,11 @@ internal struct AnimationFrameData /// public uint DataSize; + /// + /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. + /// + public const uint HeaderSize = 16; + /// /// The X coordinate of the upper left corner of the frame is Frame X * 2. /// @@ -45,4 +52,40 @@ internal struct AnimationFrameData /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// public AnimationDisposalMethod DisposalMethod; + + /// + /// Reads the animation frame header. + /// + /// The stream to read from. + /// Animation frame data. + public static AnimationFrameData Parse(BufferedReadStream stream) + { + Span buffer = stackalloc byte[4]; + + AnimationFrameData data = new() + { + DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), + + // 3 bytes for the X coordinate of the upper left corner of the frame. + X = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + + // 3 bytes for the Y coordinate of the upper left corner of the frame. + Y = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + + // Frame width Minus One. + Width = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + + // Frame height Minus One. + Height = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + + // Frame duration. + Duration = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + }; + + byte flags = (byte)stream.ReadByte(); + data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; + data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; + + return data; + } } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index ab78d18604..d7787b3a00 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Drawing; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -92,7 +93,7 @@ protected void WriteRiffHeader(Stream stream, uint riffSize) { stream.Write(WebpConstants.RiffFourCc); BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize); - stream.Write(this.scratchBuffer.Span.Slice(0, 4)); + stream.Write(this.scratchBuffer.Span[..4]); stream.Write(WebpConstants.WebpHeader); } @@ -129,7 +130,7 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); uint size = (uint)metadataBytes.Length; - Span buf = this.scratchBuffer.Span.Slice(0, 4); + Span buf = this.scratchBuffer.Span[..4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -143,6 +144,61 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh } } + /// + /// Writes the color profile() to the stream. + /// + /// The stream to write to. + /// The color profile bytes. + protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); + + /// + /// Writes the animation parameter() to the stream. + /// + /// The stream to write to. + /// + /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + /// The number of times to loop the animation. If it is 0, this means infinitely. + protected void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) + { + Span buf = this.scratchBuffer.Span[..4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, background); + stream.Write(buf); + BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount); + stream.Write(buf[..2]); + } + + /// + /// Writes the animation frame() to the stream. + /// + /// The stream to write to. + /// Animation frame data. + /// Frame data. + protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, byte[] data) + { + uint size = AnimationFrameData.HeaderSize + (uint)data.Length; + Span buf = this.scratchBuffer.Span[..4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); + stream.Write(buf); + BinaryPrimitives.WriteUInt32BigEndian(buf, size); + stream.Write(buf); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); + byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); + stream.WriteByte(flag); + stream.Write(data); + } + /// /// Writes the alpha chunk to the stream. /// @@ -152,7 +208,7 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { uint size = (uint)dataBytes.Length + 1; - Span buf = this.scratchBuffer.Span.Slice(0, 4); + Span buf = this.scratchBuffer.Span[..4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -161,7 +217,7 @@ protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDa byte flags = 0; if (alphaDataIsCompressed) { - flags |= 1; + flags = 1; } stream.WriteByte(flags); @@ -174,30 +230,6 @@ protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDa } } - /// - /// Writes the color profile to the stream. - /// - /// The stream to write to. - /// The color profile bytes. - protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) - { - uint size = (uint)iccProfileBytes.Length; - - Span buf = this.scratchBuffer.Span.Slice(0, 4); - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, size); - stream.Write(buf); - - stream.Write(iccProfileBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } - } - /// /// Writes a VP8X header to the stream. /// @@ -246,8 +278,9 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfi flags |= 32; } - Span buf = this.scratchBuffer.Span.Slice(0, 4); - stream.Write(WebpConstants.Vp8XMagicBytes); + Span buf = this.scratchBuffer.Span[..4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X); + stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, flags); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 5b4eab64a3..597ecef42a 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -701,12 +701,11 @@ private void WriteWebpHeaders( private void WriteVp8Header(Stream stream, uint size) { - Span vp8ChunkHeader = stackalloc byte[WebpConstants.ChunkHeaderSize]; - - WebpConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader); - BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader[4..], size); - - stream.Write(vp8ChunkHeader); + Span buf = stackalloc byte[WebpConstants.TagSize]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); } private void WriteFrameHeader(Stream stream, uint size0) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 9dc7912392..a042f68968 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -105,7 +105,7 @@ public Vp8LBitWriter Clone() { byte[] clonedBuffer = new byte[this.Buffer.Length]; System.Buffer.BlockCopy(this.Buffer, 0, clonedBuffer, 0, this.cur); - return new Vp8LBitWriter(clonedBuffer, this.bits, this.used, this.cur); + return new(clonedBuffer, this.bits, this.used, this.cur); } /// @@ -186,12 +186,13 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, X } // Write magic bytes indicating its a lossless webp. - stream.Write(WebpConstants.Vp8LMagicBytes); + Span scratchBuffer = stackalloc byte[WebpConstants.TagSize]; + BinaryPrimitives.WriteUInt32BigEndian(scratchBuffer, (uint)WebpChunkType.Vp8L); + stream.Write(scratchBuffer); // Write Vp8 Header. - Span scratchBuffer = stackalloc byte[8]; BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size); - stream.Write(scratchBuffer.Slice(0, 4)); + stream.Write(scratchBuffer); stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); // Write the encoded bytes of the image to the stream. @@ -226,7 +227,7 @@ private void PutBitsFlushBits() Span scratchBuffer = stackalloc byte[8]; BinaryPrimitives.WriteUInt64LittleEndian(scratchBuffer, this.bits); - scratchBuffer.Slice(0, 4).CopyTo(this.Buffer.AsSpan(this.cur)); + scratchBuffer[..4].CopyTo(this.Buffer.AsSpan(this.cur)); this.cur += WriterBytes; this.bits >>= WriterBits; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 469e4c9ab0..9b82cc5983 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -254,7 +254,7 @@ public void Encode(Image image, Stream stream) XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; // Convert image pixels to bgra array. - bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); + bool hasAlpha = this.ConvertPixelsToBgra(image.Frames.RootFrame, width, height); // Write the image size. this.WriteImageSize(width, height); @@ -263,7 +263,7 @@ public void Encode(Image image, Stream stream) this.WriteAlphaAndVersion(hasAlpha); // Encode the main image stream. - this.EncodeStream(image); + this.EncodeStream(image.Frames.RootFrame); // Write bytes from the bitwriter buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); @@ -273,23 +273,23 @@ public void Encode(Image image, Stream stream) /// Encodes the alpha image data using the webp lossless compression. /// /// The type of the pixel. - /// The to encode from. + /// The 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(Image image, IMemoryOwner alphaData) + public int EncodeAlphaImageData(ImageFrame frame, IMemoryOwner alphaData) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + int width = frame.Width; + int height = frame.Height; int pixelCount = width * height; // Convert image pixels to bgra array. - this.ConvertPixelsToBgra(image, width, height); + this.ConvertPixelsToBgra(frame, width, height); // The image-stream will NOT contain any headers describing the image dimension, the dimension is already known. - this.EncodeStream(image); + this.EncodeStream(frame); this.bitWriter.Finish(); int size = this.bitWriter.NumBytes(); if (size >= pixelCount) @@ -333,12 +333,12 @@ private void WriteAlphaAndVersion(bool hasAlpha) /// Encodes the image stream using lossless webp format. /// /// The pixel type. - /// The image to encode. - private void EncodeStream(Image image) + /// The frame to encode. + private void EncodeStream(ImageFrame frame) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + int width = frame.Width; + int height = frame.Height; Span bgra = this.Bgra.GetSpan(); Span encodedData = this.EncodedData.GetSpan(); @@ -447,14 +447,14 @@ private void EncodeStream(Image image) /// Converts the pixels of the image to bgra. /// /// The type of the pixels. - /// The image to convert. + /// The frame to convert. /// The width of the image. /// The height of the image. /// true, if the image is non opaque. - private bool ConvertPixelsToBgra(Image image, int width, int height) + private bool ConvertPixelsToBgra(ImageFrame frame, int width, int height) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; + Buffer2D imageBuffer = frame.PixelBuffer; bool nonOpaque = false; Span bgra = this.Bgra.GetSpan(); Span bgraBytes = MemoryMarshal.Cast(bgra); @@ -1149,35 +1149,41 @@ private EntropyIx AnalyzeEntropy(ReadOnlySpan bgra, int width, int height, entropyComp[j] = bitEntropy.BitsEntropyRefine(); } - entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] + - entropyComp[(int)HistoIx.HistoRed] + - entropyComp[(int)HistoIx.HistoGreen] + - entropyComp[(int)HistoIx.HistoBlue]; - entropy[(int)EntropyIx.Spatial] = entropyComp[(int)HistoIx.HistoAlphaPred] + - entropyComp[(int)HistoIx.HistoRedPred] + - entropyComp[(int)HistoIx.HistoGreenPred] + - entropyComp[(int)HistoIx.HistoBluePred]; - entropy[(int)EntropyIx.SubGreen] = entropyComp[(int)HistoIx.HistoAlpha] + - entropyComp[(int)HistoIx.HistoRedSubGreen] + - entropyComp[(int)HistoIx.HistoGreen] + - entropyComp[(int)HistoIx.HistoBlueSubGreen]; - entropy[(int)EntropyIx.SpatialSubGreen] = entropyComp[(int)HistoIx.HistoAlphaPred] + - entropyComp[(int)HistoIx.HistoRedPredSubGreen] + - entropyComp[(int)HistoIx.HistoGreenPred] + - entropyComp[(int)HistoIx.HistoBluePredSubGreen]; + entropy[(int)EntropyIx.Direct] = + entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRed] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlue]; + entropy[(int)EntropyIx.Spatial] = + entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPred] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePred]; + entropy[(int)EntropyIx.SubGreen] = + entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRedSubGreen] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlueSubGreen]; + entropy[(int)EntropyIx.SpatialSubGreen] = + entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPredSubGreen] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePredSubGreen]; entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette]; // When including transforms, there is an overhead in bits from // storing them. This overhead is small but matters for small images. // For spatial, there are 14 transformations. - entropy[(int)EntropyIx.Spatial] += LosslessUtils.SubSampleSize(width, transformBits) * - LosslessUtils.SubSampleSize(height, transformBits) * - LosslessUtils.FastLog2(14); + entropy[(int)EntropyIx.Spatial] += + LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(14); // For color transforms: 24 as only 3 channels are considered in a ColorTransformElement. - entropy[(int)EntropyIx.SpatialSubGreen] += LosslessUtils.SubSampleSize(width, transformBits) * - LosslessUtils.SubSampleSize(height, transformBits) * - LosslessUtils.FastLog2(24); + entropy[(int)EntropyIx.SpatialSubGreen] += + LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(24); // For palettes, add the cost of storing the palette. // We empirically estimate the cost of a compressed entry as 8 bits. diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index f17d965e87..ce5d3bac11 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -323,7 +323,7 @@ public void Encode(Image image, Stream stream) Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - bool hasAlpha = YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, this.configuration, this.memoryAllocator, y, u, v); int yStride = width; int uvStride = (yStride + 1) >> 1; @@ -393,7 +393,7 @@ public void Encode(Image image, Stream stream) { // TODO: This can potentially run in an separate task. encodedAlphaData = AlphaEncoder.EncodeAlpha( - image, + image.Frames.RootFrame, this.configuration, this.memoryAllocator, this.skipMetadata, diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index 8ef7fe9cba..d669a37b74 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -262,17 +262,17 @@ private static void PackAndStore(Vector128 a, Vector128 b, Vector128 /// Converts the RGB values of the image to YUV. /// /// The pixel type of the image. - /// The image to convert. + /// The frame to convert. /// The global configuration. /// The memory allocator. /// Span to store the luma component of the image. /// 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(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) + public static bool ConvertRgbToYuv(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; + Buffer2D imageBuffer = frame.PixelBuffer; int width = imageBuffer.Width; int height = imageBuffer.Height; int uvWidth = (width + 1) >> 1; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 90c9c70b26..65f654dddc 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -138,7 +138,7 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat private uint ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, uint width, uint height, Color backgroundColor) where TPixel : unmanaged, IPixel { - AnimationFrameData frameData = this.ReadFrameHeader(stream); + AnimationFrameData frameData = AnimationFrameData.Parse(stream); long streamStartPosition = stream.Position; Span buffer = stackalloc byte[4]; @@ -173,7 +173,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima ImageFrame imageFrame; if (previousFrame is null) { - image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); + image = new(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); @@ -258,7 +258,7 @@ private Buffer2D DecodeImageData(AnimationFrameData frameData, W try { - Buffer2D pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer; + Buffer2D pixelBufferDecoded = decodedImage.GetRootFramePixelBuffer(); if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); @@ -353,42 +353,6 @@ private void RestoreToBackground(ImageFrame imageFrame, Color ba pixelRegion.Fill(backgroundPixel); } - /// - /// Reads the animation frame header. - /// - /// The stream to read from. - /// Animation frame data. - private AnimationFrameData ReadFrameHeader(BufferedReadStream stream) - { - Span buffer = stackalloc byte[4]; - - AnimationFrameData data = new() - { - DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), - - // 3 bytes for the X coordinate of the upper left corner of the frame. - X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer), - - // 3 bytes for the Y coordinate of the upper left corner of the frame. - Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer), - - // Frame width Minus One. - Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1, - - // Frame height Minus One. - Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1, - - // Frame duration. - Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) - }; - - byte flags = (byte)stream.ReadByte(); - data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; - data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; - - return data; - } - /// public void Dispose() => this.alphaData?.Dispose(); } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs new file mode 100644 index 0000000000..bfa64b6797 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp; + +/// +/// Encoder for animated webp images. +/// +public class WebpAnimationEncoder +{ + // 可能不需要这个屌东西 +} diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index a7ae474e46..becd622e17 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -2,13 +2,12 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Drawing; using SixLabors.ImageSharp.Formats.Webp.BitReader; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp; @@ -91,7 +90,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer); uint width = tmp & 0x3fff; sbyte xScale = (sbyte)(tmp >> 6); - tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2)); + tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..]); uint height = tmp & 0x3fff; sbyte yScale = (sbyte)(tmp >> 6); remaining -= 7; @@ -105,14 +104,14 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe WebpThrowHelper.ThrowImageFormatException("bad partition length"); } - var vp8FrameHeader = new Vp8FrameHeader() + Vp8FrameHeader vp8FrameHeader = new() { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; - var bitReader = new Vp8BitReader( + Vp8BitReader bitReader = new( stream, remaining, memoryAllocator, @@ -121,7 +120,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe Remaining = remaining }; - return new WebpImageInfo() + return new() { Width = width, Height = height, @@ -145,7 +144,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); - var bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); + Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); @@ -174,7 +173,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); } - return new WebpImageInfo() + return new() { Width = width, Height = height, @@ -231,13 +230,13 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span } // 3 bytes for the width. - uint width = ReadUnsignedInt24Bit(stream, buffer) + 1; + uint width = ReadUInt24LittleEndian(stream, buffer) + 1; // 3 bytes for the height. - uint height = ReadUnsignedInt24Bit(stream, buffer) + 1; + uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - var info = new WebpImageInfo() + WebpImageInfo info = new() { Width = width, Height = height, @@ -253,7 +252,7 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span /// The stream to read from. /// The buffer to store the read data into. /// A unsigned 24 bit integer. - public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span buffer) + public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span buffer) { if (stream.Read(buffer, 0, 3) == 3) { @@ -261,7 +260,28 @@ public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span bu return BinaryPrimitives.ReadUInt32LittleEndian(buffer); } - throw new ImageFormatException("Invalid Webp data, could not read unsigned integer."); + throw new ImageFormatException("Invalid Webp data, could not read unsigned 24 bit integer."); + } + + /// + /// Writes a unsigned 24 bit integer. + /// + /// The stream to read from. + /// The uint24 data to write. + public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data) + { + if (data >= 1 << 24) + { + throw new InvalidDataException($"Invalid data, {data} is not a unsigned 24 bit integer."); + } + + uint* ptr = &data; + byte* b = (byte*)ptr; + + // Write the data in little endian. + stream.WriteByte(b[0]); + stream.WriteByte(b[1]); + stream.WriteByte(b[2]); } /// @@ -298,7 +318,7 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span if (stream.Read(buffer) == 4) { - var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); + WebpChunkType chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); return chunkType; } @@ -335,7 +355,7 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType if (metadata.ExifProfile != null) { - metadata.ExifProfile = new ExifProfile(exifData); + metadata.ExifProfile = new(exifData); } break; @@ -349,7 +369,7 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType if (metadata.XmpProfile != null) { - metadata.XmpProfile = new XmpProfile(xmpData); + metadata.XmpProfile = new(xmpData); } break; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkType.cs b/src/ImageSharp/Formats/Webp/WebpChunkType.cs index 802d7f7288..5836dc6c09 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkType.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkType.cs @@ -12,45 +12,54 @@ internal enum WebpChunkType : uint /// /// Header signaling the use of the VP8 format. /// + /// VP8 (Single) Vp8 = 0x56503820U, /// /// Header signaling the image uses lossless encoding. /// + /// VP8L (Single) Vp8L = 0x5650384CU, /// /// Header for a extended-VP8 chunk. /// + /// VP8X (Single) Vp8X = 0x56503858U, /// /// Chunk contains information about the alpha channel. /// + /// ALPH (Single) Alpha = 0x414C5048U, /// /// Chunk which contains a color profile. /// + /// ICCP (Single) Iccp = 0x49434350U, /// /// Chunk which contains EXIF metadata about the image. /// + /// EXIF (Single) Exif = 0x45584946U, /// /// Chunk contains XMP metadata about the image. /// + /// XMP (Single) Xmp = 0x584D5020U, /// /// For an animated image, this chunk contains the global parameters of the animation. /// + /// ANIM (Single) AnimationParameter = 0x414E494D, /// /// For animated images, this chunk contains information about a single frame. If the Animation flag is not set, then this chunk SHOULD NOT be present. /// + /// ANMF (Multiple) Animation = 0x414E4D46, } diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index d105d8dd62..1433772757 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -33,39 +33,6 @@ internal static class WebpConstants /// public const byte Vp8LHeaderMagicByte = 0x2F; - /// - /// Signature bytes identifying a lossy image. - /// - public static readonly byte[] Vp8MagicBytes = - { - 0x56, // V - 0x50, // P - 0x38, // 8 - 0x20 // ' ' - }; - - /// - /// Signature bytes identifying a lossless image. - /// - public static readonly byte[] Vp8LMagicBytes = - { - 0x56, // V - 0x50, // P - 0x38, // 8 - 0x4C // L - }; - - /// - /// Signature bytes identifying a VP8X header. - /// - public static readonly byte[] Vp8XMagicBytes = - { - 0x56, // V - 0x50, // P - 0x38, // 8 - 0x58 // X - }; - /// /// The header bytes identifying RIFF file. /// diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 8832ac1068..63d3e1aead 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -93,11 +93,6 @@ public Image Decode(BufferedReadStream stream, CancellationToken return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } - if (this.webImageInfo.Features is { Animation: true }) - { - WebpThrowHelper.ThrowNotSupportedException("Animations are not supported"); - } - image = new Image(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) diff --git a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs index 9b03a447a9..433b280bc3 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs @@ -143,7 +143,7 @@ public void ConvertRgbToYuv_Works(TestImageProvider provider) }; // act - YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); + YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v); // assert Assert.True(expectedY.AsSpan().SequenceEqual(y)); @@ -249,7 +249,7 @@ public void ConvertRgbToYuv_WithAlpha_Works(TestImageProvider pr }; // act - YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); + YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v); // assert Assert.True(expectedY.AsSpan().SequenceEqual(y)); From 62ab3a1eef3c9e9af4af683f512471f81bf3e5d4 Mon Sep 17 00:00:00 2001 From: Poker Date: Sun, 22 Oct 2023 22:54:33 +0800 Subject: [PATCH 02/19] refactor --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 11 +- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 104 +++++++++--------- .../Formats/Webp/Lossy/Vp8EncIterator.cs | 5 + .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +- .../Formats/Webp/WebpAnimationDecoder.cs | 5 + 5 files changed, 73 insertions(+), 56 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index d7787b3a00..4252f895b8 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; -using System.Drawing; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -181,7 +180,7 @@ protected void WriteAnimationParameter(Stream stream, uint background, ushort lo /// The stream to write to. /// Animation frame data. /// Frame data. - protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, byte[] data) + protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, Span data) { uint size = AnimationFrameData.HeaderSize + (uint)data.Length; Span buf = this.scratchBuffer.Span[..4]; @@ -260,6 +259,14 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfi flags |= 8; } + /* + if (isAnimated) + { + // Set animated flag. + flags |= 2; + } + */ + if (xmpProfile != null) { // Set xmp bit. diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 597ecef42a..cd84f109eb 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -116,7 +116,7 @@ public int PutCoeffs(int ctx, Vp8Residual residual) else { this.PutBit(v >= 9, 165); - this.PutBit(!((v & 1) != 0), 145); + this.PutBit((v & 1) == 0, 145); } } else @@ -462,7 +462,7 @@ public void WriteEncodedImageToStream( Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc); // Partition #0 with header and partition sizes. - uint size0 = this.GeneratePartition0(bitWriterPartZero); + uint size0 = bitWriterPartZero.GeneratePartition0(); uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; vp8Size += numBytes; @@ -495,47 +495,47 @@ public void WriteEncodedImageToStream( } } - private uint GeneratePartition0(Vp8BitWriter bitWriter) + private uint GeneratePartition0() { - bitWriter.PutBitUniform(0); // colorspace - bitWriter.PutBitUniform(0); // clamp type + this.PutBitUniform(0); // colorspace + this.PutBitUniform(0); // clamp type - this.WriteSegmentHeader(bitWriter); - this.WriteFilterHeader(bitWriter); + this.WriteSegmentHeader(); + this.WriteFilterHeader(); - bitWriter.PutBits(0, 2); + this.PutBits(0, 2); - this.WriteQuant(bitWriter); - bitWriter.PutBitUniform(0); - this.WriteProbas(bitWriter); - this.CodeIntraModes(bitWriter); + this.WriteQuant(); + this.PutBitUniform(0); + this.WriteProbas(); + this.CodeIntraModes(); - bitWriter.Finish(); + this.Finish(); - return (uint)bitWriter.NumBytes(); + return (uint)this.NumBytes(); } - private void WriteSegmentHeader(Vp8BitWriter bitWriter) + private void WriteSegmentHeader() { Vp8EncSegmentHeader hdr = this.enc.SegmentHeader; Vp8EncProba proba = this.enc.Proba; - if (bitWriter.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0) + if (this.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0) { // We always 'update' the quant and filter strength values. int updateData = 1; - bitWriter.PutBitUniform(hdr.UpdateMap ? 1 : 0); - if (bitWriter.PutBitUniform(updateData) != 0) + this.PutBitUniform(hdr.UpdateMap ? 1 : 0); + if (this.PutBitUniform(updateData) != 0) { // We always use absolute values, not relative ones. - bitWriter.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.) + this.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.) for (int s = 0; s < WebpConstants.NumMbSegments; ++s) { - bitWriter.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7); + this.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7); } for (int s = 0; s < WebpConstants.NumMbSegments; ++s) { - bitWriter.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6); + this.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6); } } @@ -543,50 +543,50 @@ private void WriteSegmentHeader(Vp8BitWriter bitWriter) { for (int s = 0; s < 3; ++s) { - if (bitWriter.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0) + if (this.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0) { - bitWriter.PutBits(proba.Segments[s], 8); + this.PutBits(proba.Segments[s], 8); } } } } } - private void WriteFilterHeader(Vp8BitWriter bitWriter) + private void WriteFilterHeader() { Vp8FilterHeader hdr = this.enc.FilterHeader; bool useLfDelta = hdr.I4x4LfDelta != 0; - bitWriter.PutBitUniform(hdr.Simple ? 1 : 0); - bitWriter.PutBits((uint)hdr.FilterLevel, 6); - bitWriter.PutBits((uint)hdr.Sharpness, 3); - if (bitWriter.PutBitUniform(useLfDelta ? 1 : 0) != 0) + this.PutBitUniform(hdr.Simple ? 1 : 0); + this.PutBits((uint)hdr.FilterLevel, 6); + this.PutBits((uint)hdr.Sharpness, 3); + if (this.PutBitUniform(useLfDelta ? 1 : 0) != 0) { // '0' is the default value for i4x4LfDelta at frame #0. bool needUpdate = hdr.I4x4LfDelta != 0; - if (bitWriter.PutBitUniform(needUpdate ? 1 : 0) != 0) + if (this.PutBitUniform(needUpdate ? 1 : 0) != 0) { // we don't use refLfDelta => emit four 0 bits. - bitWriter.PutBits(0, 4); + this.PutBits(0, 4); // we use modeLfDelta for i4x4 - bitWriter.PutSignedBits(hdr.I4x4LfDelta, 6); - bitWriter.PutBits(0, 3); // all others unused. + this.PutSignedBits(hdr.I4x4LfDelta, 6); + this.PutBits(0, 3); // all others unused. } } } // Nominal quantization parameters - private void WriteQuant(Vp8BitWriter bitWriter) + private void WriteQuant() { - bitWriter.PutBits((uint)this.enc.BaseQuant, 7); - bitWriter.PutSignedBits(this.enc.DqY1Dc, 4); - bitWriter.PutSignedBits(this.enc.DqY2Dc, 4); - bitWriter.PutSignedBits(this.enc.DqY2Ac, 4); - bitWriter.PutSignedBits(this.enc.DqUvDc, 4); - bitWriter.PutSignedBits(this.enc.DqUvAc, 4); + this.PutBits((uint)this.enc.BaseQuant, 7); + this.PutSignedBits(this.enc.DqY1Dc, 4); + this.PutSignedBits(this.enc.DqY2Dc, 4); + this.PutSignedBits(this.enc.DqY2Ac, 4); + this.PutSignedBits(this.enc.DqUvDc, 4); + this.PutSignedBits(this.enc.DqUvAc, 4); } - private void WriteProbas(Vp8BitWriter bitWriter) + private void WriteProbas() { Vp8EncProba probas = this.enc.Proba; for (int t = 0; t < WebpConstants.NumTypes; ++t) @@ -599,25 +599,25 @@ private void WriteProbas(Vp8BitWriter bitWriter) { byte p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p]; bool update = p0 != WebpLookupTables.DefaultCoeffsProba[t, b, c, p]; - if (bitWriter.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p])) + if (this.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p])) { - bitWriter.PutBits(p0, 8); + this.PutBits(p0, 8); } } } } } - if (bitWriter.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0) + if (this.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0) { - bitWriter.PutBits(probas.SkipProba, 8); + this.PutBits(probas.SkipProba, 8); } } // Writes the partition #0 modes (that is: all intra modes) - private void CodeIntraModes(Vp8BitWriter bitWriter) + private void CodeIntraModes() { - var it = new Vp8EncIterator(this.enc.YTop, this.enc.UvTop, this.enc.Nz, this.enc.MbInfo, this.enc.Preds, this.enc.TopDerr, this.enc.Mbw, this.enc.Mbh); + Vp8EncIterator it = new(this.enc); int predsWidth = this.enc.PredsWidth; do @@ -627,18 +627,18 @@ private void CodeIntraModes(Vp8BitWriter bitWriter) Span preds = it.Preds.AsSpan(predIdx); if (this.enc.SegmentHeader.UpdateMap) { - bitWriter.PutSegment(mb.Segment, this.enc.Proba.Segments); + this.PutSegment(mb.Segment, this.enc.Proba.Segments); } if (this.enc.Proba.UseSkipProba) { - bitWriter.PutBit(mb.Skip, this.enc.Proba.SkipProba); + this.PutBit(mb.Skip, this.enc.Proba.SkipProba); } - if (bitWriter.PutBit(mb.MacroBlockType != 0, 145)) + if (this.PutBit(mb.MacroBlockType != 0, 145)) { // i16x16 - bitWriter.PutI16Mode(preds[0]); + this.PutI16Mode(preds[0]); } else { @@ -649,7 +649,7 @@ private void CodeIntraModes(Vp8BitWriter bitWriter) for (int x = 0; x < 4; x++) { byte[] probas = WebpLookupTables.ModesProba[topPred[x], left]; - left = bitWriter.PutI4Mode(it.Preds[predIdx + x], probas); + left = this.PutI4Mode(it.Preds[predIdx + x], probas); } topPred = it.Preds.AsSpan(predIdx); @@ -657,7 +657,7 @@ private void CodeIntraModes(Vp8BitWriter bitWriter) } } - bitWriter.PutUvMode(mb.UvMode); + this.PutUvMode(mb.UvMode); } while (it.Next()); } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index 7211f93766..a7c96edb7c 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -50,6 +50,11 @@ internal class Vp8EncIterator private int uvTopIdx; + public Vp8EncIterator(Vp8Encoder enc) + : this(enc.YTop, enc.UvTop, enc.Nz, enc.MbInfo, enc.Preds, enc.TopDerr, enc.Mbw, enc.Mbh) + { + } + public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, sbyte[] topDerr, int mbw, int mbh) { this.YTop = yTop; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index ce5d3bac11..c65099af88 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -328,7 +328,7 @@ public void Encode(Image image, Stream stream) int yStride = width; int uvStride = (yStride + 1) >> 1; - Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); + Vp8EncIterator it = new(this); Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; @@ -520,7 +520,7 @@ private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8Rd Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); + Vp8EncIterator it = new(this); long size = 0; long sizeP0 = 0; long distortion = 0; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 65f654dddc..81a7aebdf9 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -162,6 +162,11 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima features.AlphaChunkHeader = alphaChunkHeader; break; case WebpChunkType.Vp8L: + if (hasAlpha) + { + WebpThrowHelper.ThrowNotSupportedException("Alpha channel is not supported for lossless webp images."); + } + webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); break; default: From 95d36af396d5f57a9d79ca1e3a158c9b18e95a4a Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 19:50:28 +0800 Subject: [PATCH 03/19] (Vp8) Write total size after writing. Separate the writes of each block --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 47 ++++-- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 135 +++++++----------- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 10 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 8 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 27 ++-- .../Formats/Webp/WebpChunkParsingUtils.cs | 11 +- .../Metadata/Profiles/ICC/IccProfile.cs | 4 +- 7 files changed, 116 insertions(+), 126 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 4252f895b8..b82b764fc3 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -41,17 +42,23 @@ internal abstract class BitWriterBase public byte[] Buffer => this.buffer; + /// + /// Gets the number of bytes of the encoded image data. + /// + /// The number of bytes of the image data. + public abstract int NumBytes { get; } + /// /// Writes the encoded bytes of the image to the stream. Call Finish() before this. /// /// The stream to write to. - public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes())); + public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes)); /// /// Writes the encoded bytes of the image to the given buffer. Call Finish() before this. /// /// The destination buffer. - public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest); + public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes).CopyTo(dest); /// /// Resizes the buffer to write to. @@ -59,12 +66,6 @@ internal abstract class BitWriterBase /// The extra size in bytes needed. public abstract void BitWriterResize(int extraSize); - /// - /// Returns the number of bytes of the encoded image data. - /// - /// The number of bytes of the image data. - public abstract int NumBytes(); - /// /// Flush leftover bits. /// @@ -86,6 +87,7 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired) /// /// Writes the RIFF header to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The block length. protected void WriteRiffHeader(Stream stream, uint riffSize) @@ -99,6 +101,7 @@ protected void WriteRiffHeader(Stream stream, uint riffSize) /// /// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// + /// Think of it as a static method — none of the other members are called except for /// The metadata profile bytes. /// The metadata chunk size in bytes. protected static uint MetadataChunkSize(byte[] metadataBytes) @@ -118,9 +121,26 @@ protected static uint AlphaChunkSize(Span alphaBytes) return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); } + /// + /// Overwrites ides the write file size. + /// + /// The stream to write to. + protected static void OverwriteFileSize(Stream stream) + { + uint position = (uint)stream.Position; + stream.Position = 4; + byte[] buffer = new byte[4]; + + // "RIFF"(4)+uint32 size(4) + BinaryPrimitives.WriteUInt32LittleEndian(buffer, position - WebpConstants.ChunkHeaderSize); + stream.Write(buffer); + stream.Position = position; + } + /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The metadata profile's bytes. /// The chuck type to write. @@ -146,6 +166,7 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh /// /// Writes the color profile() to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The color profile bytes. protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); @@ -153,6 +174,7 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh /// /// Writes the animation parameter() to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. @@ -177,6 +199,7 @@ protected void WriteAnimationParameter(Stream stream, uint background, ushort lo /// /// Writes the animation frame() to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// Animation frame data. /// Frame data. @@ -201,6 +224,7 @@ protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, /// /// Writes the alpha chunk to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The alpha channel data bytes. /// Indicates, if the alpha channel data is compressed. @@ -232,14 +256,15 @@ protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDa /// /// Writes a VP8X header to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// A exif profile or null, if it does not exist. /// A XMP profile or null, if it does not exist. - /// The color profile bytes. + /// The color profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, byte[]? iccProfileBytes, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -279,7 +304,7 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfi flags |= 16; } - if (iccProfileBytes != null) + if (iccProfile != null) { // Set iccp flag. flags |= 32; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index cd84f109eb..5dd5d335de 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -72,7 +72,7 @@ public Vp8BitWriter(int expectedSize, Vp8Encoder enc) } /// - public override int NumBytes() => (int)this.pos; + public override int NumBytes => (int)this.pos; public int PutCoeffs(int ctx, Vp8Residual residual) { @@ -395,67 +395,58 @@ private void Flush() } /// - /// Writes the encoded image to the stream. + /// Write the trunks before data trunk. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. + /// The width of the image. + /// The height of the image. /// The exif profile. /// The XMP profile. /// The color profile. - /// The width of the image. - /// The height of the image. /// Flag indicating, if a alpha channel is present. /// The alpha channel data. /// Indicates, if the alpha data is compressed. - public void WriteEncodedImageToStream( + public void WriteTrunksBeforeData( Stream stream, + uint width, + uint height, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, - uint width, - uint height, bool hasAlpha, Span alphaData, bool alphaDataIsCompressed) { - bool isVp8X = false; - byte[]? exifBytes = null; - byte[]? xmpBytes = null; - byte[]? iccProfileBytes = null; - uint riffSize = 0; - if (exifProfile != null) - { - isVp8X = true; - exifBytes = exifProfile.ToByteArray(); - riffSize += MetadataChunkSize(exifBytes!); - } + // Write file size later + this.WriteRiffHeader(stream, 0); - if (xmpProfile != null) + // Write VP8X, header if necessary. + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; + if (isVp8X) { - isVp8X = true; - xmpBytes = xmpProfile.Data; - riffSize += MetadataChunkSize(xmpBytes!); - } + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); - if (iccProfile != null) - { - isVp8X = true; - iccProfileBytes = iccProfile.ToByteArray(); - riffSize += MetadataChunkSize(iccProfileBytes); - } + if (iccProfile != null) + { + this.WriteColorProfile(stream, iccProfile.ToByteArray()); + } - if (hasAlpha) - { - isVp8X = true; - riffSize += AlphaChunkSize(alphaData); + if (hasAlpha) + { + this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + } } + } - if (isVp8X) - { - riffSize += ExtendedFileChunkSize; - } + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + public void WriteEncodedImageToStream(Stream stream) + { + uint numBytes = (uint)this.NumBytes; - this.Finish(); - uint numBytes = (uint)this.NumBytes(); int mbSize = this.enc.Mbw * this.enc.Mbh; int expectedSize = (int)((uint)mbSize * 7 / 8); @@ -469,12 +460,10 @@ public void WriteEncodedImageToStream( uint pad = vp8Size & 1; vp8Size += pad; - // Compute RIFF size. - // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. - riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; + // Emit header and partition #0 + this.WriteVp8Header(stream, vp8Size); + this.WriteFrameHeader(stream, size0); - // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfileBytes, hasAlpha, alphaData, alphaDataIsCompressed); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -483,16 +472,31 @@ public void WriteEncodedImageToStream( { stream.WriteByte(0); } + } + /// + /// Write the trunks after data trunk. + /// + /// Think of it as a static method — none of the other members are called except for + /// The stream to write to. + /// The exif profile. + /// The XMP profile. + public void WriteTrunksAfterData( + Stream stream, + ExifProfile? exifProfile, + XmpProfile? xmpProfile) + { if (exifProfile != null) { - this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); + this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); } if (xmpProfile != null) { - this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); + this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); } + + OverwriteFileSize(stream); } private uint GeneratePartition0() @@ -512,7 +516,7 @@ private uint GeneratePartition0() this.Finish(); - return (uint)this.NumBytes(); + return (uint)this.NumBytes; } private void WriteSegmentHeader() @@ -662,43 +666,6 @@ private void CodeIntraModes() while (it.Next()); } - private void WriteWebpHeaders( - Stream stream, - uint size0, - uint vp8Size, - uint riffSize, - bool isVp8X, - uint width, - uint height, - ExifProfile? exifProfile, - XmpProfile? xmpProfile, - byte[]? iccProfileBytes, - bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) - { - this.WriteRiffHeader(stream, riffSize); - - // Write VP8X, header if necessary. - if (isVp8X) - { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfileBytes, width, height, hasAlpha); - - if (iccProfileBytes != null) - { - this.WriteColorProfile(stream, iccProfileBytes); - } - - if (hasAlpha) - { - this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); - } - } - - this.WriteVp8Header(stream, vp8Size); - this.WriteFrameHeader(stream, size0); - } - private void WriteVp8Header(Stream stream, uint size) { Span buf = stackalloc byte[WebpConstants.TagSize]; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index a042f68968..0ac1b4038a 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -47,6 +47,9 @@ public Vp8LBitWriter(int expectedSize) { } + /// + public override int NumBytes => this.cur + ((this.used + 7) >> 3); + /// /// Initializes a new instance of the class. /// Used internally for cloning. @@ -98,9 +101,6 @@ public void WriteHuffmanCodeWithExtraBits(HuffmanTreeCode code, int codeIndex, i this.PutBits((uint)((bits << depth) | symbol), depth + nBits); } - /// - public override int NumBytes() => this.cur + ((this.used + 7) >> 3); - public Vp8LBitWriter Clone() { byte[] clonedBuffer = new byte[this.Buffer.Length]; @@ -166,7 +166,7 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, X } this.Finish(); - uint size = (uint)this.NumBytes(); + uint size = (uint)this.NumBytes; size++; // One byte extra for the VP8L signature. // Write RIFF header. @@ -177,7 +177,7 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, X // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccBytes, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); if (iccBytes != null) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 9b82cc5983..d27bfcd956 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -291,7 +291,7 @@ public int EncodeAlphaImageData(ImageFrame frame, IMemoryOwner= pixelCount) { // Compressing would not yield in smaller data -> leave the data uncompressed. @@ -425,9 +425,9 @@ private void EncodeStream(ImageFrame frame) lowEffort); // If we are better than what we already have. - if (isFirstConfig || this.bitWriter.NumBytes() < bestSize) + if (isFirstConfig || this.bitWriter.NumBytes < bestSize) { - bestSize = this.bitWriter.NumBytes(); + bestSize = this.bitWriter.NumBytes; BitWriterSwap(ref this.bitWriter, ref bitWriterBest); } @@ -676,7 +676,7 @@ private void EncodeImage(int width, int height, bool useCache, CrunchConfig conf this.StoreImageToBitMask(width, this.HistoBits, refsBest, histogramSymbols, huffmanCodes); // Keep track of the smallest image so far. - if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes())) + if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes < bitWriterBest.NumBytes)) { Vp8LBitWriter tmp = this.bitWriter; this.bitWriter = bitWriterBest; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index c65099af88..56397e66d4 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -408,16 +409,21 @@ public void Encode(Image image, Stream stream) } } - this.bitWriter.WriteEncodedImageToStream( + this.bitWriter.Finish(); + this.bitWriter.WriteTrunksBeforeData( stream, + (uint)width, + (uint)height, exifProfile, xmpProfile, metadata.IccProfile, - (uint)width, - (uint)height, hasAlpha, alphaData[..alphaDataSize], this.alphaCompression && alphaCompressionSucceeded); + + this.bitWriter.WriteEncodedImageToStream(stream); + + this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); } finally { @@ -862,10 +868,11 @@ private void SetSegmentProbas() this.ResetSegments(); } - this.SegmentHeader.Size = (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) + - (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) + - (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) + - (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2]))); + this.SegmentHeader.Size = + (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) + + (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) + + (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) + + (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2]))); } else { @@ -1027,7 +1034,7 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid it.NzToBytes(); - int pos1 = this.bitWriter.NumBytes(); + int pos1 = this.bitWriter.NumBytes; if (i16) { residual.Init(0, 1, this.Proba); @@ -1054,7 +1061,7 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid } } - int pos2 = this.bitWriter.NumBytes(); + int pos2 = this.bitWriter.NumBytes; // U/V residual.Init(0, 2, this.Proba); @@ -1072,7 +1079,7 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid } } - int pos3 = this.bitWriter.NumBytes(); + int pos3 = this.bitWriter.NumBytes; it.LumaBits = pos2 - pos1; it.UvBits = pos3 - pos2; it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index becd622e17..9e9f0f7f62 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -76,7 +76,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); } - if (!buffer.Slice(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) + if (!buffer[..3].SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) { WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); } @@ -111,14 +111,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe PartitionLength = partitionLength }; - Vp8BitReader bitReader = new( - stream, - remaining, - memoryAllocator, - partitionLength) - { - Remaining = remaining - }; + Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; return new() { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index 3b5e438299..be7350bc44 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -158,8 +158,7 @@ public bool CheckIsValid() Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) && Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) && Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) && - this.Header.Size >= minSize && - this.Header.Size < maxSize; + this.Header.Size is >= minSize and < maxSize; } /// @@ -175,7 +174,6 @@ public byte[] ToByteArray() return copy; } - IccWriter writer = new(); return IccWriter.Write(this); } From 437144dab5bb08743aa43e2411ec7bd38963b1c0 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 21:14:31 +0800 Subject: [PATCH 04/19] (Vp8L) Write total size after writing. Separate the writes of each block --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 76 ++++++++++++++++++ .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 80 +------------------ .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 76 +----------------- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 17 +++- .../Formats/Webp/Lossy/Vp8Encoder.cs | 1 - 5 files changed, 97 insertions(+), 153 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index b82b764fc3..ad7d69f130 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -137,6 +137,82 @@ protected static void OverwriteFileSize(Stream stream) stream.Position = position; } + /// + /// Write the trunks before data trunk. + /// + /// Think of it as a static method — none of the other members are called except for + /// The stream to write to. + /// The width of the image. + /// The height of the image. + /// The exif profile. + /// The XMP profile. + /// The color profile. + /// Flag indicating, if a alpha channel is present. + /// The alpha channel data. + /// Indicates, if the alpha data is compressed. + public void WriteTrunksBeforeData( + Stream stream, + uint width, + uint height, + ExifProfile? exifProfile, + XmpProfile? xmpProfile, + IccProfile? iccProfile, + bool hasAlpha, + Span alphaData, + bool alphaDataIsCompressed) + { + // Write file size later + this.WriteRiffHeader(stream, 0); + + // Write VP8X, header if necessary. + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; + if (isVp8X) + { + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + + if (iccProfile != null) + { + this.WriteColorProfile(stream, iccProfile.ToByteArray()); + } + + if (hasAlpha) + { + this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + } + } + } + + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + public abstract void WriteEncodedImageToStream(Stream stream); + + /// + /// Write the trunks after data trunk. + /// + /// Think of it as a static method — none of the other members are called except for + /// The stream to write to. + /// The exif profile. + /// The XMP profile. + public void WriteTrunksAfterData( + Stream stream, + ExifProfile? exifProfile, + XmpProfile? xmpProfile) + { + if (exifProfile != null) + { + this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); + } + + if (xmpProfile != null) + { + this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); + } + + OverwriteFileSize(stream); + } + /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 5dd5d335de..923d2a69c4 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -3,9 +3,6 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Webp.Lossy; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -394,56 +391,8 @@ private void Flush() } } - /// - /// Write the trunks before data trunk. - /// - /// Think of it as a static method — none of the other members are called except for - /// The stream to write to. - /// The width of the image. - /// The height of the image. - /// The exif profile. - /// The XMP profile. - /// The color profile. - /// Flag indicating, if a alpha channel is present. - /// The alpha channel data. - /// Indicates, if the alpha data is compressed. - public void WriteTrunksBeforeData( - Stream stream, - uint width, - uint height, - ExifProfile? exifProfile, - XmpProfile? xmpProfile, - IccProfile? iccProfile, - bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) - { - // Write file size later - this.WriteRiffHeader(stream, 0); - - // Write VP8X, header if necessary. - bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; - if (isVp8X) - { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); - - if (iccProfile != null) - { - this.WriteColorProfile(stream, iccProfile.ToByteArray()); - } - - if (hasAlpha) - { - this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); - } - } - } - - /// - /// Writes the encoded image to the stream. - /// - /// The stream to write to. - public void WriteEncodedImageToStream(Stream stream) + /// + public override void WriteEncodedImageToStream(Stream stream) { uint numBytes = (uint)this.NumBytes; @@ -474,31 +423,6 @@ public void WriteEncodedImageToStream(Stream stream) } } - /// - /// Write the trunks after data trunk. - /// - /// Think of it as a static method — none of the other members are called except for - /// The stream to write to. - /// The exif profile. - /// The XMP profile. - public void WriteTrunksAfterData( - Stream stream, - ExifProfile? exifProfile, - XmpProfile? xmpProfile) - { - if (exifProfile != null) - { - this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); - } - - if (xmpProfile != null) - { - this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); - } - - OverwriteFileSize(stream); - } - private uint GeneratePartition0() { this.PutBitUniform(0); // colorspace diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 0ac1b4038a..bce77c9e5c 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -3,9 +3,6 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Webp.Lossless; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -122,68 +119,11 @@ public override void Finish() this.used = 0; } - /// - /// Writes the encoded image to the stream. - /// - /// The stream to write to. - /// The exif profile. - /// The XMP profile. - /// The color profile. - /// The width of the image. - /// The height of the image. - /// Flag indicating, if a alpha channel is present. - public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha) + /// + public override void WriteEncodedImageToStream(Stream stream) { - bool isVp8X = false; - byte[]? exifBytes = null; - byte[]? xmpBytes = null; - byte[]? iccBytes = null; - uint riffSize = 0; - if (exifProfile != null) - { - isVp8X = true; - exifBytes = exifProfile.ToByteArray(); - riffSize += MetadataChunkSize(exifBytes!); - } - - if (xmpProfile != null) - { - isVp8X = true; - xmpBytes = xmpProfile.Data; - riffSize += MetadataChunkSize(xmpBytes!); - } - - if (iccProfile != null) - { - isVp8X = true; - iccBytes = iccProfile.ToByteArray(); - riffSize += MetadataChunkSize(iccBytes); - } - - if (isVp8X) - { - riffSize += ExtendedFileChunkSize; - } - - this.Finish(); - uint size = (uint)this.NumBytes; - size++; // One byte extra for the VP8L signature. - - // Write RIFF header. + uint size = (uint)this.NumBytes + 1; // One byte extra for the VP8L signature uint pad = size & 1; - riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + size + pad; - this.WriteRiffHeader(stream, riffSize); - - // Write VP8X, header if necessary. - if (isVp8X) - { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); - - if (iccBytes != null) - { - this.WriteColorProfile(stream, iccBytes); - } - } // Write magic bytes indicating its a lossless webp. Span scratchBuffer = stackalloc byte[WebpConstants.TagSize]; @@ -201,16 +141,6 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, X { stream.WriteByte(0); } - - if (exifProfile != null) - { - this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); - } - - if (xmpProfile != null) - { - this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); - } } /// diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index d27bfcd956..4d526e7b4b 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -265,8 +266,22 @@ public void Encode(Image image, Stream stream) // Encode the main image stream. this.EncodeStream(image.Frames.RootFrame); + this.bitWriter.Finish(); + this.bitWriter.WriteTrunksBeforeData( + stream, + (uint)width, + (uint)height, + exifProfile, + xmpProfile, + metadata.IccProfile, + false /*hasAlpha*/, + Span.Empty, + false); + // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); + this.bitWriter.WriteEncodedImageToStream(stream); + + this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 56397e66d4..f744827bf3 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -8,7 +8,6 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; From fbc08bd6a683a8c96777dfc17441d3fabb7f554c Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 23:21:11 +0800 Subject: [PATCH 05/19] Implement Vp8 encoder --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 106 +++++++-------- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 5 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 121 ++++++++++++++---- .../Formats/Webp/WebpEncoderCore.cs | 30 ++++- .../Formats/WebP/WebpEncoderTests.cs | 7 + 5 files changed, 180 insertions(+), 89 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index ad7d69f130..4a9da3cbb1 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -87,21 +87,20 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired) /// /// Writes the RIFF header to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The block length. - protected void WriteRiffHeader(Stream stream, uint riffSize) + protected static void WriteRiffHeader(Stream stream, uint riffSize) { stream.Write(WebpConstants.RiffFourCc); - BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize); - stream.Write(this.scratchBuffer.Span[..4]); + Span buf = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize); + stream.Write(buf); stream.Write(WebpConstants.WebpHeader); } /// /// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// - /// Think of it as a static method — none of the other members are called except for /// The metadata profile bytes. /// The metadata chunk size in bytes. protected static uint MetadataChunkSize(byte[] metadataBytes) @@ -125,22 +124,11 @@ protected static uint AlphaChunkSize(Span alphaBytes) /// Overwrites ides the write file size. /// /// The stream to write to. - protected static void OverwriteFileSize(Stream stream) - { - uint position = (uint)stream.Position; - stream.Position = 4; - byte[] buffer = new byte[4]; - - // "RIFF"(4)+uint32 size(4) - BinaryPrimitives.WriteUInt32LittleEndian(buffer, position - WebpConstants.ChunkHeaderSize); - stream.Write(buffer); - stream.Position = position; - } + protected static void OverwriteFileSize(Stream stream) => OverwriteFrameSize(stream, 4); /// /// Write the trunks before data trunk. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The width of the image. /// The height of the image. @@ -148,9 +136,8 @@ protected static void OverwriteFileSize(Stream stream) /// The XMP profile. /// The color profile. /// Flag indicating, if a alpha channel is present. - /// The alpha channel data. - /// Indicates, if the alpha data is compressed. - public void WriteTrunksBeforeData( + /// Flag indicating, if an animation parameter is present. + public static void WriteTrunksBeforeData( Stream stream, uint width, uint height, @@ -158,26 +145,20 @@ public void WriteTrunksBeforeData( XmpProfile? xmpProfile, IccProfile? iccProfile, bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) + bool hasAnimation) { // Write file size later - this.WriteRiffHeader(stream, 0); + WriteRiffHeader(stream, 0); // Write VP8X, header if necessary. - bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation; if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation); if (iccProfile != null) { - this.WriteColorProfile(stream, iccProfile.ToByteArray()); - } - - if (hasAlpha) - { - this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + WriteColorProfile(stream, iccProfile.ToByteArray()); } } } @@ -191,23 +172,22 @@ public void WriteTrunksBeforeData( /// /// Write the trunks after data trunk. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The exif profile. /// The XMP profile. - public void WriteTrunksAfterData( + public static void WriteTrunksAfterData( Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile) { if (exifProfile != null) { - this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); + WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); } if (xmpProfile != null) { - this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); + WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); } OverwriteFileSize(stream); @@ -216,16 +196,15 @@ public void WriteTrunksAfterData( /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The metadata profile's bytes. /// The chuck type to write. - protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) + protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) { DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); uint size = (uint)metadataBytes.Length; - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -242,15 +221,13 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh /// /// Writes the color profile() to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The color profile bytes. - protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); + protected static void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); /// /// Writes the animation parameter() to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. @@ -259,9 +236,9 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh /// The background color is also used when the Disposal method is 1. /// /// The number of times to loop the animation. If it is 0, this means infinitely. - protected void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) + public static void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) { - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); @@ -275,17 +252,15 @@ protected void WriteAnimationParameter(Stream stream, uint background, ushort lo /// /// Writes the animation frame() to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// Animation frame data. - /// Frame data. - protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, Span data) + public static long WriteAnimationFrame(Stream stream, AnimationFrameData animation) { - uint size = AnimationFrameData.HeaderSize + (uint)data.Length; - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); stream.Write(buf); - BinaryPrimitives.WriteUInt32BigEndian(buf, size); + long position = stream.Position; + BinaryPrimitives.WriteUInt32BigEndian(buf, 0); stream.Write(buf); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y); @@ -294,20 +269,35 @@ protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); stream.WriteByte(flag); - stream.Write(data); + return position; + } + + /// + /// Overwrites ides the write frame size. + /// + /// The stream to write to. + /// Previous position. + public static void OverwriteFrameSize(Stream stream, long prevPosition) + { + uint position = (uint)stream.Position; + stream.Position = prevPosition; + byte[] buffer = new byte[4]; + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)(position - prevPosition - 4)); + stream.Write(buffer); + stream.Position = position; } /// /// Writes the alpha chunk to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The alpha channel data bytes. /// Indicates, if the alpha channel data is compressed. - protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) + public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { uint size = (uint)dataBytes.Length + 1; - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -332,7 +322,6 @@ protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDa /// /// Writes a VP8X header to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// A exif profile or null, if it does not exist. /// A XMP profile or null, if it does not exist. @@ -340,7 +329,8 @@ protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDa /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha) + /// Flag indicating, if an animation parameter is present. + protected static void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation) { if (width > MaxDimension || height > MaxDimension) { @@ -360,13 +350,11 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfi flags |= 8; } - /* - if (isAnimated) + if (hasAnimation) { // Set animated flag. flags |= 2; } - */ if (xmpProfile != null) { @@ -386,7 +374,7 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfi flags |= 32; } - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 4d526e7b4b..5859d8a872 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -267,7 +267,7 @@ public void Encode(Image image, Stream stream) this.EncodeStream(image.Frames.RootFrame); this.bitWriter.Finish(); - this.bitWriter.WriteTrunksBeforeData( + BitWriterBase.WriteTrunksBeforeData( stream, (uint)width, (uint)height, @@ -275,13 +275,12 @@ public void Encode(Image image, Stream stream) xmpProfile, metadata.IccProfile, false /*hasAlpha*/, - Span.Empty, false); // Write bytes from the bitwriter buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream); - this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index f744827bf3..ccd7d8b6d5 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -88,7 +88,8 @@ internal class Vp8Encoder : IDisposable private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11; - private const long HeaderSizeEstimate = WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize; + private const long HeaderSizeEstimate = + WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize; private const int QMin = 0; @@ -165,7 +166,7 @@ public Vp8Encoder( // TODO: make partition_limit configurable? const int limit = 100; // original code: limit = 100 - config->partition_limit; this.maxI4HeaderBits = - 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve. + 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve. this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh]; for (int i = 0; i < this.MbInfo.Length; i++) @@ -308,22 +309,88 @@ public Vp8Encoder( /// private int MbHeaderLimit { get; } + public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation, uint background = 0, uint loopCount = 0) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + metadata.SyncProfiles(); + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + BitWriterBase.WriteTrunksBeforeData( + stream, + (uint)image.Width, + (uint)image.Height, + exifProfile, + xmpProfile, + metadata.IccProfile, + hasAlpha, + hasAnimation); + + if (hasAnimation) + { + BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + } + } + + public void EncodeFooter(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + public void EncodeAnimation(ImageFrame frame, Stream stream) + where TPixel : unmanaged, IPixel => + this.Encode(frame, stream, true, null); + /// /// Encodes the image to the specified stream from the . /// /// The pixel format. /// The to encode from. /// The to encode the image data to. - public void Encode(Image image, Stream stream) + public void EncodeStatic(Image image, Stream stream) + where TPixel : unmanaged, IPixel => + this.Encode(image.Frames.RootFrame, stream, false, image); + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// Flag indicating, if an animation parameter is present. + /// The to encode from. + private void Encode(ImageFrame frame, Stream stream, bool hasAnimation, Image image) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + int width = frame.Width; + int height = frame.Height; + int pixelCount = width * height; Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - bool hasAlpha = YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, this.configuration, this.memoryAllocator, y, u, v); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(frame, this.configuration, this.memoryAllocator, y, u, v); + + if (!hasAnimation) + { + this.EncodeHeader(image, stream, hasAlpha, false); + } int yStride = width; int uvStride = (yStride + 1) >> 1; @@ -375,13 +442,6 @@ public void Encode(Image image, Stream stream) // Store filter stats. this.AdjustFilterStrength(); - // Write bytes from the bitwriter buffer to the stream. - ImageMetadata metadata = image.Metadata; - metadata.SyncProfiles(); - - ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; - XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; - // Extract and encode alpha channel data, if present. int alphaDataSize = 0; bool alphaCompressionSucceeded = false; @@ -393,7 +453,7 @@ public void Encode(Image image, Stream stream) { // TODO: This can potentially run in an separate task. encodedAlphaData = AlphaEncoder.EncodeAlpha( - image.Frames.RootFrame, + frame, this.configuration, this.memoryAllocator, this.skipMetadata, @@ -409,20 +469,31 @@ public void Encode(Image image, Stream stream) } this.bitWriter.Finish(); - this.bitWriter.WriteTrunksBeforeData( - stream, - (uint)width, - (uint)height, - exifProfile, - xmpProfile, - metadata.IccProfile, - hasAlpha, - alphaData[..alphaDataSize], - this.alphaCompression && alphaCompressionSucceeded); + + long prevPosition = 0; + + if (hasAnimation) + { + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + { + Width = (uint)frame.Width, + Height = (uint)frame.Height + }); + } + + if (hasAlpha) + { + Span data = alphaData[..alphaDataSize]; + bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded; + BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed); + } this.bitWriter.WriteEncodedImageToStream(stream); - this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + if (hasAnimation) + { + BitWriterBase.OverwriteFrameSize(stream, prevPosition); + } } finally { diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 49512e03b5..2751f99134 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -144,7 +144,7 @@ public void Encode(Image image, Stream stream, CancellationToken } else { - using Vp8Encoder enc = new( + using Vp8Encoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -156,7 +156,33 @@ public void Encode(Image image, Stream stream, CancellationToken this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); - enc.Encode(image, stream); + if (image.Frames.Count > 1) + { + encoder.EncodeHeader(image, stream, false, true); + + foreach (ImageFrame imageFrame in image.Frames) + { + using Vp8Encoder enc = new( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); + enc.EncodeAnimation(imageFrame, stream); + } + } + else + { + encoder.EncodeStatic(image, stream); + } + + encoder.EncodeFooter(image, stream); } } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 6c5fa50ff6..4b100e854e 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -17,6 +17,13 @@ public class WebpEncoderTests { private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); + [Fact] + public void Encode_AnimatedLossy() + { + Image image = Image.Load(@"C:\Users\poker\Desktop\1.webp"); + image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp"); + } + [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)] From 87de52141219f23a39559a8bae09d79c30e109d6 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 23:58:56 +0800 Subject: [PATCH 06/19] Implement Vp8L encoder --- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 87 +++++++++++++------ .../Formats/Webp/WebpEncoderCore.cs | 32 ++++++- .../Formats/WebP/WebpEncoderTests.cs | 7 +- 3 files changed, 97 insertions(+), 29 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 5859d8a872..d301df94f6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -10,7 +10,6 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -236,26 +235,59 @@ public Vp8LEncoder( /// public Vp8LHashChain HashChain { get; } - /// - /// Encodes the image as lossless webp to the specified stream. - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) + public void EncodeHeader(Image image, Stream stream, bool hasAnimation, uint background = 0, uint loopCount = 0) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - + // Write bytes from the bitwriter buffer to the stream. ImageMetadata metadata = image.Metadata; metadata.SyncProfiles(); ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + BitWriterBase.WriteTrunksBeforeData( + stream, + (uint)image.Width, + (uint)image.Height, + exifProfile, + xmpProfile, + metadata.IccProfile, + false, + hasAnimation); + + if (hasAnimation) + { + BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + } + } + + public void EncodeFooter(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + } + + /// + /// Encodes the image as lossless webp to the specified stream. + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// Flag indicating, if an animation parameter is present. + public void Encode(ImageFrame frame, 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(image.Frames.RootFrame, width, height); + bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height); // Write the image size. this.WriteImageSize(width, height); @@ -264,23 +296,28 @@ public void Encode(Image image, Stream stream) this.WriteAlphaAndVersion(hasAlpha); // Encode the main image stream. - this.EncodeStream(image.Frames.RootFrame); + this.EncodeStream(frame); this.bitWriter.Finish(); - BitWriterBase.WriteTrunksBeforeData( - stream, - (uint)width, - (uint)height, - exifProfile, - xmpProfile, - metadata.IccProfile, - false /*hasAlpha*/, - false); + + long prevPosition = 0; + + if (hasAnimation) + { + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + { + Width = (uint)frame.Width, + Height = (uint)frame.Height + }); + } // Write bytes from the bitwriter buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream); - BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + if (hasAnimation) + { + BitWriterBase.OverwriteFrameSize(stream, prevPosition); + } } /// @@ -1843,9 +1880,9 @@ public void Dispose() { this.Bgra.Dispose(); this.EncodedData.Dispose(); - this.BgraScratch.Dispose(); + this.BgraScratch?.Dispose(); this.Palette.Dispose(); - this.TransformData.Dispose(); + this.TransformData?.Dispose(); this.HashChain.Dispose(); } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 2751f99134..47712071bf 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,7 +129,7 @@ public void Encode(Image image, Stream stream, CancellationToken if (lossless) { - using Vp8LEncoder enc = new( + using Vp8LEncoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -140,7 +140,34 @@ public void Encode(Image image, Stream stream, CancellationToken this.transparentColorMode, this.nearLossless, this.nearLosslessQuality); - enc.Encode(image, stream); + + bool hasAnimation = image.Frames.Count > 1; + encoder.EncodeHeader(image, stream, hasAnimation); + if (hasAnimation) + { + foreach (ImageFrame imageFrame in image.Frames) + { + using Vp8LEncoder enc = new( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); + + enc.Encode(imageFrame, stream, true); + } + } + else + { + encoder.Encode(image.Frames.RootFrame, stream, false); + } + + encoder.EncodeFooter(image, stream); } else { @@ -174,6 +201,7 @@ public void Encode(Image image, Stream stream, CancellationToken this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); + enc.EncodeAnimation(imageFrame, stream); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 4b100e854e..1721cd938b 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -20,8 +20,11 @@ public class WebpEncoderTests [Fact] public void Encode_AnimatedLossy() { - Image image = Image.Load(@"C:\Users\poker\Desktop\1.webp"); - image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp"); + Image image = Image.Load(@"C:\WorkSpace\ImageSharp\tests\Images\Input\Webp\leo_animated_lossless.webp"); + image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp", new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossless + }); } [Theory] From 2c260b27bf273f7154a93a43bf1e5149776fbe5d Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 24 Oct 2023 00:09:10 +0800 Subject: [PATCH 07/19] add unit test and format --- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 6 ++-- .../Formats/WebP/WebpEncoderTests.cs | 29 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index bce77c9e5c..0b71a3ed0c 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -44,9 +44,6 @@ public Vp8LBitWriter(int expectedSize) { } - /// - public override int NumBytes => this.cur + ((this.used + 7) >> 3); - /// /// Initializes a new instance of the class. /// Used internally for cloning. @@ -59,6 +56,9 @@ private Vp8LBitWriter(byte[] buffer, ulong bits, int used, int cur) this.cur = cur; } + /// + public override int NumBytes => this.cur + ((this.used + 7) >> 3); + /// /// This function writes bits into bytes in increasing addresses (little endian), /// and within a byte least-significant-bit first. This function can write up to 32 bits in one go. diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 1721cd938b..d81c9eb93a 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -17,14 +18,28 @@ public class WebpEncoderTests { private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); - [Fact] - public void Encode_AnimatedLossy() + [Theory] + [WithFile(Lossless.Animated, PixelTypes.Rgba32)] + public void Encode_AnimatedLossless(TestImageProvider provider) + where TPixel : unmanaged, IPixel { - Image image = Image.Load(@"C:\WorkSpace\ImageSharp\tests\Images\Input\Webp\leo_animated_lossless.webp"); - image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp", new WebpEncoder() - { - FileFormat = WebpFileFormatType.Lossless - }); + using Image image = provider.GetImage(); + using MemoryStream memStream = new(); + image.SaveAsWebp(memStream, new() { FileFormat = WebpFileFormatType.Lossless }); + + // TODO: DebugSave, VerifySimilarity + } + + [Theory] + [WithFile(Lossy.Animated, PixelTypes.Rgba32)] + public void Encode_AnimatedLossy(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + using MemoryStream memStream = new(); + image.SaveAsWebp(memStream, new()); + + // TODO: DebugSave, VerifySimilarity } [Theory] From 6fed95b5165f4216d9cb476d54fbd3e7faf5f59b Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 24 Oct 2023 09:03:23 +0800 Subject: [PATCH 08/19] remove unused scratchBuffer field --- src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 4a9da3cbb1..89db7ed645 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -22,11 +22,6 @@ internal abstract class BitWriterBase /// private byte[] buffer; - /// - /// A scratch buffer to reduce allocations. - /// - private ScratchBuffer scratchBuffer; // mutable struct, don't make readonly - /// /// Initializes a new instance of the class. /// From dbb89603ee4ac639f8b6fa08fde747250b10d7b5 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 24 Oct 2023 12:38:17 +0800 Subject: [PATCH 09/19] encode FrameDuration --- src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs | 3 ++- src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index d301df94f6..af472845ac 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -307,7 +307,8 @@ public void Encode(ImageFrame frame, Stream stream, bool hasAnim prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() { Width = (uint)frame.Width, - Height = (uint)frame.Height + Height = (uint)frame.Height, + Duration = frame.Metadata.GetWebpMetadata().FrameDuration }); } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index ccd7d8b6d5..40dbb90de6 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -477,7 +477,8 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() { Width = (uint)frame.Width, - Height = (uint)frame.Height + Height = (uint)frame.Height, + Duration = frame.Metadata.GetWebpMetadata().FrameDuration }); } From bd2d4550a998bb521cc8cce39bad9be42da201bf Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 25 Oct 2023 16:47:14 +0800 Subject: [PATCH 10/19] refactor to follow style rules --- src/ImageSharp/Formats/Webp/AlphaDecoder.cs | 2 +- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 17 ++---- .../Formats/Webp/AnimationFrameData.cs | 2 +- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 4 +- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 2 +- .../Webp/Lossless/BackwardReferenceEncoder.cs | 2 +- .../Formats/Webp/Lossless/CostManager.cs | 2 +- .../Formats/Webp/Lossless/PixOrCopy.cs | 7 ++- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 18 +++--- .../Webp/Lossless/WebpLosslessDecoder.cs | 4 +- .../Formats/Webp/Lossy/Vp8EncIterator.cs | 6 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 16 ++--- .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 18 +++--- .../Formats/Webp/WebpAnimationDecoder.cs | 12 ++-- .../Formats/Webp/WebpAnimationEncoder.cs | 12 ---- .../Formats/Webp/WebpChunkParsingUtils.cs | 20 ++++--- src/ImageSharp/Formats/Webp/WebpDecoder.cs | 9 ++- .../Formats/Webp/WebpDecoderCore.cs | 27 +++++---- .../Formats/Webp/WebpDecoderOptions.cs | 2 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 2 +- .../Formats/Webp/WebpEncoderCore.cs | 58 ++++--------------- src/ImageSharp/Formats/Webp/WebpFormat.cs | 6 +- 22 files changed, 99 insertions(+), 149 deletions(-) delete mode 100644 src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs index 289ebd35ca..63e6541354 100644 --- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs @@ -59,7 +59,7 @@ public AlphaDecoder(int width, int height, IMemoryOwner data, byte alphaCh if (this.Compressed) { - Vp8LBitReader bitReader = new(data); + Vp8LBitReader bitReader = new Vp8LBitReader(data); this.LosslessDecoder = new WebpLosslessDecoder(bitReader, memoryAllocator, configuration); this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true); diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index a18d44fde4..2084686969 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -43,17 +43,8 @@ public static IMemoryOwner EncodeAlpha( { const WebpEncodingMethod effort = WebpEncodingMethod.Default; const int quality = 8 * (int)effort; - using Vp8LEncoder lossLessEncoder = new( - memoryAllocator, - configuration, - width, - height, - quality, - skipMetadata, - effort, - WebpTransparentColorMode.Preserve, - false, - 0); + using Vp8LEncoder lossLessEncoder = new Vp8LEncoder(memoryAllocator, configuration, width, height, quality, + skipMetadata, effort, WebpTransparentColorMode.Preserve, false, 0); // 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, @@ -81,7 +72,7 @@ private static ImageFrame DispatchAlphaToGreen(ImageFrame alphaAsFrame = new(Configuration.Default, width, height); + ImageFrame alphaAsFrame = new ImageFrame(Configuration.Default, width, height); for (int y = 0; y < height; y++) { @@ -91,7 +82,7 @@ private static ImageFrame DispatchAlphaToGreen(ImageFrame buffer = stackalloc byte[4]; - AnimationFrameData data = new() + AnimationFrameData data = new AnimationFrameData { DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 923d2a69c4..81530706d6 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -399,7 +399,7 @@ public override void WriteEncodedImageToStream(Stream stream) int mbSize = this.enc.Mbw * this.enc.Mbh; int expectedSize = (int)((uint)mbSize * 7 / 8); - Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc); + Vp8BitWriter bitWriterPartZero = new Vp8BitWriter(expectedSize, this.enc); // Partition #0 with header and partition sizes. uint size0 = bitWriterPartZero.GeneratePartition0(); @@ -545,7 +545,7 @@ private void WriteProbas() // Writes the partition #0 modes (that is: all intra modes) private void CodeIntraModes() { - Vp8EncIterator it = new(this.enc); + Vp8EncIterator it = new Vp8EncIterator(this.enc); int predsWidth = this.enc.PredsWidth; do diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 0b71a3ed0c..dc867fa85e 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -102,7 +102,7 @@ public Vp8LBitWriter Clone() { byte[] clonedBuffer = new byte[this.Buffer.Length]; System.Buffer.BlockCopy(this.Buffer, 0, clonedBuffer, 0, this.cur); - return new(clonedBuffer, this.bits, this.used, this.cur); + return new Vp8LBitWriter(clonedBuffer, this.bits, this.used, this.cur); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 61133142bf..03cb1990b9 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -775,7 +775,7 @@ private static void BackwardReferencesRle(int xSize, int ySize, ReadOnlySpan bgra, int cacheBits, Vp8LBackwardRefs refs) { int pixelIndex = 0; - ColorCache colorCache = new(cacheBits); + ColorCache colorCache = new ColorCache(cacheBits); for (int idx = 0; idx < refs.Refs.Count; idx++) { PixOrCopy v = refs.Refs[idx]; diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs index e393c065ec..63ce9dbec6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs @@ -17,7 +17,7 @@ internal sealed class CostManager : IDisposable private const int FreeIntervalsStartCount = 25; - private readonly Stack freeIntervals = new(FreeIntervalsStartCount); + private readonly Stack freeIntervals = new Stack(FreeIntervalsStartCount); public CostManager(MemoryAllocator memoryAllocator, IMemoryOwner distArray, int pixCount, CostModel costModel) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs index 6a28e5b3fb..61804812d5 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs @@ -15,7 +15,7 @@ internal sealed class PixOrCopy public uint BgraOrDistance { get; set; } public static PixOrCopy CreateCacheIdx(int idx) => - new() + new PixOrCopy { Mode = PixOrCopyMode.CacheIdx, BgraOrDistance = (uint)idx, @@ -23,14 +23,15 @@ public static PixOrCopy CreateCacheIdx(int idx) => }; public static PixOrCopy CreateLiteral(uint bgra) => - new() + new PixOrCopy { Mode = PixOrCopyMode.Literal, BgraOrDistance = bgra, Len = 1 }; - public static PixOrCopy CreateCopy(uint distance, ushort len) => new() + public static PixOrCopy CreateCopy(uint distance, ushort len) => + new PixOrCopy { Mode = PixOrCopyMode.Copy, BgraOrDistance = distance, diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index af472845ac..3da27229ab 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -304,7 +304,7 @@ public void Encode(ImageFrame frame, Stream stream, bool hasAnim if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, @@ -547,7 +547,7 @@ private CrunchConfig[] EncoderAnalyze(ReadOnlySpan bgra, int width, int he EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero); bool doNotCache = false; - List crunchConfigs = new(); + List crunchConfigs = new List(); if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100) { @@ -641,8 +641,8 @@ private void EncodeImage(int width, int height, bool useCache, CrunchConfig conf Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - Vp8LHistogram tmpHisto = new(cacheBits); - List histogramImage = new(histogramImageXySize); + Vp8LHistogram tmpHisto = new Vp8LHistogram(cacheBits); + List histogramImage = new List(histogramImageXySize); for (int i = 0; i < histogramImageXySize; i++) { histogramImage.Add(new Vp8LHistogram(cacheBits)); @@ -839,9 +839,9 @@ private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8L refsTmp1, refsTmp2); - List histogramImage = new() + List histogramImage = new List { - new(cacheBits) + new Vp8LHistogram(cacheBits) }; // Build histogram image and symbols from backward references. @@ -941,7 +941,7 @@ private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; - HuffmanTreeCode huffmanCode = new() + HuffmanTreeCode huffmanCode = new HuffmanTreeCode { NumSymbols = WebpConstants.CodeLengthCodes, CodeLengths = codeLengthBitDepth, @@ -1192,7 +1192,7 @@ private EntropyIx AnalyzeEntropy(ReadOnlySpan bgra, int width, int height, histo[(int)HistoIx.HistoBluePred * 256]++; histo[(int)HistoIx.HistoAlphaPred * 256]++; - Vp8LBitEntropy bitEntropy = new(); + Vp8LBitEntropy bitEntropy = new Vp8LBitEntropy(); for (int j = 0; j < (int)HistoIx.HistoTotal; j++) { bitEntropy.Init(); @@ -1318,7 +1318,7 @@ private bool AnalyzeAndCreatePalette(ReadOnlySpan bgra, int width, int hei /// The number of palette entries. private static int GetColorPalette(ReadOnlySpan bgra, int width, int height, Span palette) { - HashSet colors = new(); + HashSet colors = new HashSet(); for (int y = 0; y < height; y++) { ReadOnlySpan bgraRow = bgra.Slice(y * width, width); diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index 19ea424199..54dd1d6ed1 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -95,7 +95,7 @@ public WebpLosslessDecoder(Vp8LBitReader bitReader, MemoryAllocator memoryAlloca public void Decode(Buffer2D pixels, int width, int height) where TPixel : unmanaged, IPixel { - using (Vp8LDecoder decoder = new(width, height, this.memoryAllocator)) + using (Vp8LDecoder decoder = new Vp8LDecoder(width, height, this.memoryAllocator)) { this.DecodeImageStream(decoder, width, height, true); this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); @@ -616,7 +616,7 @@ private void ReadHuffmanCodeLengths(Span table, int[] codeLengthCod private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) { Vp8LTransformType transformType = (Vp8LTransformType)this.bitReader.ReadValue(2); - Vp8LTransform transform = new(transformType, xSize, ySize); + Vp8LTransform transform = new Vp8LTransform(transformType, xSize, ySize); // Each transform is allowed to be used only once. foreach (Vp8LTransform decoderTransform in decoder.Transforms) diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index a7c96edb7c..52c7e9703b 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -396,7 +396,7 @@ public int MbAnalyzeBestIntra16Mode() this.MakeLuma16Preds(); for (mode = 0; mode < maxMode; mode++) { - Vp8Histogram histo = new(); + Vp8Histogram histo = new Vp8Histogram(); histo.CollectHistogram(this.YuvIn.AsSpan(YOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]), 0, 16); int alpha = histo.GetAlpha(); if (alpha > bestAlpha) @@ -414,7 +414,7 @@ public int MbAnalyzeBestIntra4Mode(int bestAlpha) { Span modes = stackalloc byte[16]; const int maxMode = MaxIntra4Mode; - Vp8Histogram totalHisto = new(); + Vp8Histogram totalHisto = new Vp8Histogram(); int curHisto = 0; this.StartI4(); do @@ -467,7 +467,7 @@ public int MbAnalyzeBestUvMode() this.MakeChroma8Preds(); for (mode = 0; mode < maxMode; ++mode) { - Vp8Histogram histo = new(); + Vp8Histogram histo = new Vp8Histogram(); histo.CollectHistogram(this.YuvIn.AsSpan(UOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]), 16, 16 + 4 + 4); int alpha = histo.GetAlpha(); if (alpha > bestAlpha) diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 40dbb90de6..e62eb6cfc3 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -395,7 +395,7 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni int yStride = width; int uvStride = (yStride + 1) >> 1; - Vp8EncIterator it = new(this); + Vp8EncIterator it = new Vp8EncIterator(this); Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; @@ -416,8 +416,8 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni this.StatLoop(width, height, yStride, uvStride); it.Init(); Vp8EncIterator.InitFilter(); - Vp8ModeScore info = new(); - Vp8Residual residual = new(); + Vp8ModeScore info = new Vp8ModeScore(); + Vp8Residual residual = new Vp8Residual(); do { bool dontUseSkip = !this.Proba.UseSkipProba; @@ -474,7 +474,7 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, @@ -529,7 +529,7 @@ private void StatLoop(int width, int height, int yStride, int uvStride) Vp8RdLevel rdOpt = this.method >= WebpEncodingMethod.Level3 || doSearch ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone; int nbMbs = this.Mbw * this.Mbh; - PassStats stats = new(targetSize, targetPsnr, QMin, QMax, this.quality); + PassStats stats = new PassStats(targetSize, targetPsnr, QMin, QMax, this.quality); this.Proba.ResetTokenStats(); // Fast mode: quick analysis pass over few mbs. Better than nothing. @@ -597,7 +597,7 @@ private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8Rd Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - Vp8EncIterator it = new(this); + Vp8EncIterator it = new Vp8EncIterator(this); long size = 0; long sizeP0 = 0; long distortion = 0; @@ -605,7 +605,7 @@ private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8Rd it.Init(); this.SetLoopParams(stats.Q); - Vp8ModeScore info = new(); + Vp8ModeScore info = new Vp8ModeScore(); do { info.Clear(); @@ -1167,7 +1167,7 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd) { int x, y, ch; - Vp8Residual residual = new(); + Vp8Residual residual = new Vp8Residual(); bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; it.NzToBytes(); diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 7952b15b44..4ac516f055 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -62,7 +62,7 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI // Paragraph 9.2: color space and clamp type follow. sbyte colorSpace = (sbyte)this.bitReader.ReadValue(1); sbyte clampType = (sbyte)this.bitReader.ReadValue(1); - Vp8PictureHeader pictureHeader = new() + Vp8PictureHeader pictureHeader = new Vp8PictureHeader { Width = (uint)width, Height = (uint)height, @@ -73,10 +73,11 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI }; // Paragraph 9.3: Parse the segment header. - Vp8Proba proba = new(); + Vp8Proba proba = new Vp8Proba(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, this.memoryAllocator)) + using (Vp8Decoder decoder = new Vp8Decoder(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, + this.memoryAllocator)) { Vp8Io io = InitializeVp8Io(decoder, pictureHeader); @@ -101,13 +102,8 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI if (info.Features?.Alpha == true) { - using (AlphaDecoder alphaDecoder = new( - width, - height, - alphaData, - info.Features.AlphaChunkHeader, - this.memoryAllocator, - this.configuration)) + using (AlphaDecoder alphaDecoder = new AlphaDecoder(width, height, alphaData, + info.Features.AlphaChunkHeader, this.memoryAllocator, this.configuration)) { alphaDecoder.Decode(); DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); @@ -1062,7 +1058,7 @@ private static int GetLargeValue(Vp8BitReader br, byte[] p) private Vp8SegmentHeader ParseSegmentHeader(Vp8Proba proba) { - Vp8SegmentHeader vp8SegmentHeader = new() + Vp8SegmentHeader vp8SegmentHeader = new Vp8SegmentHeader { UseSegment = this.bitReader.ReadBool() }; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 81a7aebdf9..6922e37d6e 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -153,7 +153,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima } WebpImageInfo? webpInfo = null; - WebpFeatures features = new(); + WebpFeatures features = new WebpFeatures(); switch (chunkType) { case WebpChunkType.Vp8: @@ -178,7 +178,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima ImageFrame imageFrame; if (previousFrame is null) { - image = new(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); + image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); @@ -259,19 +259,21 @@ private byte ReadAlphaData(BufferedReadStream stream) private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - Image decodedImage = new((int)frameData.Width, (int)frameData.Height); + Image decodedImage = new Image((int)frameData.Width, (int)frameData.Height); try { Buffer2D pixelBufferDecoded = decodedImage.GetRootFramePixelBuffer(); if (webpInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = + new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height); } else { - WebpLossyDecoder lossyDecoder = new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + WebpLossyDecoder lossyDecoder = + new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData); } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs deleted file mode 100644 index bfa64b6797..0000000000 --- a/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Webp; - -/// -/// Encoder for animated webp images. -/// -public class WebpAnimationEncoder -{ - // 可能不需要这个屌东西 -} diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 9e9f0f7f62..f4e40090cf 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -8,6 +8,8 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp; @@ -104,16 +106,16 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe WebpThrowHelper.ThrowImageFormatException("bad partition length"); } - Vp8FrameHeader vp8FrameHeader = new() + Vp8FrameHeader vp8FrameHeader = new Vp8FrameHeader { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; - Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; + Vp8BitReader bitReader = new Vp8BitReader(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; - return new() + return new WebpImageInfo { Width = width, Height = height, @@ -137,7 +139,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); - Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); + Vp8LBitReader bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); @@ -166,7 +168,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); } - return new() + return new WebpImageInfo { Width = width, Height = height, @@ -229,7 +231,7 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - WebpImageInfo info = new() + WebpImageInfo info = new WebpImageInfo { Width = width, Height = height, @@ -291,7 +293,7 @@ public static uint ReadChunkSize(BufferedReadStream stream, Span buffer) if (stream.Read(buffer) == 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); - return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; + return chunkSize % 2 == 0 ? chunkSize : chunkSize + 1; } throw new ImageFormatException("Invalid Webp data, could not read chunk size."); @@ -348,7 +350,7 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType if (metadata.ExifProfile != null) { - metadata.ExifProfile = new(exifData); + metadata.ExifProfile = new ExifProfile(exifData); } break; @@ -362,7 +364,7 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType if (metadata.XmpProfile != null) { - metadata.XmpProfile = new(xmpData); + metadata.XmpProfile = new XmpProfile(xmpData); } break; diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs index e23b817ccd..dfbf4ef0e6 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -17,7 +17,7 @@ private WebpDecoder() /// /// Gets the shared instance. /// - public static WebpDecoder Instance { get; } = new(); + public static WebpDecoder Instance { get; } = new WebpDecoder(); /// protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) @@ -25,7 +25,7 @@ protected override ImageInfo Identify(DecoderOptions options, Stream stream, Can Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = options }); + using WebpDecoderCore decoder = new WebpDecoderCore(new WebpDecoderOptions() { GeneralOptions = options }); return decoder.Identify(options.Configuration, stream, cancellationToken); } @@ -35,7 +35,7 @@ protected override Image Decode(WebpDecoderOptions options, Stre Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - using WebpDecoderCore decoder = new(options); + using WebpDecoderCore decoder = new WebpDecoderCore(options); Image image = decoder.Decode(options.GeneralOptions.Configuration, stream, cancellationToken); ScaleToTargetSize(options.GeneralOptions, image); @@ -52,6 +52,5 @@ protected override Image Decode(DecoderOptions options, Stream stream, Cancellat => this.Decode(options, stream, cancellationToken); /// - protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) - => new() { GeneralOptions = options }; + protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new WebpDecoderOptions { GeneralOptions = options }; } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 63d3e1aead..bb54d99a04 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -8,7 +8,9 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp; @@ -71,7 +73,7 @@ public WebpDecoderCore(WebpDecoderOptions options) public DecoderOptions Options { get; } /// - public Size Dimensions => new((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); + public Size Dimensions => new Size((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) @@ -80,7 +82,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken Image? image = null; try { - ImageMetadata metadata = new(); + ImageMetadata metadata = new ImageMetadata(); Span buffer = stackalloc byte[4]; uint fileSize = ReadImageHeader(stream, buffer); @@ -89,7 +91,8 @@ public Image Decode(BufferedReadStream stream, CancellationToken { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames, this.backgroundColorHandling); + using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, + this.configuration, this.maxFrames, this.backgroundColorHandling); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -97,12 +100,14 @@ public Image Decode(BufferedReadStream stream, CancellationToken Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, + this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixels, image.Width, image.Height); } else { - WebpLossyDecoder lossyDecoder = new(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + WebpLossyDecoder lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, + this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); } @@ -127,12 +132,12 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat { ReadImageHeader(stream, stackalloc byte[4]); - ImageMetadata metadata = new(); + ImageMetadata metadata = new ImageMetadata(); using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) { return new ImageInfo( new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), - new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), + new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), metadata); } } @@ -173,7 +178,7 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - WebpFeatures features = new(); + WebpFeatures features = new WebpFeatures(); switch (chunkType) { case WebpChunkType.Vp8: @@ -327,7 +332,7 @@ private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata, return; } - metadata.ExifProfile = new(exifData); + metadata.ExifProfile = new ExifProfile(exifData); } } @@ -354,7 +359,7 @@ private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata, S return; } - metadata.XmpProfile = new(xmpData); + metadata.XmpProfile = new XmpProfile(xmpData); } } @@ -380,7 +385,7 @@ private void ReadIccProfile(BufferedReadStream stream, ImageMetadata metadata, S WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); } - IccProfile profile = new(iccpData); + IccProfile profile = new IccProfile(iccpData); if (profile.CheckIsValid()) { metadata.IccProfile = profile; diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs index 6fb15acbb4..8840805b1f 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; public sealed class WebpDecoderOptions : ISpecializedDecoderOptions { /// - public DecoderOptions GeneralOptions { get; init; } = new(); + public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions(); /// /// Gets the flag to decide how to handle the background color Animation Chunk. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index 29d0c9e3b0..13c9798dbb 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.Configuration); + WebpEncoderCore encoder = new WebpEncoderCore(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 47712071bf..dcff53f3af 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,17 +129,9 @@ public void Encode(Image image, Stream stream, CancellationToken if (lossless) { - using Vp8LEncoder encoder = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.transparentColorMode, - this.nearLossless, - this.nearLosslessQuality); + using Vp8LEncoder encoder = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, + this.nearLossless, this.nearLosslessQuality); bool hasAnimation = image.Frames.Count > 1; encoder.EncodeHeader(image, stream, hasAnimation); @@ -147,17 +139,9 @@ public void Encode(Image image, Stream stream, CancellationToken { foreach (ImageFrame imageFrame in image.Frames) { - using Vp8LEncoder enc = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.transparentColorMode, - this.nearLossless, - this.nearLosslessQuality); + using Vp8LEncoder enc = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, + this.nearLossless, this.nearLosslessQuality); enc.Encode(imageFrame, stream, true); } @@ -171,36 +155,18 @@ public void Encode(Image image, Stream stream, CancellationToken } else { - using Vp8Encoder encoder = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.entropyPasses, - this.filterStrength, - this.spatialNoiseShaping, - this.alphaCompression); + using Vp8Encoder encoder = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, this.filterStrength, + this.spatialNoiseShaping, this.alphaCompression); if (image.Frames.Count > 1) { encoder.EncodeHeader(image, stream, false, true); foreach (ImageFrame imageFrame in image.Frames) { - using Vp8Encoder enc = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.entropyPasses, - this.filterStrength, - this.spatialNoiseShaping, - this.alphaCompression); + using Vp8Encoder enc = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, + this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); enc.EncodeAnimation(imageFrame, stream); } diff --git a/src/ImageSharp/Formats/Webp/WebpFormat.cs b/src/ImageSharp/Formats/Webp/WebpFormat.cs index 29c74b11bf..197041234e 100644 --- a/src/ImageSharp/Formats/Webp/WebpFormat.cs +++ b/src/ImageSharp/Formats/Webp/WebpFormat.cs @@ -15,7 +15,7 @@ private WebpFormat() /// /// Gets the shared instance. /// - public static WebpFormat Instance { get; } = new(); + public static WebpFormat Instance { get; } = new WebpFormat(); /// public string Name => "Webp"; @@ -30,8 +30,8 @@ private WebpFormat() public IEnumerable FileExtensions => WebpConstants.FileExtensions; /// - public WebpMetadata CreateDefaultFormatMetadata() => new(); + public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata(); /// - public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new WebpFrameMetadata(); } From d4483217b623aa751f5591d7edc93a7c812a91b2 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 25 Oct 2023 19:48:07 +0800 Subject: [PATCH 11/19] fix SA1117 --- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 13 ++++- .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 15 ++++- .../Formats/Webp/WebpDecoderCore.cs | 19 ++++-- .../Formats/Webp/WebpEncoderCore.cs | 58 +++++++++++++++---- 4 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index 2084686969..cbd2aa8e7f 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -43,8 +43,17 @@ public static IMemoryOwner EncodeAlpha( { const WebpEncodingMethod effort = WebpEncodingMethod.Default; const int quality = 8 * (int)effort; - using Vp8LEncoder lossLessEncoder = new Vp8LEncoder(memoryAllocator, configuration, width, height, quality, - skipMetadata, effort, WebpTransparentColorMode.Preserve, false, 0); + using Vp8LEncoder lossLessEncoder = new( + memoryAllocator, + configuration, + width, + height, + quality, + skipMetadata, + effort, + WebpTransparentColorMode.Preserve, + false, + 0); // 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, diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 4ac516f055..354bcdbb44 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -76,7 +76,11 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI Vp8Proba proba = new Vp8Proba(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new Vp8Decoder(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, + using (Vp8Decoder decoder = new Vp8Decoder( + info.Vp8FrameHeader, + pictureHeader, + vp8SegmentHeader, + proba, this.memoryAllocator)) { Vp8Io io = InitializeVp8Io(decoder, pictureHeader); @@ -102,8 +106,13 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI if (info.Features?.Alpha == true) { - using (AlphaDecoder alphaDecoder = new AlphaDecoder(width, height, alphaData, - info.Features.AlphaChunkHeader, this.memoryAllocator, this.configuration)) + using (AlphaDecoder alphaDecoder = new AlphaDecoder( + width, + height, + alphaData, + info.Features.AlphaChunkHeader, + this.memoryAllocator, + this.configuration)) { alphaDecoder.Decode(); DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index bb54d99a04..bc875c8890 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -91,8 +91,11 @@ public Image Decode(BufferedReadStream stream, CancellationToken { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, - this.configuration, this.maxFrames, this.backgroundColorHandling); + using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder( + this.memoryAllocator, + this.configuration, + this.maxFrames, + this.backgroundColorHandling); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -100,14 +103,18 @@ public Image Decode(BufferedReadStream stream, CancellationToken Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, - this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder( + this.webImageInfo.Vp8LBitReader, + this.memoryAllocator, + this.configuration); losslessDecoder.Decode(pixels, image.Width, image.Height); } else { - WebpLossyDecoder lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, - this.memoryAllocator, this.configuration); + WebpLossyDecoder lossyDecoder = new WebpLossyDecoder( + this.webImageInfo.Vp8BitReader, + this.memoryAllocator, + this.configuration); lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index dcff53f3af..d945cc3990 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,9 +129,17 @@ public void Encode(Image image, Stream stream, CancellationToken if (lossless) { - using Vp8LEncoder encoder = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, - this.nearLossless, this.nearLosslessQuality); + using Vp8LEncoder encoder = new Vp8LEncoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); bool hasAnimation = image.Frames.Count > 1; encoder.EncodeHeader(image, stream, hasAnimation); @@ -139,9 +147,17 @@ public void Encode(Image image, Stream stream, CancellationToken { foreach (ImageFrame imageFrame in image.Frames) { - using Vp8LEncoder enc = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, - this.nearLossless, this.nearLosslessQuality); + using Vp8LEncoder enc = new Vp8LEncoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); enc.Encode(imageFrame, stream, true); } @@ -155,18 +171,36 @@ public void Encode(Image image, Stream stream, CancellationToken } else { - using Vp8Encoder encoder = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, this.filterStrength, - this.spatialNoiseShaping, this.alphaCompression); + using Vp8Encoder encoder = new Vp8Encoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); if (image.Frames.Count > 1) { encoder.EncodeHeader(image, stream, false, true); foreach (ImageFrame imageFrame in image.Frames) { - using Vp8Encoder enc = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, - this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); + using Vp8Encoder enc = new Vp8Encoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); enc.EncodeAnimation(imageFrame, stream); } From b89bc54aa20b7868010d34734bd1a6ea88be92a2 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 31 Oct 2023 18:27:35 +0800 Subject: [PATCH 12/19] Add new member to WebpFrameMetadata --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 8 +++-- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 30 +++++++++++-------- .../Formats/Webp/Lossy/Vp8Encoder.cs | 26 +++++++++------- .../Formats/Webp/WebpAnimationDecoder.cs | 29 +++++++++--------- ...lendingMethod.cs => WebpBlendingMethod.cs} | 2 +- .../Formats/Webp/WebpDecoderCore.cs | 16 +++++----- ...isposalMethod.cs => WebpDisposalMethod.cs} | 2 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 4 +-- .../Formats/Webp/WebpEncoderCore.cs | 8 ++--- ...AnimationFrameData.cs => WebpFrameData.cs} | 14 ++++----- .../Formats/Webp/WebpFrameMetadata.cs | 19 ++++++++++-- src/ImageSharp/Formats/Webp/WebpMetadata.cs | 9 ++++++ .../Formats/WebP/WebpDecoderTests.cs | 4 +-- 13 files changed, 102 insertions(+), 69 deletions(-) rename src/ImageSharp/Formats/Webp/{AnimationBlendingMethod.cs => WebpBlendingMethod.cs} (95%) rename src/ImageSharp/Formats/Webp/{AnimationDisposalMethod.cs => WebpDisposalMethod.cs} (94%) rename src/ImageSharp/Formats/Webp/{AnimationFrameData.cs => WebpFrameData.cs} (83%) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 89db7ed645..c1860c9c59 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -231,14 +231,14 @@ protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, /// The background color is also used when the Disposal method is 1. /// /// The number of times to loop the animation. If it is 0, this means infinitely. - public static void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) + public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount) { Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, background); + BinaryPrimitives.WriteUInt32LittleEndian(buf, background.ToRgba32().Rgba); stream.Write(buf); BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount); stream.Write(buf[..2]); @@ -249,7 +249,7 @@ public static void WriteAnimationParameter(Stream stream, uint background, ushor /// /// The stream to write to. /// Animation frame data. - public static long WriteAnimationFrame(Stream stream, AnimationFrameData animation) + public static long WriteAnimationFrame(Stream stream, WebpFrameData animation) { Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); @@ -262,6 +262,8 @@ public static long WriteAnimationFrame(Stream stream, AnimationFrameData animati WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); stream.WriteByte(flag); return position; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 3da27229ab..9156d5bdf8 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -235,7 +235,7 @@ public Vp8LEncoder( /// public Vp8LHashChain HashChain { get; } - public void EncodeHeader(Image image, Stream stream, bool hasAnimation, uint background = 0, uint loopCount = 0) + public void EncodeHeader(Image image, Stream stream, bool hasAnimation) where TPixel : unmanaged, IPixel { // Write bytes from the bitwriter buffer to the stream. @@ -257,7 +257,8 @@ public void EncodeHeader(Image image, Stream stream, bool hasAni if (hasAnimation) { - BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount); } } @@ -304,11 +305,14 @@ public void Encode(ImageFrame frame, Stream stream, bool hasAnim if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData + WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, - Duration = frame.Metadata.GetWebpMetadata().FrameDuration + Duration = frameMetadata.FrameDelay, + BlendingMethod = frameMetadata.BlendMethod, + DisposalMethod = frameMetadata.DisposalMethod }); } @@ -547,7 +551,7 @@ private CrunchConfig[] EncoderAnalyze(ReadOnlySpan bgra, int width, int he EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero); bool doNotCache = false; - List crunchConfigs = new List(); + List crunchConfigs = new(); if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100) { @@ -641,8 +645,8 @@ private void EncodeImage(int width, int height, bool useCache, CrunchConfig conf Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - Vp8LHistogram tmpHisto = new Vp8LHistogram(cacheBits); - List histogramImage = new List(histogramImageXySize); + Vp8LHistogram tmpHisto = new(cacheBits); + List histogramImage = new(histogramImageXySize); for (int i = 0; i < histogramImageXySize; i++) { histogramImage.Add(new Vp8LHistogram(cacheBits)); @@ -839,7 +843,7 @@ private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8L refsTmp1, refsTmp2); - List histogramImage = new List + List histogramImage = new() { new Vp8LHistogram(cacheBits) }; @@ -941,7 +945,7 @@ private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; - HuffmanTreeCode huffmanCode = new HuffmanTreeCode + HuffmanTreeCode huffmanCode = new() { NumSymbols = WebpConstants.CodeLengthCodes, CodeLengths = codeLengthBitDepth, @@ -1192,7 +1196,7 @@ private EntropyIx AnalyzeEntropy(ReadOnlySpan bgra, int width, int height, histo[(int)HistoIx.HistoBluePred * 256]++; histo[(int)HistoIx.HistoAlphaPred * 256]++; - Vp8LBitEntropy bitEntropy = new Vp8LBitEntropy(); + Vp8LBitEntropy bitEntropy = new(); for (int j = 0; j < (int)HistoIx.HistoTotal; j++) { bitEntropy.Init(); @@ -1318,7 +1322,7 @@ private bool AnalyzeAndCreatePalette(ReadOnlySpan bgra, int width, int hei /// The number of palette entries. private static int GetColorPalette(ReadOnlySpan bgra, int width, int height, Span palette) { - HashSet colors = new HashSet(); + HashSet colors = new(); for (int y = 0; y < height; y++) { ReadOnlySpan bgraRow = bgra.Slice(y * width, width); @@ -1870,9 +1874,9 @@ public void AllocateTransformBuffer(int width, int height) /// public void ClearRefs() { - for (int i = 0; i < this.Refs.Length; i++) + foreach (Vp8LBackwardRefs t in this.Refs) { - this.Refs[i].Refs.Clear(); + t.Refs.Clear(); } } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index e62eb6cfc3..3a6e9a2ccd 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -309,7 +309,7 @@ public Vp8Encoder( /// private int MbHeaderLimit { get; } - public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation, uint background = 0, uint loopCount = 0) + public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation) where TPixel : unmanaged, IPixel { // Write bytes from the bitwriter buffer to the stream. @@ -331,7 +331,8 @@ public void EncodeHeader(Image image, Stream stream, bool hasAlp if (hasAnimation) { - BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount); } } @@ -395,7 +396,7 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni int yStride = width; int uvStride = (yStride + 1) >> 1; - Vp8EncIterator it = new Vp8EncIterator(this); + Vp8EncIterator it = new(this); Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; @@ -416,8 +417,8 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni this.StatLoop(width, height, yStride, uvStride); it.Init(); Vp8EncIterator.InitFilter(); - Vp8ModeScore info = new Vp8ModeScore(); - Vp8Residual residual = new Vp8Residual(); + Vp8ModeScore info = new(); + Vp8Residual residual = new(); do { bool dontUseSkip = !this.Proba.UseSkipProba; @@ -474,11 +475,14 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData + WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, - Duration = frame.Metadata.GetWebpMetadata().FrameDuration + Duration = frameMetadata.FrameDelay, + BlendingMethod = frameMetadata.BlendMethod, + DisposalMethod = frameMetadata.DisposalMethod }); } @@ -529,7 +533,7 @@ private void StatLoop(int width, int height, int yStride, int uvStride) Vp8RdLevel rdOpt = this.method >= WebpEncodingMethod.Level3 || doSearch ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone; int nbMbs = this.Mbw * this.Mbh; - PassStats stats = new PassStats(targetSize, targetPsnr, QMin, QMax, this.quality); + PassStats stats = new(targetSize, targetPsnr, QMin, QMax, this.quality); this.Proba.ResetTokenStats(); // Fast mode: quick analysis pass over few mbs. Better than nothing. @@ -597,7 +601,7 @@ private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8Rd Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - Vp8EncIterator it = new Vp8EncIterator(this); + Vp8EncIterator it = new(this); long size = 0; long sizeP0 = 0; long distortion = 0; @@ -605,7 +609,7 @@ private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8Rd it.Init(); this.SetLoopParams(stats.Q); - Vp8ModeScore info = new Vp8ModeScore(); + Vp8ModeScore info = new(); do { info.Clear(); @@ -1167,7 +1171,7 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd) { int x, y, ch; - Vp8Residual residual = new Vp8Residual(); + Vp8Residual residual = new(); bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; it.NzToBytes(); diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 6922e37d6e..87657dfabb 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -138,7 +138,7 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat private uint ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, uint width, uint height, Color backgroundColor) where TPixel : unmanaged, IPixel { - AnimationFrameData frameData = AnimationFrameData.Parse(stream); + WebpFrameData frameData = WebpFrameData.Parse(stream); long streamStartPosition = stream.Position; Span buffer = stackalloc byte[4]; @@ -153,7 +153,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima } WebpImageInfo? webpInfo = null; - WebpFeatures features = new WebpFeatures(); + WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: @@ -180,7 +180,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima { image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); - SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); + SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData); imageFrame = image.Frames.RootFrame; } @@ -188,7 +188,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima { currentFrame = image!.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection. - SetFrameMetadata(currentFrame.Metadata, frameData.Duration); + SetFrameMetadata(currentFrame.Metadata, frameData); imageFrame = currentFrame; } @@ -199,7 +199,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima int frameHeight = (int)frameData.Height; Rectangle regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight); - if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose) + if (frameData.DisposalMethod is WebpDisposalMethod.Dispose) { this.RestoreToBackground(imageFrame, backgroundColor); } @@ -207,7 +207,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight); - if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending) + if (previousFrame != null && frameData.BlendingMethod is WebpBlendingMethod.AlphaBlending) { this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight); } @@ -222,12 +222,13 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima /// Sets the frames metadata. /// /// The metadata. - /// The frame duration. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetFrameMetadata(ImageFrameMetadata meta, uint duration) + /// The frame data. + private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData frameData) { WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); - frameMetadata.FrameDuration = duration; + frameMetadata.FrameDelay = frameData.Duration; + frameMetadata.BlendMethod = frameData.BlendingMethod; + frameMetadata.DisposalMethod = frameData.DisposalMethod; } /// @@ -256,10 +257,10 @@ private byte ReadAlphaData(BufferedReadStream stream) /// The frame data. /// The webp information. /// A decoded image. - private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) + private Buffer2D DecodeImageData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - Image decodedImage = new Image((int)frameData.Width, (int)frameData.Height); + Image decodedImage = new((int)frameData.Width, (int)frameData.Height); try { @@ -267,13 +268,13 @@ private Buffer2D DecodeImageData(AnimationFrameData frameData, W if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = - new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height); } else { WebpLossyDecoder lossyDecoder = - new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData); } diff --git a/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs similarity index 95% rename from src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs rename to src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs index 99b2462cea..cbd0e9a8cc 100644 --- a/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs +++ b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// -internal enum AnimationBlendingMethod +public enum WebpBlendingMethod { /// /// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending. diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index bc875c8890..de188b137b 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -73,7 +73,7 @@ public WebpDecoderCore(WebpDecoderOptions options) public DecoderOptions Options { get; } /// - public Size Dimensions => new Size((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); + public Size Dimensions => new((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) @@ -82,7 +82,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken Image? image = null; try { - ImageMetadata metadata = new ImageMetadata(); + ImageMetadata metadata = new(); Span buffer = stackalloc byte[4]; uint fileSize = ReadImageHeader(stream, buffer); @@ -91,7 +91,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder( + using WebpAnimationDecoder animationDecoder = new( this.memoryAllocator, this.configuration, this.maxFrames, @@ -103,7 +103,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder( + WebpLosslessDecoder losslessDecoder = new( this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); @@ -111,7 +111,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } else { - WebpLossyDecoder lossyDecoder = new WebpLossyDecoder( + WebpLossyDecoder lossyDecoder = new( this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); @@ -139,7 +139,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat { ReadImageHeader(stream, stackalloc byte[4]); - ImageMetadata metadata = new ImageMetadata(); + ImageMetadata metadata = new(); using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) { return new ImageInfo( @@ -185,7 +185,7 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - WebpFeatures features = new WebpFeatures(); + WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: @@ -392,7 +392,7 @@ private void ReadIccProfile(BufferedReadStream stream, ImageMetadata metadata, S WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); } - IccProfile profile = new IccProfile(iccpData); + IccProfile profile = new(iccpData); if (profile.CheckIsValid()) { metadata.IccProfile = profile; diff --git a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs similarity index 94% rename from src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs rename to src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs index 23bc37c283..d409973a99 100644 --- a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs +++ b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// -internal enum AnimationDisposalMethod +public enum WebpDisposalMethod { /// /// Do not dispose. Leave the canvas as is. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index 13c9798dbb..bc93df3a5b 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; - namespace SixLabors.ImageSharp.Formats.Webp; /// @@ -82,7 +80,7 @@ public sealed class WebpEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - WebpEncoderCore encoder = new WebpEncoderCore(this, image.Configuration); + WebpEncoderCore encoder = new(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index d945cc3990..47712071bf 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,7 +129,7 @@ public void Encode(Image image, Stream stream, CancellationToken if (lossless) { - using Vp8LEncoder encoder = new Vp8LEncoder( + using Vp8LEncoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -147,7 +147,7 @@ public void Encode(Image image, Stream stream, CancellationToken { foreach (ImageFrame imageFrame in image.Frames) { - using Vp8LEncoder enc = new Vp8LEncoder( + using Vp8LEncoder enc = new( this.memoryAllocator, this.configuration, image.Width, @@ -171,7 +171,7 @@ public void Encode(Image image, Stream stream, CancellationToken } else { - using Vp8Encoder encoder = new Vp8Encoder( + using Vp8Encoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -189,7 +189,7 @@ public void Encode(Image image, Stream stream, CancellationToken foreach (ImageFrame imageFrame in image.Frames) { - using Vp8Encoder enc = new Vp8Encoder( + using Vp8Encoder enc = new( this.memoryAllocator, this.configuration, image.Width, diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/WebpFrameData.cs similarity index 83% rename from src/ImageSharp/Formats/Webp/AnimationFrameData.cs rename to src/ImageSharp/Formats/Webp/WebpFrameData.cs index 27a1815fe3..e2bcfd7c36 100644 --- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameData.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; -internal struct AnimationFrameData +internal struct WebpFrameData { /// /// The animation chunk size. @@ -46,23 +46,23 @@ internal struct AnimationFrameData /// /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// - public AnimationBlendingMethod BlendingMethod; + public WebpBlendingMethod BlendingMethod; /// /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// - public AnimationDisposalMethod DisposalMethod; + public WebpDisposalMethod DisposalMethod; /// /// Reads the animation frame header. /// /// The stream to read from. /// Animation frame data. - public static AnimationFrameData Parse(BufferedReadStream stream) + public static WebpFrameData Parse(BufferedReadStream stream) { Span buffer = stackalloc byte[4]; - AnimationFrameData data = new AnimationFrameData + WebpFrameData data = new() { DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), @@ -83,8 +83,8 @@ public static AnimationFrameData Parse(BufferedReadStream stream) }; byte flags = (byte)stream.ReadByte(); - data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; - data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; + data.DisposalMethod = (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose; + data.BlendingMethod = (flags & (1 << 1)) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending; return data; } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index bce1b09d6f..ef21d8b6fe 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -19,13 +19,28 @@ public WebpFrameMetadata() /// Initializes a new instance of the class. /// /// The metadata to create an instance from. - private WebpFrameMetadata(WebpFrameMetadata other) => this.FrameDuration = other.FrameDuration; + private WebpFrameMetadata(WebpFrameMetadata other) + { + this.FrameDelay = other.FrameDelay; + this.DisposalMethod = other.DisposalMethod; + this.BlendMethod = other.BlendMethod; + } + + /// + /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// + public WebpBlendingMethod BlendMethod { get; set; } + + /// + /// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// + public WebpDisposalMethod DisposalMethod { get; set; } /// /// Gets or sets the frame duration. The time to wait before displaying the next frame, /// in 1 millisecond units. Note the interpretation of frame duration of 0 (and often smaller and equal to 10) is implementation defined. /// - public uint FrameDuration { get; set; } + public uint FrameDelay { get; set; } /// public IDeepCloneable DeepClone() => new WebpFrameMetadata(this); diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index 5d1051c751..a6bb0a7b80 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -23,6 +23,7 @@ private WebpMetadata(WebpMetadata other) { this.FileFormat = other.FileFormat; this.AnimationLoopCount = other.AnimationLoopCount; + this.AnimationBackground = other.AnimationBackground; } /// @@ -35,6 +36,14 @@ private WebpMetadata(WebpMetadata other) /// public ushort AnimationLoopCount { get; set; } = 1; + /// + /// Gets or sets the default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + public Color AnimationBackground { get; set; } + /// public IDeepCloneable DeepClone() => new WebpMetadata(this); } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index c0fc00b82d..c3a777c153 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -308,7 +308,7 @@ public void Decode_AnimatedLossless_VerifyAllFrames(TestImageProvider(TestImageProvider Date: Wed, 1 Nov 2023 22:46:20 +0800 Subject: [PATCH 13/19] fix --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 1 - .../Formats/Webp/Lossless/Vp8LEncoder.cs | 4 + .../Webp/Lossless/WebpLosslessDecoder.cs | 115 +++++----- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 + .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 198 +++++++++--------- .../Formats/Webp/WebpAnimationDecoder.cs | 53 ++--- src/ImageSharp/Formats/Webp/WebpFrameData.cs | 2 + 7 files changed, 188 insertions(+), 189 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index c1860c9c59..cbf96a91af 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -263,7 +263,6 @@ public static long WriteAnimationFrame(Stream stream, WebpFrameData animation) WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); - // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); stream.WriteByte(flag); return position; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 9156d5bdf8..42aa667ac5 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -306,8 +306,12 @@ public void Encode(ImageFrame frame, Stream stream, bool hasAnim if (hasAnimation) { WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { + X = 0, + Y = 0, Width = (uint)frame.Width, Height = (uint)frame.Height, Duration = frameMetadata.FrameDelay, diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index 54dd1d6ed1..e4c2a7ddf6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -95,12 +95,10 @@ public WebpLosslessDecoder(Vp8LBitReader bitReader, MemoryAllocator memoryAlloca public void Decode(Buffer2D pixels, int width, int height) where TPixel : unmanaged, IPixel { - using (Vp8LDecoder decoder = new Vp8LDecoder(width, height, this.memoryAllocator)) - { - this.DecodeImageStream(decoder, width, height, true); - this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); - this.DecodePixelValues(decoder, pixels, width, height); - } + using Vp8LDecoder decoder = new(width, height, this.memoryAllocator); + this.DecodeImageStream(decoder, width, height, true); + this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); + this.DecodePixelValues(decoder, pixels, width, height); } public IMemoryOwner DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0) @@ -616,15 +614,12 @@ private void ReadHuffmanCodeLengths(Span table, int[] codeLengthCod private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) { Vp8LTransformType transformType = (Vp8LTransformType)this.bitReader.ReadValue(2); - Vp8LTransform transform = new Vp8LTransform(transformType, xSize, ySize); + Vp8LTransform transform = new(transformType, xSize, ySize); // Each transform is allowed to be used only once. - foreach (Vp8LTransform decoderTransform in decoder.Transforms) + if (decoder.Transforms.Any(decoderTransform => decoderTransform.TransformType == transform.TransformType)) { - if (decoderTransform.TransformType == transform.TransformType) - { - WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once"); - } + WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once"); } switch (transformType) @@ -744,61 +739,69 @@ public void DecodeAlphaData(AlphaDecoder dec) this.bitReader.FillBitWindow(); int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]); - if (code < WebpConstants.NumLiteralCodes) + switch (code) { - // Literal - data[pos] = (byte)code; - ++pos; - ++col; - - if (col >= width) + case < WebpConstants.NumLiteralCodes: { - col = 0; - ++row; - if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + // Literal + data[pos] = (byte)code; + ++pos; + ++col; + + if (col >= width) { - dec.ExtractPalettedAlphaRows(row); + col = 0; + ++row; + if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } } - } - } - else if (code < lenCodeLimit) - { - // Backward reference - int lengthSym = code - WebpConstants.NumLiteralCodes; - int length = this.GetCopyLength(lengthSym); - int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]); - this.bitReader.FillBitWindow(); - int distCode = this.GetCopyDistance(distSymbol); - int dist = PlaneCodeToDistance(width, distCode); - if (pos >= dist && end - pos >= length) - { - CopyBlock8B(data, pos, dist, length); - } - else - { - WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data"); + + break; } - pos += length; - col += length; - while (col >= width) + case < lenCodeLimit: { - col -= width; - ++row; - if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + // Backward reference + int lengthSym = code - WebpConstants.NumLiteralCodes; + int length = this.GetCopyLength(lengthSym); + int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]); + this.bitReader.FillBitWindow(); + int distCode = this.GetCopyDistance(distSymbol); + int dist = PlaneCodeToDistance(width, distCode); + if (pos >= dist && end - pos >= length) { - dec.ExtractPalettedAlphaRows(row); + CopyBlock8B(data, pos, dist, length); + } + else + { + WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data"); } - } - if (pos < last && (col & mask) > 0) - { - htreeGroup = GetHTreeGroupForPos(hdr, col, row); + pos += length; + col += length; + while (col >= width) + { + col -= width; + ++row; + if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } + } + + if (pos < last && (col & mask) > 0) + { + htreeGroup = GetHTreeGroupForPos(hdr, col, row); + } + + break; } - } - else - { - WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + + default: + WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + break; } this.bitReader.Eos = this.bitReader.IsEndOfStream(); diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 3a6e9a2ccd..3b73023062 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -476,8 +476,12 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni if (hasAnimation) { WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { + X = 0, + Y = 0, Width = (uint)frame.Width, Height = (uint)frame.Height, Duration = frameMetadata.FrameDelay, diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 354bcdbb44..3eb03b1724 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -62,7 +62,7 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI // Paragraph 9.2: color space and clamp type follow. sbyte colorSpace = (sbyte)this.bitReader.ReadValue(1); sbyte clampType = (sbyte)this.bitReader.ReadValue(1); - Vp8PictureHeader pictureHeader = new Vp8PictureHeader + Vp8PictureHeader pictureHeader = new() { Width = (uint)width, Height = (uint)height, @@ -73,55 +73,51 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI }; // Paragraph 9.3: Parse the segment header. - Vp8Proba proba = new Vp8Proba(); + Vp8Proba proba = new(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new Vp8Decoder( - info.Vp8FrameHeader, - pictureHeader, - vp8SegmentHeader, - proba, - this.memoryAllocator)) - { - Vp8Io io = InitializeVp8Io(decoder, pictureHeader); + using Vp8Decoder decoder = new( + info.Vp8FrameHeader, + pictureHeader, + vp8SegmentHeader, + proba, + this.memoryAllocator); + Vp8Io io = InitializeVp8Io(decoder, pictureHeader); - // Paragraph 9.4: Parse the filter specs. - this.ParseFilterHeader(decoder); - decoder.PrecomputeFilterStrengths(); + // Paragraph 9.4: Parse the filter specs. + this.ParseFilterHeader(decoder); + decoder.PrecomputeFilterStrengths(); - // Paragraph 9.5: Parse partitions. - this.ParsePartitions(decoder); + // Paragraph 9.5: Parse partitions. + this.ParsePartitions(decoder); - // Paragraph 9.6: Dequantization Indices. - this.ParseDequantizationIndices(decoder); + // Paragraph 9.6: Dequantization Indices. + this.ParseDequantizationIndices(decoder); - // Ignore the value of update probabilities. - this.bitReader.ReadBool(); + // Ignore the value of update probabilities. + this.bitReader.ReadBool(); - // Paragraph 13.4: Parse probabilities. - this.ParseProbabilities(decoder); + // Paragraph 13.4: Parse probabilities. + this.ParseProbabilities(decoder); - // Decode image data. - this.ParseFrame(decoder, io); + // Decode image data. + this.ParseFrame(decoder, io); - if (info.Features?.Alpha == true) - { - using (AlphaDecoder alphaDecoder = new AlphaDecoder( - width, - height, - alphaData, - info.Features.AlphaChunkHeader, - this.memoryAllocator, - this.configuration)) - { - alphaDecoder.Decode(); - DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); - } - } - else - { - this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels); - } + if (info.Features?.Alpha == true) + { + using AlphaDecoder alphaDecoder = new( + width, + height, + alphaData, + info.Features.AlphaChunkHeader, + this.memoryAllocator, + this.configuration); + alphaDecoder.Decode(); + DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); + } + else + { + this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels); } } @@ -199,8 +195,8 @@ private void ParseIntraMode(Vp8Decoder dec, int mbX) { // Hardcoded tree parsing. block.Segment = this.bitReader.GetBit((int)dec.Probabilities.Segments[0]) == 0 - ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1]) - : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2); + ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1]) + : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2); } else { @@ -595,57 +591,65 @@ private static void DoFilter(Vp8Decoder dec, int mbx, int mby) return; } - if (dec.Filter == LoopFilter.Simple) + switch (dec.Filter) { - int offset = dec.CacheYOffset + (mbx * 16); - if (mbx > 0) + case LoopFilter.Simple: { - LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); - } + int offset = dec.CacheYOffset + (mbx * 16); + if (mbx > 0) + { + LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); - } + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } - if (mby > 0) - { - LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); - } + if (mby > 0) + { + LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); - } - } - else if (dec.Filter == LoopFilter.Complex) - { - int uvBps = dec.CacheUvStride; - int yOffset = dec.CacheYOffset + (mbx * 16); - int uvOffset = dec.CacheUvOffset + (mbx * 8); - int hevThresh = filterInfo.HighEdgeVarianceThreshold; - if (mbx > 0) - { - LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); - LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); - } + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); - LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + break; } - if (mby > 0) + case LoopFilter.Complex: { - LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); - LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); - } + int uvBps = dec.CacheUvStride; + int yOffset = dec.CacheYOffset + (mbx * 16); + int uvOffset = dec.CacheUvOffset + (mbx * 8); + int hevThresh = filterInfo.HighEdgeVarianceThreshold; + if (mbx > 0) + { + LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); - LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + if (filterInfo.UseInnerFiltering) + { + LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + + if (mby > 0) + { + LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + + break; } } } @@ -1067,7 +1071,7 @@ private static int GetLargeValue(Vp8BitReader br, byte[] p) private Vp8SegmentHeader ParseSegmentHeader(Vp8Proba proba) { - Vp8SegmentHeader vp8SegmentHeader = new Vp8SegmentHeader + Vp8SegmentHeader vp8SegmentHeader = new() { UseSegment = this.bitReader.ReadBool() }; @@ -1333,18 +1337,12 @@ private static Vp8Io InitializeVp8Io(Vp8Decoder dec, Vp8PictureHeader pictureHea private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz) { nzCoeffs <<= 2; - if (nz > 3) + nzCoeffs |= nz switch { - nzCoeffs |= 3; - } - else if (nz > 1) - { - nzCoeffs |= 2; - } - else - { - nzCoeffs |= (uint)dcNz; - } + > 3 => 3, + > 1 => 2, + _ => (uint)dcNz + }; return nzCoeffs; } @@ -1358,13 +1356,13 @@ private static int CheckMode(int mbx, int mby, int mode) if (mbx == 0) { return mby == 0 - ? 6 // B_DC_PRED_NOTOPLEFT - : 5; // B_DC_PRED_NOLEFT + ? 6 // B_DC_PRED_NOTOPLEFT + : 5; // B_DC_PRED_NOLEFT } return mby == 0 - ? 4 // B_DC_PRED_NOTOP - : 0; // B_DC_PRED + ? 4 // B_DC_PRED_NOTOP + : 0; // B_DC_PRED } return mode; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 87657dfabb..fad6ca16cc 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -193,11 +192,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima imageFrame = currentFrame; } - int frameX = (int)(frameData.X * 2); - int frameY = (int)(frameData.Y * 2); - int frameWidth = (int)frameData.Width; - int frameHeight = (int)frameData.Height; - Rectangle regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight); + Rectangle regionRectangle = frameData.Bounds; if (frameData.DisposalMethod is WebpDisposalMethod.Dispose) { @@ -205,11 +200,11 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima } using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); - DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight); + DrawDecodedImageOnCanvas(decodedImage, imageFrame, regionRectangle); if (previousFrame != null && frameData.BlendingMethod is WebpBlendingMethod.AlphaBlending) { - this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight); + this.AlphaBlend(previousFrame, imageFrame, regionRectangle); } previousFrame = currentFrame ?? image.Frames.RootFrame; @@ -245,7 +240,7 @@ private byte ReadAlphaData(BufferedReadStream stream) byte alphaChunkHeader = (byte)stream.ReadByte(); Span alphaData = this.alphaData.GetSpan(); - stream.Read(alphaData, 0, alphaDataSize); + _ = stream.Read(alphaData, 0, alphaDataSize); return alphaChunkHeader; } @@ -260,11 +255,11 @@ private byte ReadAlphaData(BufferedReadStream stream) private Buffer2D DecodeImageData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - Image decodedImage = new((int)frameData.Width, (int)frameData.Height); + ImageFrame decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height); try { - Buffer2D pixelBufferDecoded = decodedImage.GetRootFramePixelBuffer(); + Buffer2D pixelBufferDecoded = decodedFrame.PixelBuffer; if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = @@ -282,7 +277,7 @@ private Buffer2D DecodeImageData(WebpFrameData frameData, WebpIm } catch { - decodedImage?.Dispose(); + decodedFrame?.Dispose(); throw; } finally @@ -297,20 +292,17 @@ private Buffer2D DecodeImageData(WebpFrameData frameData, WebpIm /// The type of the pixel. /// The decoded image. /// The image frame to draw into. - /// The frame x coordinate. - /// The frame y coordinate. - /// The width of the frame. - /// The height of the frame. - private static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, int frameX, int frameY, int frameWidth, int frameHeight) + /// The area of the frame. + private static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, Rectangle restoreArea) where TPixel : unmanaged, IPixel { - Buffer2D imageFramePixels = imageFrame.PixelBuffer; + Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea); int decodedRowIdx = 0; - for (int y = frameY; y < frameY + frameHeight; y++) + for (int y = 0; y < restoreArea.Height; y++) { Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..frameWidth]; - decodedPixelRow.TryCopyTo(framePixelRow[frameX..]); + Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..restoreArea.Width]; + decodedPixelRow.TryCopyTo(framePixelRow); } } @@ -321,22 +313,19 @@ private static void DrawDecodedImageOnCanvas(Buffer2D decodedIma /// The pixel format. /// The source image. /// The destination image. - /// The frame x coordinate. - /// The frame y coordinate. - /// The width of the frame. - /// The height of the frame. - private void AlphaBlend(ImageFrame src, ImageFrame dst, int frameX, int frameY, int frameWidth, int frameHeight) + /// The area of the frame. + private void AlphaBlend(ImageFrame src, ImageFrame dst, Rectangle restoreArea) where TPixel : unmanaged, IPixel { - Buffer2D srcPixels = src.PixelBuffer; - Buffer2D dstPixels = dst.PixelBuffer; + Buffer2DRegion srcPixels = src.PixelBuffer.GetRegion(restoreArea); + Buffer2DRegion dstPixels = dst.PixelBuffer.GetRegion(restoreArea); PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); - for (int y = frameY; y < frameY + frameHeight; y++) + for (int y = 0; y < restoreArea.Height; y++) { - Span srcPixelRow = srcPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); - Span dstPixelRow = dstPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); + Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); + Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); - blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f); + blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1f); } } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/WebpFrameData.cs index e2bcfd7c36..93c5d10dcd 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameData.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameData.cs @@ -53,6 +53,8 @@ internal struct WebpFrameData /// public WebpDisposalMethod DisposalMethod; + public readonly Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); + /// /// Reads the animation frame header. /// From 296da738008b06ea511b4e0123d059aa4e660a89 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 14:26:50 +0800 Subject: [PATCH 14/19] add riif helper --- src/ImageSharp/Common/Helpers/RiffHelper.cs | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/ImageSharp/Common/Helpers/RiffHelper.cs diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs new file mode 100644 index 0000000000..6354ebd663 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Text; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal class RiffHelper +{ + /// + /// The header bytes identifying RIFF file. + /// + public static readonly uint RiffFourCc = 0x52_49_46_46; + + public static void WriteRiffFile(Stream stream, string formType, Action func) => + WriteChunk(stream, RiffFourCc, s => + { + s.Write(Encoding.ASCII.GetBytes(formType)); + func(s); + }); + + public static void WriteChunk(Stream stream, uint fourCc, Action func) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + + func(stream); + + long position = stream.Position; + stream.Position = sizePosition; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 == 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Write(buffer); + + stream.Position = position; + } + + public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + stream.Write(buffer); + uint size = (uint)data.Length; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); + stream.Write(buffer); + stream.Write(data); + + // padding + if (size % 2 == 1) + { + stream.WriteByte(0); + } + } + + public static unsafe void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) + where TStruct : unmanaged + { + fixed (TStruct* ptr = &chunk) + { + WriteChunk(stream, fourCc, new Span(ptr, sizeof(TStruct))); + } + } + + public static long BeginWriteChunk(Stream stream, uint fourCc) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + + return sizePosition; + } + + public static void EndWriteChunk(Stream stream, long sizePosition) + { + Span buffer = stackalloc byte[4]; + + long position = stream.Position; + stream.Position = sizePosition; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 == 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Write(buffer); + + stream.Position = position; + } +} From 9dc0cda95e1c810f3bd56db1803fc0aa6cea4a09 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 15:06:57 +0800 Subject: [PATCH 15/19] add static --- src/ImageSharp/Common/Helpers/RiffHelper.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs index 6354ebd663..0395d9a9c9 100644 --- a/src/ImageSharp/Common/Helpers/RiffHelper.cs +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -6,12 +6,12 @@ namespace SixLabors.ImageSharp.Common.Helpers; -internal class RiffHelper +internal static class RiffHelper { /// /// The header bytes identifying RIFF file. /// - public static readonly uint RiffFourCc = 0x52_49_46_46; + private const uint RiffFourCc = 0x52_49_46_46; public static void WriteRiffFile(Stream stream, string formType, Action func) => WriteChunk(stream, RiffFourCc, s => @@ -64,7 +64,7 @@ public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan dat stream.Write(data); // padding - if (size % 2 == 1) + if (size % 2 is 1) { stream.WriteByte(0); } @@ -103,7 +103,7 @@ public static void EndWriteChunk(Stream stream, long sizePosition) uint dataSize = (uint)(position - sizePosition - 4); // padding - if (dataSize % 2 == 1) + if (dataSize % 2 is 1) { stream.WriteByte(0); position++; @@ -114,4 +114,13 @@ public static void EndWriteChunk(Stream stream, long sizePosition) stream.Position = position; } + + public static long BeginWriteRiffFile(Stream stream, string formType) + { + long sizePosition = BeginWriteChunk(stream, RiffFourCc); + stream.Write(Encoding.ASCII.GetBytes(formType)); + return sizePosition; + } + + public static void EndWriteRiffFile(Stream stream, long sizePosition) => EndWriteChunk(stream, sizePosition); } From 4beafcf65586ff2631fefd2d6b1aedc1422ab4c8 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 17:16:44 +0800 Subject: [PATCH 16/19] fix FourCC bug --- src/ImageSharp/Common/Helpers/RiffHelper.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs index 0395d9a9c9..8f06e5886f 100644 --- a/src/ImageSharp/Common/Helpers/RiffHelper.cs +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -25,7 +25,7 @@ public static void WriteChunk(Stream stream, uint fourCc, Action func) Span buffer = stackalloc byte[4]; // write the fourCC - BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); stream.Write(buffer); long sizePosition = stream.Position; @@ -34,7 +34,6 @@ public static void WriteChunk(Stream stream, uint fourCc, Action func) func(stream); long position = stream.Position; - stream.Position = sizePosition; uint dataSize = (uint)(position - sizePosition - 4); @@ -46,8 +45,8 @@ public static void WriteChunk(Stream stream, uint fourCc, Action func) } BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; stream.Write(buffer); - stream.Position = position; } @@ -56,7 +55,7 @@ public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan dat Span buffer = stackalloc byte[4]; // write the fourCC - BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); stream.Write(buffer); uint size = (uint)data.Length; BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); @@ -84,7 +83,7 @@ public static long BeginWriteChunk(Stream stream, uint fourCc) Span buffer = stackalloc byte[4]; // write the fourCC - BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); stream.Write(buffer); long sizePosition = stream.Position; @@ -98,7 +97,6 @@ public static void EndWriteChunk(Stream stream, long sizePosition) Span buffer = stackalloc byte[4]; long position = stream.Position; - stream.Position = sizePosition; uint dataSize = (uint)(position - sizePosition - 4); @@ -110,8 +108,8 @@ public static void EndWriteChunk(Stream stream, long sizePosition) } BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; stream.Write(buffer); - stream.Position = position; } From 1b0b877a147e81b9c3ff9d3e90de668b4e13cf1d Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 19:18:56 +0800 Subject: [PATCH 17/19] introduce RiffHelper --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 222 ++---------------- .../Webp/Chunks/WebpAnimationParameter.cs | 37 +++ .../Formats/Webp/Chunks/WebpFrameData.cs | 140 +++++++++++ .../Formats/Webp/Chunks/WebpVp8X.cs | 113 +++++++++ .../Formats/Webp/Lossless/Vp8LEncoder.cs | 23 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 23 +- .../Formats/Webp/WebpAnimationDecoder.cs | 3 +- .../Formats/Webp/WebpChunkParsingUtils.cs | 18 +- src/ImageSharp/Formats/Webp/WebpChunkType.cs | 2 +- src/ImageSharp/Formats/Webp/WebpConstants.cs | 10 +- src/ImageSharp/Formats/Webp/WebpFrameData.cs | 93 -------- 11 files changed, 349 insertions(+), 335 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs create mode 100644 src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs create mode 100644 src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs delete mode 100644 src/ImageSharp/Formats/Webp/WebpFrameData.cs diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index cbf96a91af..d502fd6063 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -1,8 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers.Binary; -using System.Runtime.InteropServices; +using System.Diagnostics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -15,8 +16,6 @@ internal abstract class BitWriterBase private const ulong MaxCanvasPixels = 4294967295ul; - protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize; - /// /// Buffer to write to. /// @@ -79,48 +78,6 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired) Array.Resize(ref this.buffer, newSize); } - /// - /// Writes the RIFF header to the stream. - /// - /// The stream to write to. - /// The block length. - protected static void WriteRiffHeader(Stream stream, uint riffSize) - { - stream.Write(WebpConstants.RiffFourCc); - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize); - stream.Write(buf); - stream.Write(WebpConstants.WebpHeader); - } - - /// - /// Calculates the chunk size of EXIF, XMP or ICCP metadata. - /// - /// The metadata profile bytes. - /// The metadata chunk size in bytes. - protected static uint MetadataChunkSize(byte[] metadataBytes) - { - uint metaSize = (uint)metadataBytes.Length; - return WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1); - } - - /// - /// Calculates the chunk size of a alpha chunk. - /// - /// The alpha chunk bytes. - /// The alpha data chunk size in bytes. - protected static uint AlphaChunkSize(Span alphaBytes) - { - uint alphaSize = (uint)alphaBytes.Length + 1; - return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); - } - - /// - /// Overwrites ides the write file size. - /// - /// The stream to write to. - protected static void OverwriteFileSize(Stream stream) => OverwriteFrameSize(stream, 4); - /// /// Write the trunks before data trunk. /// @@ -143,7 +100,9 @@ public static void WriteTrunksBeforeData( bool hasAnimation) { // Write file size later - WriteRiffHeader(stream, 0); + long pos = RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc); + + Debug.Assert(pos is 4, "Stream should be written from position 0."); // Write VP8X, header if necessary. bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation; @@ -153,7 +112,7 @@ public static void WriteTrunksBeforeData( if (iccProfile != null) { - WriteColorProfile(stream, iccProfile.ToByteArray()); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray()); } } } @@ -177,49 +136,17 @@ public static void WriteTrunksAfterData( { if (exifProfile != null) { - WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray()); } if (xmpProfile != null) { - WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data); } - OverwriteFileSize(stream); - } - - /// - /// Writes a metadata profile (EXIF or XMP) to the stream. - /// - /// The stream to write to. - /// The metadata profile's bytes. - /// The chuck type to write. - protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) - { - DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); - - uint size = (uint)metadataBytes.Length; - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, size); - stream.Write(buf); - stream.Write(metadataBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } + RiffHelper.EndWriteRiffFile(stream, 4); } - /// - /// Writes the color profile() to the stream. - /// - /// The stream to write to. - /// The color profile bytes. - protected static void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); - /// /// Writes the animation parameter() to the stream. /// @@ -233,55 +160,8 @@ protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, /// The number of times to loop the animation. If it is 0, this means infinitely. public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount) { - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, background.ToRgba32().Rgba); - stream.Write(buf); - BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount); - stream.Write(buf[..2]); - } - - /// - /// Writes the animation frame() to the stream. - /// - /// The stream to write to. - /// Animation frame data. - public static long WriteAnimationFrame(Stream stream, WebpFrameData animation) - { - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); - stream.Write(buf); - long position = stream.Position; - BinaryPrimitives.WriteUInt32BigEndian(buf, 0); - stream.Write(buf); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); - - byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); - stream.WriteByte(flag); - return position; - } - - /// - /// Overwrites ides the write frame size. - /// - /// The stream to write to. - /// Previous position. - public static void OverwriteFrameSize(Stream stream, long prevPosition) - { - uint position = (uint)stream.Position; - stream.Position = prevPosition; - byte[] buffer = new byte[4]; - - BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)(position - prevPosition - 4)); - stream.Write(buffer); - stream.Position = position; + WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount); + chunk.WriteTo(stream); } /// @@ -292,27 +172,17 @@ public static void OverwriteFrameSize(Stream stream, long prevPosition) /// Indicates, if the alpha channel data is compressed. public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { - uint size = (uint)dataBytes.Length + 1; - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, size); - stream.Write(buf); - + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Alpha); byte flags = 0; if (alphaDataIsCompressed) { + // TODO: Filtering and preprocessing flags = 1; } stream.WriteByte(flags); stream.Write(dataBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } + RiffHelper.EndWriteChunk(stream, pos); } /// @@ -328,66 +198,10 @@ public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alp /// Flag indicating, if an animation parameter is present. protected static void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation) { - if (width > MaxDimension || height > MaxDimension) - { - WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {MaxDimension}"); - } + WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height); - // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1. - if (width * height > MaxCanvasPixels) - { - WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1"); - } - - uint flags = 0; - if (exifProfile != null) - { - // Set exif bit. - flags |= 8; - } - - if (hasAnimation) - { - // Set animated flag. - flags |= 2; - } - - if (xmpProfile != null) - { - // Set xmp bit. - flags |= 4; - } - - if (hasAlpha) - { - // Set alpha bit. - flags |= 16; - } - - if (iccProfile != null) - { - // Set iccp flag. - flags |= 32; - } - - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, flags); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, width - 1); - stream.Write(buf[..3]); - BinaryPrimitives.WriteUInt32LittleEndian(buf, height - 1); - stream.Write(buf[..3]); - } - - private unsafe struct ScratchBuffer - { - private const int Size = 4; - private fixed byte scratch[Size]; + chunk.Validate(MaxDimension, MaxCanvasPixels); - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); + chunk.WriteTo(stream); } } diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs new file mode 100644 index 0000000000..3855a293c1 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks; + +internal readonly struct WebpAnimationParameter +{ + public WebpAnimationParameter(uint background, ushort loopCount) + { + this.Background = background; + this.LoopCount = loopCount; + } + + /// + /// Gets default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + public uint Background { get; } + + /// + /// Gets number of times to loop the animation. If it is 0, this means infinitely. + /// + public ushort LoopCount { get; } + + public void WriteTo(Stream stream) + { + Span buffer = stackalloc byte[6]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer[..4], this.Background); + BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..], this.LoopCount); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.AnimationParameter, buffer); + } +} diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs new file mode 100644 index 0000000000..f22a3fd540 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks; + +internal readonly struct WebpFrameData +{ + /// + /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. + /// + public const uint HeaderSize = 16; + + public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod) + { + this.DataSize = dataSize; + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + this.Duration = duration; + this.DisposalMethod = disposalMethod; + this.BlendingMethod = blendingMethod; + } + + public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, int flags) + : this( + dataSize, + x, + y, + width, + height, + duration, + (flags & 2) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending, + (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose) + { + } + + public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod) + : this(0, x, y, width, height, duration, blendingMethod, disposalMethod) + { + } + + /// + /// Gets the animation chunk size. + /// + public uint DataSize { get; } + + /// + /// Gets the X coordinate of the upper left corner of the frame is Frame X * 2. + /// + public uint X { get; } + + /// + /// Gets the Y coordinate of the upper left corner of the frame is Frame Y * 2. + /// + public uint Y { get; } + + /// + /// Gets the width of the frame. + /// + public uint Width { get; } + + /// + /// Gets the height of the frame. + /// + public uint Height { get; } + + /// + /// Gets the time to wait before displaying the next frame, in 1 millisecond units. + /// Note the interpretation of frame duration of 0 (and often smaller then 10) is implementation defined. + /// + public uint Duration { get; } + + /// + /// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// + public WebpBlendingMethod BlendingMethod { get; } + + /// + /// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// + public WebpDisposalMethod DisposalMethod { get; } + + public Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); + + /// + /// Writes the animation frame() to the stream. + /// + /// The stream to write to. + public long WriteHeaderTo(Stream stream) + { + byte flags = 0; + + if (this.BlendingMethod is WebpBlendingMethod.DoNotBlend) + { + // Set blending flag. + flags |= 2; + } + + if (this.DisposalMethod is WebpDisposalMethod.Dispose) + { + // Set disposal flag. + flags |= 1; + } + + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.FrameData); + + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.X); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Y); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Duration); + stream.WriteByte(flags); + + return pos; + } + + /// + /// Reads the animation frame header. + /// + /// The stream to read from. + /// Animation frame data. + public static WebpFrameData Parse(Stream stream) + { + Span buffer = stackalloc byte[4]; + + WebpFrameData data = new( + dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer), + x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + width: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + flags: stream.ReadByte()); + + return data; + } +} diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs new file mode 100644 index 0000000000..70d6870ce4 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks; + +internal readonly struct WebpVp8X +{ + public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height) + { + this.HasAnimation = hasAnimation; + this.HasXmp = hasXmp; + this.HasExif = hasExif; + this.HasAlpha = hasAlpha; + this.HasIcc = hasIcc; + this.Width = width; + this.Height = height; + } + + /// + /// Gets a value indicating whether this is an animated image. Data in 'ANIM' and 'ANMF' Chunks should be used to control the animation. + /// + public bool HasAnimation { get; } + + /// + /// Gets a value indicating whether the file contains XMP metadata. + /// + public bool HasXmp { get; } + + /// + /// Gets a value indicating whether the file contains Exif metadata. + /// + public bool HasExif { get; } + + /// + /// Gets a value indicating whether any of the frames of the image contain transparency information ("alpha"). + /// + public bool HasAlpha { get; } + + /// + /// Gets a value indicating whether the file contains an 'ICCP' Chunk. + /// + public bool HasIcc { get; } + + /// + /// Gets width of the canvas in pixels. (uint24) + /// + public uint Width { get; } + + /// + /// Gets height of the canvas in pixels. (uint24) + /// + public uint Height { get; } + + public void Validate(uint maxDimension, ulong maxCanvasPixels) + { + if (this.Width > maxDimension || this.Height > maxDimension) + { + WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {maxDimension}"); + } + + // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1. + if (this.Width * this.Height > maxCanvasPixels) + { + WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1"); + } + } + + public void WriteTo(Stream stream) + { + byte flags = 0; + + if (this.HasAnimation) + { + // Set animated flag. + flags |= 2; + } + + if (this.HasXmp) + { + // Set xmp bit. + flags |= 4; + } + + if (this.HasExif) + { + // Set exif bit. + flags |= 8; + } + + if (this.HasAlpha) + { + // Set alpha bit. + flags |= 16; + } + + if (this.HasIcc) + { + // Set icc flag. + flags |= 32; + } + + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Vp8X); + + stream.WriteByte(flags); + stream.Position += 3; // Reserved bytes + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); + + RiffHelper.EndWriteChunk(stream, pos); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 42aa667ac5..fe0131a2aa 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -6,7 +6,9 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -308,16 +310,15 @@ public void Encode(ImageFrame frame, Stream stream, bool hasAnim WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData - { - X = 0, - Y = 0, - Width = (uint)frame.Width, - Height = (uint)frame.Height, - Duration = frameMetadata.FrameDelay, - BlendingMethod = frameMetadata.BlendMethod, - DisposalMethod = frameMetadata.DisposalMethod - }); + prevPosition = new WebpFrameData( + 0, + 0, + (uint)frame.Width, + (uint)frame.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMethod, + frameMetadata.DisposalMethod) + .WriteHeaderTo(stream); } // Write bytes from the bitwriter buffer to the stream. @@ -325,7 +326,7 @@ public void Encode(ImageFrame frame, Stream stream, bool hasAnim if (hasAnimation) { - BitWriterBase.OverwriteFrameSize(stream, prevPosition); + RiffHelper.EndWriteChunk(stream, prevPosition); } } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 3b73023062..98e50bb9c2 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -4,7 +4,9 @@ using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -478,16 +480,15 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData - { - X = 0, - Y = 0, - Width = (uint)frame.Width, - Height = (uint)frame.Height, - Duration = frameMetadata.FrameDelay, - BlendingMethod = frameMetadata.BlendMethod, - DisposalMethod = frameMetadata.DisposalMethod - }); + prevPosition = new WebpFrameData( + 0, + 0, + (uint)frame.Width, + (uint)frame.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMethod, + frameMetadata.DisposalMethod) + .WriteHeaderTo(stream); } if (hasAlpha) @@ -501,7 +502,7 @@ private void Encode(ImageFrame frame, Stream stream, bool hasAni if (hasAnimation) { - BitWriterBase.OverwriteFrameSize(stream, prevPosition); + RiffHelper.EndWriteChunk(stream, prevPosition); } } finally diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index fad6ca16cc..f0e4093194 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -99,7 +100,7 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat remainingBytes -= 4; switch (chunkType) { - case WebpChunkType.Animation: + case WebpChunkType.FrameData: Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore ? new Color(new Bgra32(0, 0, 0, 0)) : features.AnimationBackgroundColor!.Value; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index f4e40090cf..80ffe8a996 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -106,14 +106,14 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe WebpThrowHelper.ThrowImageFormatException("bad partition length"); } - Vp8FrameHeader vp8FrameHeader = new Vp8FrameHeader + Vp8FrameHeader vp8FrameHeader = new() { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; - Vp8BitReader bitReader = new Vp8BitReader(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; + Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; return new WebpImageInfo { @@ -139,7 +139,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); - Vp8LBitReader bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); + Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); @@ -231,7 +231,7 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - WebpImageInfo info = new WebpImageInfo + WebpImageInfo info = new() { Width = width, Height = height, @@ -247,7 +247,7 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span /// The stream to read from. /// The buffer to store the read data into. /// A unsigned 24 bit integer. - public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span buffer) + public static uint ReadUInt24LittleEndian(Stream stream, Span buffer) { if (stream.Read(buffer, 0, 3) == 3) { @@ -286,14 +286,14 @@ public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data) /// The stream to read the data from. /// Buffer to store the data read from the stream. /// The chunk size in bytes. - public static uint ReadChunkSize(BufferedReadStream stream, Span buffer) + public static uint ReadChunkSize(Stream stream, Span buffer) { - DebugGuard.IsTrue(buffer.Length == 4, "buffer has wrong length"); + DebugGuard.IsTrue(buffer.Length is 4, "buffer has wrong length"); - if (stream.Read(buffer) == 4) + if (stream.Read(buffer) is 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); - return chunkSize % 2 == 0 ? chunkSize : chunkSize + 1; + return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1; } throw new ImageFormatException("Invalid Webp data, could not read chunk size."); diff --git a/src/ImageSharp/Formats/Webp/WebpChunkType.cs b/src/ImageSharp/Formats/Webp/WebpChunkType.cs index 5836dc6c09..12e3297775 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkType.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkType.cs @@ -61,5 +61,5 @@ internal enum WebpChunkType : uint /// For animated images, this chunk contains information about a single frame. If the Animation flag is not set, then this chunk SHOULD NOT be present. /// /// ANMF (Multiple) - Animation = 0x414E4D46, + FrameData = 0x414E4D46, } diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index 1433772757..818c843ea9 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -55,6 +55,11 @@ internal static class WebpConstants 0x50 // P }; + /// + /// The header bytes identifying a Webp. + /// + public const string WebpFourCc = "WEBP"; + /// /// 3 bits reserved for version. /// @@ -70,11 +75,6 @@ internal static class WebpConstants /// public const int Vp8FrameHeaderSize = 10; - /// - /// Size of a VP8X chunk in bytes. - /// - public const int Vp8XChunkSize = 10; - /// /// Size of a chunk header. /// diff --git a/src/ImageSharp/Formats/Webp/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/WebpFrameData.cs deleted file mode 100644 index 93c5d10dcd..0000000000 --- a/src/ImageSharp/Formats/Webp/WebpFrameData.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.IO; - -namespace SixLabors.ImageSharp.Formats.Webp; - -internal struct WebpFrameData -{ - /// - /// The animation chunk size. - /// - public uint DataSize; - - /// - /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. - /// - public const uint HeaderSize = 16; - - /// - /// The X coordinate of the upper left corner of the frame is Frame X * 2. - /// - public uint X; - - /// - /// The Y coordinate of the upper left corner of the frame is Frame Y * 2. - /// - public uint Y; - - /// - /// The width of the frame. - /// - public uint Width; - - /// - /// The height of the frame. - /// - public uint Height; - - /// - /// The time to wait before displaying the next frame, in 1 millisecond units. - /// Note the interpretation of frame duration of 0 (and often smaller then 10) is implementation defined. - /// - public uint Duration; - - /// - /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. - /// - public WebpBlendingMethod BlendingMethod; - - /// - /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. - /// - public WebpDisposalMethod DisposalMethod; - - public readonly Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); - - /// - /// Reads the animation frame header. - /// - /// The stream to read from. - /// Animation frame data. - public static WebpFrameData Parse(BufferedReadStream stream) - { - Span buffer = stackalloc byte[4]; - - WebpFrameData data = new() - { - DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), - - // 3 bytes for the X coordinate of the upper left corner of the frame. - X = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), - - // 3 bytes for the Y coordinate of the upper left corner of the frame. - Y = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), - - // Frame width Minus One. - Width = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, - - // Frame height Minus One. - Height = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, - - // Frame duration. - Duration = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) - }; - - byte flags = (byte)stream.ReadByte(); - data.DisposalMethod = (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose; - data.BlendingMethod = (flags & (1 << 1)) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending; - - return data; - } -} From a477ac13bc358811cd4bb83c6ff0f19621f51ae5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Nov 2023 21:21:11 +1000 Subject: [PATCH 18/19] Use correct alpha blending --- .../Formats/Webp/WebpAnimationDecoder.cs | 64 +++++++++---------- .../Formats/WebP/WebpDecoderTests.cs | 10 +++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Webp/landscape.webp | 3 + 4 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 tests/Images/Input/Webp/landscape.webp diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index f0e4093194..66e69d9a43 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -200,13 +200,10 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima this.RestoreToBackground(imageFrame, backgroundColor); } - using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); - DrawDecodedImageOnCanvas(decodedImage, imageFrame, regionRectangle); + using Buffer2D decodedImageFrame = this.DecodeImageFrameData(frameData, webpInfo); - if (previousFrame != null && frameData.BlendingMethod is WebpBlendingMethod.AlphaBlending) - { - this.AlphaBlend(previousFrame, imageFrame, regionRectangle); - } + bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.AlphaBlending; + DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend); previousFrame = currentFrame ?? image.Frames.RootFrame; this.restoreArea = regionRectangle; @@ -253,7 +250,7 @@ private byte ReadAlphaData(BufferedReadStream stream) /// The frame data. /// The webp information. /// A decoded image. - private Buffer2D DecodeImageData(WebpFrameData frameData, WebpImageInfo webpInfo) + private Buffer2D DecodeImageFrameData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { ImageFrame decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height); @@ -291,42 +288,43 @@ private Buffer2D DecodeImageData(WebpFrameData frameData, WebpIm /// Draws the decoded image on canvas. The decoded image can be smaller the canvas. /// /// The type of the pixel. - /// The decoded image. + /// The decoded image. /// The image frame to draw into. /// The area of the frame. - private static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, Rectangle restoreArea) + /// Whether to blend the decoded frame data onto the target frame. + private static void DrawDecodedImageFrameOnCanvas( + Buffer2D decodedImageFrame, + ImageFrame imageFrame, + Rectangle restoreArea, + bool blend) where TPixel : unmanaged, IPixel { + // Trim the destination frame to match the restore area. The source frame is already trimmed. Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea); - int decodedRowIdx = 0; - for (int y = 0; y < restoreArea.Height; y++) + if (blend) { - Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..restoreArea.Width]; - decodedPixelRow.TryCopyTo(framePixelRow); + // The destination frame has already been prepopulated with the pixel data from the previous frame + // so blending will leave the desired result which takes into consideration restoration to the + // background color within the restore area. + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + + for (int y = 0; y < restoreArea.Height; y++) + { + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + + blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f); + } + + return; } - } - /// - /// After disposing of the previous frame, render the current frame on the canvas using alpha-blending. - /// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle. - /// - /// The pixel format. - /// The source image. - /// The destination image. - /// The area of the frame. - private void AlphaBlend(ImageFrame src, ImageFrame dst, Rectangle restoreArea) - where TPixel : unmanaged, IPixel - { - Buffer2DRegion srcPixels = src.PixelBuffer.GetRegion(restoreArea); - Buffer2DRegion dstPixels = dst.PixelBuffer.GetRegion(restoreArea); - PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); for (int y = 0; y < restoreArea.Height; y++) { - Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); - Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); - - blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1f); + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + decodedPixelRow.CopyTo(framePixelRow); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index c3a777c153..4b03671e16 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -357,6 +357,16 @@ public void Decode_AnimatedLossy_IgnoreBackgroundColor_Works(TestImagePr image.CompareToOriginal(provider, ReferenceDecoder); } + [Theory] + [WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)] + public void Decode_AnimatedLossy_AlphaBlending_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(WebpDecoder.Instance); + image.DebugSaveMultiFrame(provider); + image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact); + } + [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 048b19dc5b..6ad93adfbd 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -681,6 +681,7 @@ public static class Lossless public static class Lossy { + public const string AnimatedLandscape = "Webp/landscape.webp"; public const string Earth = "Webp/earth_lossy.webp"; public const string WithExif = "Webp/exif_lossy.webp"; public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp"; diff --git a/tests/Images/Input/Webp/landscape.webp b/tests/Images/Input/Webp/landscape.webp new file mode 100644 index 0000000000..5f1f31a055 --- /dev/null +++ b/tests/Images/Input/Webp/landscape.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e9f8b7ee87ecb59d8cee5e84320da7670eb5e274e1c0a7dd5f13fe3675be62a +size 26892 From f46137847bed3d6c63f1b1f29bf8538aa7a384b2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Nov 2023 21:42:20 +1000 Subject: [PATCH 19/19] Complete encoding tests --- .../Formats/WebP/WebpEncoderTests.cs | 32 +++++++++++++++---- ...Encode_AnimatedLossy_Rgba32_landscape.webp | 3 ++ ...imatedLossy_Rgba32_leo_animated_lossy.webp | 3 ++ 3 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp create mode 100644 tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index d81c9eb93a..0ad684b277 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -24,22 +23,41 @@ public void Encode_AnimatedLossless(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - using MemoryStream memStream = new(); - image.SaveAsWebp(memStream, new() { FileFormat = WebpFileFormatType.Lossless }); + WebpEncoder encoder = new() + { + FileFormat = WebpFileFormatType.Lossless, + Quality = 100 + }; + + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "webp", encoder); - // TODO: DebugSave, VerifySimilarity + // Compare encoded result + image.VerifyEncoder(provider, "webp", string.Empty, encoder); } [Theory] [WithFile(Lossy.Animated, PixelTypes.Rgba32)] + [WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)] public void Encode_AnimatedLossy(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - using MemoryStream memStream = new(); - image.SaveAsWebp(memStream, new()); + WebpEncoder encoder = new() + { + FileFormat = WebpFileFormatType.Lossy, + Quality = 100 + }; + + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "webp", encoder); - // TODO: DebugSave, VerifySimilarity + // Compare encoded result + // The reference decoder seems to produce differences up to 0.1% but the input/output have been + // checked to be correct. + string path = provider.Utility.GetTestOutputFileName("webp", null, true); + using Image encoded = Image.Load(path); + encoded.CompareToReferenceOutput(ImageComparer.Tolerant(0.01f), provider, null, "webp"); } [Theory] diff --git a/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp new file mode 100644 index 0000000000..2312cb8576 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9ece3c7acc6f40318e3cda6b0189607df6b9b60dd112212c72ec0f6aa26431d +size 409346 diff --git a/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp new file mode 100644 index 0000000000..8474504da7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71800dff476f50ebd2a3d0cf0b4f5bef427a1c2cd8732b415511f10d3d93f9a0 +size 126382