diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 927992686..39c4beb61 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -88,7 +88,8 @@ internal abstract class BitWriterBase /// The color profile. /// Flag indicating, if a alpha channel is present. /// Flag indicating, if an animation parameter is present. - public static void WriteTrunksBeforeData( + /// A or a default instance. + public static WebpVp8X WriteTrunksBeforeData( Stream stream, uint width, uint height, @@ -102,16 +103,19 @@ internal abstract class BitWriterBase RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc); // Write VP8X, header if necessary. + WebpVp8X vp8x = default; bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation; if (isVp8X) { - WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation); + vp8x = WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation); if (iccProfile != null) { RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray()); } } + + return vp8x; } /// @@ -124,10 +128,16 @@ internal abstract class BitWriterBase /// Write the trunks after data trunk. /// /// The stream to write to. - /// The exif profile. + /// The VP8X chunk. + /// Whether to update the chunk. + /// The initial position of the stream before encoding. + /// The EXIF profile. /// The XMP profile. public static void WriteTrunksAfterData( Stream stream, + in WebpVp8X vp8x, + bool updateVp8x, + long initialPosition, ExifProfile? exifProfile, XmpProfile? xmpProfile) { @@ -141,7 +151,7 @@ internal abstract class BitWriterBase RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data); } - RiffHelper.EndWriteRiffFile(stream, 4); + RiffHelper.EndWriteRiffFile(stream, in vp8x, updateVp8x, initialPosition); } /// @@ -186,19 +196,21 @@ internal abstract class BitWriterBase /// Writes a VP8X header to the stream. /// /// 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. + /// An EXIF profile or null, if it does not exist. + /// An XMP profile or null, if it does not exist. /// The color profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. /// 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) + protected static WebpVp8X WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation) { WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height); chunk.Validate(MaxDimension, MaxCanvasPixels); chunk.WriteTo(stream); + + return chunk; } } diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs index f781d6114..491f71650 100644 --- a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Chunks; -internal readonly struct WebpVp8X +internal readonly struct WebpVp8X : IEquatable { public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height) { @@ -51,6 +51,24 @@ internal readonly struct WebpVp8X /// public uint Height { get; } + public static bool operator ==(WebpVp8X left, WebpVp8X right) => left.Equals(right); + + public static bool operator !=(WebpVp8X left, WebpVp8X right) => !(left == right); + + public override bool Equals(object? obj) => obj is WebpVp8X x && this.Equals(x); + + public bool Equals(WebpVp8X other) + => this.HasAnimation == other.HasAnimation + && this.HasXmp == other.HasXmp + && this.HasExif == other.HasExif + && this.HasAlpha == other.HasAlpha + && this.HasIcc == other.HasIcc + && this.Width == other.Width + && this.Height == other.Height; + + public override int GetHashCode() + => HashCode.Combine(this.HasAnimation, this.HasXmp, this.HasExif, this.HasAlpha, this.HasIcc, this.Width, this.Height); + public void Validate(uint maxDimension, ulong maxCanvasPixels) { if (this.Width > maxDimension || this.Height > maxDimension) @@ -65,6 +83,9 @@ internal readonly struct WebpVp8X } } + public WebpVp8X WithAlpha(bool hasAlpha) + => new(this.HasAnimation, this.HasXmp, this.HasExif, hasAlpha, this.HasIcc, this.Width, this.Height); + public void WriteTo(Stream stream) { byte flags = 0; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index f15cb3eb5..f658e40f6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -236,7 +236,7 @@ internal class Vp8LEncoder : IDisposable /// public Vp8LHashChain HashChain { get; } - public void EncodeHeader(Image image, Stream stream, bool hasAnimation) + public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAnimation) where TPixel : unmanaged, IPixel { // Write bytes from the bit-writer buffer to the stream. @@ -246,7 +246,8 @@ internal class Vp8LEncoder : IDisposable ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; - BitWriterBase.WriteTrunksBeforeData( + // The alpha flag is updated following encoding. + WebpVp8X vp8x = BitWriterBase.WriteTrunksBeforeData( stream, (uint)image.Width, (uint)image.Height, @@ -261,9 +262,11 @@ internal class Vp8LEncoder : IDisposable WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); } + + return vp8x; } - public void EncodeFooter(Image image, Stream stream) + public void EncodeFooter(Image image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition) where TPixel : unmanaged, IPixel { // Write bytes from the bit-writer buffer to the stream. @@ -272,7 +275,9 @@ internal class Vp8LEncoder : IDisposable ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; - BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + bool updateVp8x = hasAlpha && vp8x != default; + WebpVp8X updated = updateVp8x ? vp8x.WithAlpha(true) : vp8x; + BitWriterBase.WriteTrunksAfterData(stream, in updated, updateVp8x, initialPosition, exifProfile, xmpProfile); } /// @@ -284,7 +289,8 @@ internal class Vp8LEncoder : IDisposable /// The frame metadata. /// The to encode the image data to. /// Flag indicating, if an animation parameter is present. - public void Encode(ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation) + /// A indicating whether the frame contains an alpha channel. + public bool Encode(ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation) where TPixel : unmanaged, IPixel { // Convert image pixels to bgra array. @@ -323,6 +329,8 @@ internal class Vp8LEncoder : IDisposable { RiffHelper.EndWriteChunk(stream, prevPosition); } + + return hasAlpha; } /// @@ -501,7 +509,7 @@ internal class Vp8LEncoder : IDisposable /// The type of the pixels. /// The frame pixel buffer to convert. /// true, if the image is non opaque. - private bool ConvertPixelsToBgra(Buffer2DRegion pixels) + public bool ConvertPixelsToBgra(Buffer2DRegion pixels) where TPixel : unmanaged, IPixel { bool nonOpaque = false; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 6e9e4f9cd..40d91ecf1 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -310,7 +310,7 @@ internal class Vp8Encoder : IDisposable /// private int MbHeaderLimit { get; } - public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation) + public WebpVp8X EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation) where TPixel : unmanaged, IPixel { // Write bytes from the bitwriter buffer to the stream. @@ -320,7 +320,7 @@ internal class Vp8Encoder : IDisposable ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; - BitWriterBase.WriteTrunksBeforeData( + WebpVp8X vp8x = BitWriterBase.WriteTrunksBeforeData( stream, (uint)image.Width, (uint)image.Height, @@ -335,9 +335,11 @@ internal class Vp8Encoder : IDisposable WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image); BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); } + + return vp8x; } - public void EncodeFooter(Image image, Stream stream) + public void EncodeFooter(Image image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition) where TPixel : unmanaged, IPixel { // Write bytes from the bitwriter buffer to the stream. @@ -346,7 +348,9 @@ internal class Vp8Encoder : IDisposable ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; - BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + bool updateVp8x = hasAlpha && vp8x != default; + WebpVp8X updated = updateVp8x ? vp8x.WithAlpha(true) : vp8x; + BitWriterBase.WriteTrunksAfterData(stream, in updated, updateVp8x, initialPosition, exifProfile, xmpProfile); } /// @@ -357,9 +361,10 @@ internal class Vp8Encoder : IDisposable /// The stream to encode the image data to. /// The region of interest within the frame to encode. /// The frame metadata. - public void EncodeAnimation(ImageFrame frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata) - where TPixel : unmanaged, IPixel => - this.Encode(stream, frame, bounds, frameMetadata, true, null); + /// A indicating whether the frame contains an alpha channel. + public bool EncodeAnimation(ImageFrame frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata) + where TPixel : unmanaged, IPixel + => this.Encode(stream, frame, bounds, frameMetadata, true, null); /// /// Encodes the static image frame to the specified stream. @@ -384,7 +389,8 @@ internal class Vp8Encoder : IDisposable /// The frame metadata. /// Flag indicating, if an animation parameter is present. /// The image to encode from. - private void Encode(Stream stream, ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image image) + /// A indicating whether the frame contains an alpha channel. + private bool Encode(Stream stream, ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image image) where TPixel : unmanaged, IPixel { int width = bounds.Width; @@ -514,6 +520,8 @@ internal class Vp8Encoder : IDisposable { encodedAlphaData?.Dispose(); } + + return hasAlpha; } /// diff --git a/src/ImageSharp/Formats/Webp/RiffHelper.cs b/src/ImageSharp/Formats/Webp/RiffHelper.cs index d3862ea8b..b6318c748 100644 --- a/src/ImageSharp/Formats/Webp/RiffHelper.cs +++ b/src/ImageSharp/Formats/Webp/RiffHelper.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using System.Text; +using SixLabors.ImageSharp.Formats.Webp.Chunks; namespace SixLabors.ImageSharp.Formats.Webp; @@ -107,6 +108,7 @@ internal static class RiffHelper position++; } + // Add the size of the encoded file to the Riff header. BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); stream.Position = sizePosition; stream.Write(buffer); @@ -120,5 +122,18 @@ internal static class RiffHelper return sizePosition; } - public static void EndWriteRiffFile(Stream stream, long sizePosition) => EndWriteChunk(stream, sizePosition); + public static void EndWriteRiffFile(Stream stream, in WebpVp8X vp8x, bool updateVp8x, long sizePosition) + { + EndWriteChunk(stream, sizePosition + 4); + + // Write the VP8X chunk if necessary. + if (updateVp8x) + { + long position = stream.Position; + + stream.Position = sizePosition + 12; + vp8x.WriteTo(stream); + stream.Position = position; + } + } } diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 80ffe8a99..07f09d45e 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -2,7 +2,6 @@ // 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; diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 2991f355f..21f0f4946 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -54,7 +54,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable /// /// The flag to decide how to handle the background color in the Animation Chunk. /// - private BackgroundColorHandling backgroundColorHandling; + private readonly BackgroundColorHandling backgroundColorHandling; /// /// Initializes a new instance of the class. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index e37c1d179..d29759f9a 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Memory; @@ -143,12 +144,14 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.nearLossless, this.nearLosslessQuality); - encoder.EncodeHeader(image, stream, hasAnimation); + long initialPosition = stream.Position; + bool hasAlpha = false; + WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation); // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame); - encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation); + hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation); if (hasAnimation) { @@ -190,14 +193,14 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.nearLossless, this.nearLosslessQuality); - animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation); + hasAlpha |= animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation); previousFrame = currentFrame; previousDisposal = frameMetadata.DisposalMethod; } } - encoder.EncodeFooter(image, stream); + encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); } else { @@ -214,17 +217,20 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.spatialNoiseShaping, this.alphaCompression); + long initialPosition = stream.Position; + bool hasAlpha = false; + WebpVp8X vp8x = default; if (image.Frames.Count > 1) { - // TODO: What about alpha here? - encoder.EncodeHeader(image, stream, false, true); + // The alpha flag is updated following encoding. + vp8x = encoder.EncodeHeader(image, stream, false, true); // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame); WebpDisposalMethod previousDisposal = frameMetadata.DisposalMethod; - encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata); + hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata); // Encode additional frames // This frame is reused to store de-duplicated pixel buffers. @@ -263,18 +269,19 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.spatialNoiseShaping, this.alphaCompression); - animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata); + hasAlpha |= animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata); previousFrame = currentFrame; previousDisposal = frameMetadata.DisposalMethod; } + + encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); } else { encoder.EncodeStatic(stream, image); + encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); } - - encoder.EncodeFooter(image, stream); } } }