diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs new file mode 100644 index 0000000000..8f06e5886f --- /dev/null +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -0,0 +1,124 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Text; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal static class RiffHelper +{ + /// + /// The header bytes identifying RIFF file. + /// + private const 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.WriteUInt32BigEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + + func(stream); + + long position = stream.Position; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 == 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; + 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.WriteUInt32BigEndian(buffer, fourCc); + stream.Write(buffer); + uint size = (uint)data.Length; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); + stream.Write(buffer); + stream.Write(data); + + // padding + if (size % 2 is 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.WriteUInt32BigEndian(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; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 is 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; + stream.Write(buffer); + 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); +} 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 596715b205..cbd2aa8e7f 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,19 +73,19 @@ 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 ImageFrame(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++) @@ -95,23 +95,23 @@ private static Image DispatchAlphaToGreen(Image image, S } } - 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 deleted file mode 100644 index 714ec428ec..0000000000 --- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Webp; - -internal struct AnimationFrameData -{ - /// - /// The animation chunk size. - /// - public uint DataSize; - - /// - /// 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 AnimationBlendingMethod 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; -} diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index ab78d18604..d502fd6063 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -1,9 +1,11 @@ // 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; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -14,18 +16,11 @@ internal abstract class BitWriterBase private const ulong MaxCanvasPixels = 4294967295ul; - protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize; - /// /// Buffer to write to. /// 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. /// @@ -41,17 +36,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 +60,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. /// @@ -84,63 +79,89 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired) } /// - /// Writes the RIFF header to the stream. + /// Write the trunks before data trunk. /// /// The stream to write to. - /// The block length. - protected void WriteRiffHeader(Stream stream, uint riffSize) + /// 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. + /// Flag indicating, if an animation parameter is present. + public static void WriteTrunksBeforeData( + Stream stream, + uint width, + uint height, + ExifProfile? exifProfile, + XmpProfile? xmpProfile, + IccProfile? iccProfile, + bool hasAlpha, + bool hasAnimation) { - stream.Write(WebpConstants.RiffFourCc); - BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize); - stream.Write(this.scratchBuffer.Span.Slice(0, 4)); - stream.Write(WebpConstants.WebpHeader); + // Write file size later + 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; + if (isVp8X) + { + WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation); + + if (iccProfile != null) + { + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray()); + } + } } /// - /// Calculates the chunk size of EXIF, XMP or ICCP metadata. + /// Writes the encoded image to the stream. /// - /// 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); - } + /// The stream to write to. + public abstract void WriteEncodedImageToStream(Stream stream); /// - /// Calculates the chunk size of a alpha chunk. + /// Write the trunks after data trunk. /// - /// The alpha chunk bytes. - /// The alpha data chunk size in bytes. - protected static uint AlphaChunkSize(Span alphaBytes) + /// The stream to write to. + /// The exif profile. + /// The XMP profile. + public static void WriteTrunksAfterData( + Stream stream, + ExifProfile? exifProfile, + XmpProfile? xmpProfile) { - uint alphaSize = (uint)alphaBytes.Length + 1; - return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); + if (exifProfile != null) + { + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray()); + } + + if (xmpProfile != null) + { + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data); + } + + RiffHelper.EndWriteRiffFile(stream, 4); } /// - /// Writes a metadata profile (EXIF or XMP) to the stream. + /// Writes the animation parameter() to the stream. /// /// The stream to write to. - /// The metadata profile's bytes. - /// The chuck type to write. - protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) + /// + /// 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. + public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount) { - DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); - - uint size = (uint)metadataBytes.Length; - Span buf = this.scratchBuffer.Span.Slice(0, 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); - } + WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount); + chunk.WriteTo(stream); } /// @@ -149,53 +170,19 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh /// 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.Slice(0, 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) { - flags |= 1; + // TODO: Filtering and preprocessing + flags = 1; } stream.WriteByte(flags); stream.Write(dataBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } - } - - /// - /// 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); - } + RiffHelper.EndWriteChunk(stream, pos); } /// @@ -204,65 +191,17 @@ protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) /// 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) + /// 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}"); - } - - // 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 (xmpProfile != null) - { - // Set xmp bit. - flags |= 4; - } + WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height); - if (hasAlpha) - { - // Set alpha bit. - flags |= 16; - } - - if (iccProfileBytes != null) - { - // Set iccp flag. - flags |= 32; - } - - Span buf = this.scratchBuffer.Span.Slice(0, 4); - stream.Write(WebpConstants.Vp8XMagicBytes); - 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/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 5b4eab64a3..81530706d6 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; @@ -72,7 +69,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) { @@ -116,7 +113,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 @@ -394,87 +391,28 @@ private void Flush() } } - /// - /// 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. - /// The alpha channel data. - /// Indicates, if the alpha data is compressed. - public void WriteEncodedImageToStream( - Stream stream, - ExifProfile? exifProfile, - XmpProfile? xmpProfile, - IccProfile? iccProfile, - uint width, - uint height, - bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) + /// + public override void WriteEncodedImageToStream(Stream stream) { - 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!); - } - - if (xmpProfile != null) - { - isVp8X = true; - xmpBytes = xmpProfile.Data; - riffSize += MetadataChunkSize(xmpBytes!); - } - - if (iccProfile != null) - { - isVp8X = true; - iccProfileBytes = iccProfile.ToByteArray(); - riffSize += MetadataChunkSize(iccProfileBytes); - } - - if (hasAlpha) - { - isVp8X = true; - riffSize += AlphaChunkSize(alphaData); - } - - if (isVp8X) - { - riffSize += ExtendedFileChunkSize; - } + 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); - Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc); + Vp8BitWriter bitWriterPartZero = new Vp8BitWriter(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; 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,59 +421,49 @@ public void WriteEncodedImageToStream( { stream.WriteByte(0); } - - if (exifProfile != null) - { - this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); - } - - if (xmpProfile != null) - { - this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); - } } - 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 +471,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 +527,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 Vp8EncIterator(this.enc); int predsWidth = this.enc.PredsWidth; do @@ -627,18 +555,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 +577,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,56 +585,18 @@ private void CodeIntraModes(Vp8BitWriter bitWriter) } } - bitWriter.PutUvMode(mb.UvMode); + this.PutUvMode(mb.UvMode); } 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 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..dc867fa85e 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; @@ -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. @@ -98,9 +98,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]; @@ -122,76 +119,20 @@ 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, iccBytes, width, height, hasAlpha); - - if (iccBytes != null) - { - this.WriteColorProfile(stream, iccBytes); - } - } // 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. @@ -200,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); - } } /// @@ -226,7 +157,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/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/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 211185dbba..2e7dd722fc 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -779,7 +779,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 cedc809382..d6b10ada55 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 878d487a86..4fdbb31d37 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; @@ -235,26 +237,60 @@ 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) 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) + { + WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount); + } + } + + 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, width, height); + bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height); // Write the image size. this.WriteImageSize(width, height); @@ -263,35 +299,60 @@ public void Encode(Image image, Stream stream) this.WriteAlphaAndVersion(hasAlpha); // Encode the main image stream. - this.EncodeStream(image); + this.EncodeStream(frame); + + this.bitWriter.Finish(); + + long prevPosition = 0; + + if (hasAnimation) + { + WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. + prevPosition = new WebpFrameData( + 0, + 0, + (uint)frame.Width, + (uint)frame.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMethod, + frameMetadata.DisposalMethod) + .WriteHeaderTo(stream); + } // 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); + + if (hasAnimation) + { + RiffHelper.EndWriteChunk(stream, prevPosition); + } } /// /// 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(); + int size = this.bitWriter.NumBytes; if (size >= pixelCount) { // Compressing would not yield in smaller data -> leave the data uncompressed. @@ -333,12 +394,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(); @@ -425,9 +486,9 @@ private void EncodeStream(Image image) 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); } @@ -447,14 +508,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); @@ -682,7 +743,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)) { (bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest); } @@ -1154,35 +1215,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. @@ -1844,9 +1911,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(); } } @@ -1855,9 +1922,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/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index 19ea424199..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(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) @@ -619,12 +617,9 @@ private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) 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/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index 7211f93766..52c7e9703b 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; @@ -391,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) @@ -409,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 @@ -462,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 f17d965e87..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; @@ -88,7 +90,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 +168,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,27 +311,94 @@ public Vp8Encoder( /// private int MbHeaderLimit { get; } + public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation) + 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) + { + WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount); + } + } + + 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, 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; - 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; @@ -375,13 +445,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 +456,7 @@ public void Encode(Image image, Stream stream) { // TODO: This can potentially run in an separate task. encodedAlphaData = AlphaEncoder.EncodeAlpha( - image, + frame, this.configuration, this.memoryAllocator, this.skipMetadata, @@ -408,16 +471,39 @@ public void Encode(Image image, Stream stream) } } - this.bitWriter.WriteEncodedImageToStream( - stream, - exifProfile, - xmpProfile, - metadata.IccProfile, - (uint)width, - (uint)height, - hasAlpha, - alphaData[..alphaDataSize], - this.alphaCompression && alphaCompressionSucceeded); + this.bitWriter.Finish(); + + long prevPosition = 0; + + if (hasAnimation) + { + WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. + prevPosition = new WebpFrameData( + 0, + 0, + (uint)frame.Width, + (uint)frame.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMethod, + frameMetadata.DisposalMethod) + .WriteHeaderTo(stream); + } + + if (hasAlpha) + { + Span data = alphaData[..alphaDataSize]; + bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded; + BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed); + } + + this.bitWriter.WriteEncodedImageToStream(stream); + + if (hasAnimation) + { + RiffHelper.EndWriteChunk(stream, prevPosition); + } } finally { @@ -520,7 +606,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; @@ -862,10 +948,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 +1114,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 +1141,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 +1159,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/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 7952b15b44..3eb03b1724 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -76,47 +76,48 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI Vp8Proba proba = new(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new(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( - 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); } } @@ -194,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 { @@ -590,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; } } } @@ -1328,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; } @@ -1353,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/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..66e69d9a43 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -100,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; @@ -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); + WebpFrameData frameData = WebpFrameData.Parse(stream); long streamStartPosition = stream.Position; Span buffer = stackalloc byte[4]; @@ -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: @@ -175,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; } @@ -183,29 +188,22 @@ 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; } - 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 AnimationDisposalMethod.Dispose) + if (frameData.DisposalMethod is WebpDisposalMethod.Dispose) { this.RestoreToBackground(imageFrame, backgroundColor); } - using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); - DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight); + using Buffer2D decodedImageFrame = this.DecodeImageFrameData(frameData, webpInfo); - if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending) - { - this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight); - } + bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.AlphaBlending; + DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend); previousFrame = currentFrame ?? image.Frames.RootFrame; this.restoreArea = regionRectangle; @@ -217,12 +215,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; } /// @@ -239,7 +238,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; } @@ -251,22 +250,24 @@ private byte ReadAlphaData(BufferedReadStream stream) /// The frame data. /// The webp information. /// A decoded image. - private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) + private Buffer2D DecodeImageFrameData(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.Frames.RootFrame.PixelBuffer; + Buffer2D pixelBufferDecoded = decodedFrame.PixelBuffer; if (webpInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = + new(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(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData); } @@ -274,7 +275,7 @@ private Buffer2D DecodeImageData(AnimationFrameData frameData, W } catch { - decodedImage?.Dispose(); + decodedFrame?.Dispose(); throw; } finally @@ -287,48 +288,43 @@ private Buffer2D DecodeImageData(AnimationFrameData frameData, W /// 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 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. + /// 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 { - Buffer2D imageFramePixels = imageFrame.PixelBuffer; - int decodedRowIdx = 0; - for (int y = frameY; y < frameY + frameHeight; y++) + // Trim the destination frame to match the restore area. The source frame is already trimmed. + Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea); + if (blend) { - Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..frameWidth]; - decodedPixelRow.TryCopyTo(framePixelRow[frameX..]); + // 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 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) - where TPixel : unmanaged, IPixel - { - Buffer2D srcPixels = src.PixelBuffer; - Buffer2D dstPixels = dst.PixelBuffer; - 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); - - blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f); + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + decodedPixelRow.CopyTo(framePixelRow); } } @@ -353,42 +349,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/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/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index a7ae474e46..80ffe8a996 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -2,6 +2,7 @@ // 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; @@ -77,7 +78,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"); } @@ -91,7 +92,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,23 +106,16 @@ 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( - stream, - remaining, - memoryAllocator, - partitionLength) - { - Remaining = remaining - }; + Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; - return new WebpImageInfo() + return new WebpImageInfo { Width = width, Height = height, @@ -145,7 +139,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 +168,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); } - return new WebpImageInfo() + return new WebpImageInfo { Width = width, Height = height, @@ -231,13 +225,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 +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 ReadUnsignedInt24Bit(BufferedReadStream stream, Span buffer) + public static uint ReadUInt24LittleEndian(Stream stream, Span buffer) { if (stream.Read(buffer, 0, 3) == 3) { @@ -261,7 +255,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]); } /// @@ -271,14 +286,14 @@ public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span bu /// 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."); @@ -298,7 +313,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; } diff --git a/src/ImageSharp/Formats/Webp/WebpChunkType.cs b/src/ImageSharp/Formats/Webp/WebpChunkType.cs index 802d7f7288..12e3297775 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. /// - Animation = 0x414E4D46, + /// ANMF (Multiple) + FrameData = 0x414E4D46, } diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index d105d8dd62..818c843ea9 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. /// @@ -88,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. /// @@ -103,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/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 8832ac1068..de188b137b 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; @@ -89,25 +91,30 @@ 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( + this.memoryAllocator, + this.configuration, + this.maxFrames, + this.backgroundColorHandling); 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) { - WebpLosslessDecoder losslessDecoder = new(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = new( + 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( + this.webImageInfo.Vp8BitReader, + this.memoryAllocator, + this.configuration); lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); } @@ -137,7 +144,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat { 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); } } @@ -332,7 +339,7 @@ private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata, return; } - metadata.ExifProfile = new(exifData); + metadata.ExifProfile = new ExifProfile(exifData); } } @@ -359,7 +366,7 @@ private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata, S return; } - metadata.XmpProfile = new(xmpData); + metadata.XmpProfile = new XmpProfile(xmpData); } } 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/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 29d0c9e3b0..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; /// diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 49512e03b5..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,11 +140,38 @@ 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 { - using Vp8Encoder enc = new( + using Vp8Encoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -156,7 +183,34 @@ 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/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(); } 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/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); } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index c0fc00b82d..4b03671e16 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(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/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 6c5fa50ff6..0ad684b277 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -17,6 +17,49 @@ public class WebpEncoderTests { private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); + [Theory] + [WithFile(Lossless.Animated, PixelTypes.Rgba32)] + public void Encode_AnimatedLossless(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + WebpEncoder encoder = new() + { + FileFormat = WebpFileFormatType.Lossless, + Quality = 100 + }; + + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "webp", encoder); + + // 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(); + WebpEncoder encoder = new() + { + FileFormat = WebpFileFormatType.Lossy, + Quality = 100 + }; + + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "webp", encoder); + + // 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] [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)] 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)); 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/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 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