From c68ef21613e237dc4220ecfe80347693527b192b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 29 Oct 2021 17:29:56 +0200 Subject: [PATCH 1/2] Write exif profile with padding if needed --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 49 +++++++++++++++---- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 4 +- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 9 ++-- .../Formats/Webp/WebpEncoderCore.cs | 2 - .../Formats/WebP/WebpMetaDataTests.cs | 25 ++++++++++ 5 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 41623f2878..31e636b6bc 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -10,11 +10,22 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { internal abstract class BitWriterBase { + private const uint MaxDimension = 16777215; + + 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 readonly byte[] scratchBuffer = new byte[4]; + /// /// Initializes a new instance of the class. /// @@ -81,13 +92,25 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired) /// The block length. protected void WriteRiffHeader(Stream stream, uint riffSize) { - Span buf = stackalloc byte[4]; stream.Write(WebpConstants.RiffFourCc); - BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize); - stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer, riffSize); + stream.Write(this.scratchBuffer.AsSpan(0, 4)); stream.Write(WebpConstants.WebpHeader); } + /// + /// Calculates the exif chunk size. + /// + /// The exif profile bytes. + /// The exif chunk size in bytes. + protected uint ExifChunkSize(byte[] exifBytes) + { + uint exifSize = (uint)exifBytes.Length; + uint exifChunkSize = WebpConstants.ChunkHeaderSize + exifSize + (exifSize & 1); + + return exifChunkSize; + } + /// /// Writes the Exif profile to the stream. /// @@ -97,12 +120,19 @@ protected void WriteExifProfile(Stream stream, byte[] exifBytes) { DebugGuard.NotNull(exifBytes, nameof(exifBytes)); - Span buf = stackalloc byte[4]; + uint size = (uint)exifBytes.Length; + Span buf = this.scratchBuffer.AsSpan(0, 4); BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Exif); stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, (uint)exifBytes.Length); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); stream.Write(buf); stream.Write(exifBytes); + + // Add padding byte if needed. + if ((size & 1) == 1) + { + stream.WriteByte(0); + } } /// @@ -114,14 +144,13 @@ protected void WriteExifProfile(Stream stream, byte[] exifBytes) /// The height of the image. protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height) { - int maxDimension = 16777215; - if (width > maxDimension || height > maxDimension) + if (width > MaxDimension || height > MaxDimension) { - WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {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 > 4294967295ul) + if (width * height > MaxCanvasPixels) { WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1"); } @@ -133,7 +162,7 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint widt flags |= 8; } - Span buf = stackalloc byte[4]; + Span buf = this.scratchBuffer.AsSpan(0, 4); stream.Write(WebpConstants.Vp8XMagicBytes); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); stream.Write(buf); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 7628247fd6..2c943f64f0 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -408,9 +408,9 @@ public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifPr if (exifProfile != null) { isVp8X = true; - riffSize += WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize; + riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); - riffSize += WebpConstants.ChunkHeaderSize + (uint)exifBytes.Length; + riffSize += this.ExifChunkSize(exifBytes); } this.Finish(); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 2f942231fb..2ce2f5550c 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -130,16 +130,15 @@ public override void Finish() /// public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height) { - Span buffer = stackalloc byte[4]; bool isVp8X = false; byte[] exifBytes = null; uint riffSize = 0; if (exifProfile != null) { isVp8X = true; - riffSize += WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize; + riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); - riffSize += WebpConstants.ChunkHeaderSize + (uint)exifBytes.Length; + riffSize += this.ExifChunkSize(exifBytes); } this.Finish(); @@ -161,8 +160,8 @@ public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifPr stream.Write(WebpConstants.Vp8LMagicBytes); // Write Vp8 Header. - BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); - stream.Write(buffer); + BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer, size); + stream.Write(this.scratchBuffer.AsSpan(0, 4)); stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); // Write the encoded bytes of the image to the stream. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index a61fc72530..8640261b17 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -4,11 +4,9 @@ using System.IO; using System.Threading; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 81067a41f5..a051de1c01 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -63,6 +63,31 @@ public void IgnoreMetadata_ControlsWhetherIccpIsParsed(TestImageProvider } } + [Theory] + [InlineData(WebpFileFormatType.Lossy)] + [InlineData(WebpFileFormatType.Lossless)] + public void Encode_WritesExifWithPadding(WebpFileFormatType fileFormatType) + { + // arrange + using var input = new Image(25, 25); + using var memoryStream = new MemoryStream(); + var expectedExif = new ExifProfile(); + string expectedSoftware = "ImageSharp"; + expectedExif.SetValue(ExifTag.Software, expectedSoftware); + input.Metadata.ExifProfile = expectedExif; + + // act + input.Save(memoryStream, new WebpEncoder() { FileFormat = fileFormatType }); + memoryStream.Position = 0; + + // assert + using var image = Image.Load(memoryStream); + ExifProfile actualExif = image.Metadata.ExifProfile; + Assert.NotNull(actualExif); + Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count); + Assert.Equal(expectedSoftware, actualExif.GetValue(ExifTag.Software).Value); + } + [Theory] [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32)] public void EncodeLossyWebp_PreservesExif(TestImageProvider provider) From 7f3c8ffbd0ed8c41e801a361113ee05c40d3c38c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 29 Oct 2021 19:45:46 +0200 Subject: [PATCH 2/2] Make sure the alpha flag in VP8X and VP8L are the same --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 18 ++++++++---------- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 17 ++++++++++++----- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 13 ++++++++++--- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +++- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 31e636b6bc..9208881360 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -63,15 +63,6 @@ internal abstract class BitWriterBase /// public abstract void Finish(); - /// - /// Writes the encoded image to the stream. - /// - /// The stream to write to. - /// The exif profile. - /// The width of the image. - /// The height of the image. - public abstract void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height); - protected void ResizeBuffer(int maxBytes, int sizeRequired) { int newSize = (3 * maxBytes) >> 1; @@ -142,7 +133,8 @@ protected void WriteExifProfile(Stream stream, byte[] exifBytes) /// A exif profile or null, if it does not exist. /// The width of the image. /// The height of the image. - protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height) + /// Flag indicating, if a alpha channel is present. + protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -162,6 +154,12 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint widt flags |= 8; } + if (hasAlpha) + { + // Set alpha bit. + flags |= 16; + } + Span buf = this.scratchBuffer.AsSpan(0, 4); stream.Write(WebpConstants.Vp8XMagicBytes); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 2c943f64f0..3b2f943db5 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -399,8 +399,15 @@ private void Flush() } } - /// - public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height) + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + /// The exif 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, uint width, uint height, bool hasAlpha) { bool isVp8X = false; byte[] exifBytes = null; @@ -433,7 +440,7 @@ public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifPr riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, hasAlpha); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -616,14 +623,14 @@ private void CodeIntraModes(Vp8BitWriter bitWriter) while (it.Next()); } - private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile) + private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, bool hasAlpha) { this.WriteRiffHeader(stream, riffSize); // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, width, height); + this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha); } this.WriteVp8Header(stream, vp8Size); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 2ce2f5550c..b83865aa36 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -127,8 +127,15 @@ public override void Finish() this.used = 0; } - /// - public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height) + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + /// The exif 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, uint width, uint height, bool hasAlpha) { bool isVp8X = false; byte[] exifBytes = null; @@ -153,7 +160,7 @@ public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifPr // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, width, height); + this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha); } // Write magic bytes indicating its a lossless webp. diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 693585637c..2fb3fbc6aa 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -234,7 +234,7 @@ public void Encode(Image image, Stream stream) this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height); + this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 37808d56c2..d41da790b3 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -317,6 +317,8 @@ public void Encode(Image image, Stream stream) this.bitWriter = new Vp8BitWriter(expectedSize, this); // TODO: EncodeAlpha(); + bool hasAlpha = false; + // Stats-collection loop. this.StatLoop(width, height, yStride, uvStride); it.Init(); @@ -348,7 +350,7 @@ public void Encode(Image image, Stream stream) // Write bytes from the bitwriter buffer to the stream. image.Metadata.SyncProfiles(); - this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height); + this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha); } ///