From 5db2d28e69bfca649dc1ca52079793d6d337f52d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 22 Jan 2021 13:52:56 +0100 Subject: [PATCH] Write EXIF chunk, if Exif profile is present --- .../Formats/WebP/BitWriter/BitWriterBase.cs | 70 +++++++++++++++++-- .../Formats/WebP/BitWriter/Vp8BitWriter.cs | 47 ++++++++----- .../Formats/WebP/BitWriter/Vp8LBitWriter.cs | 29 +++++++- .../Formats/WebP/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/WebP/Lossy/Vp8Encoder.cs | 2 +- src/ImageSharp/Formats/WebP/WebpConstants.cs | 16 +++++ .../Formats/WebP/WebpDecoderCore.cs | 14 ++-- .../Formats/WebP/WebpThrowHelper.cs | 17 ++--- 8 files changed, 157 insertions(+), 40 deletions(-) diff --git a/src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs index 5946aeb67..60cca018c 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs @@ -4,6 +4,7 @@ using System; using System.Buffers.Binary; using System.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter { @@ -55,7 +56,10 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter /// Writes the encoded image to the stream. /// /// The stream to write to. - public abstract void WriteEncodedImageToStream(Stream stream); + /// 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 bool ResizeBuffer(int maxBytes, int sizeRequired) { @@ -84,12 +88,68 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter /// The block length. protected void WriteRiffHeader(Stream stream, uint riffSize) { - Span buffer = stackalloc byte[4]; - + Span buf = stackalloc byte[4]; stream.Write(WebpConstants.RiffFourCc); - BinaryPrimitives.WriteUInt32LittleEndian(buffer, riffSize); - stream.Write(buffer); + BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize); + stream.Write(buf); stream.Write(WebpConstants.WebPHeader); } + + /// + /// Writes the Exif profile to the stream. + /// + /// The stream to write to. + /// The exif profile bytes. + protected void WriteExifProfile(Stream stream, byte[] exifBytes) + { + DebugGuard.NotNull(exifBytes, nameof(exifBytes)); + + Span buf = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Exif); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, (uint)exifBytes.Length); + stream.Write(buf); + stream.Write(exifBytes); + } + + /// + /// Writes a VP8X header to the stream. + /// + /// The stream to write to. + /// 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) + { + int maxDimension = 16777215; + 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 > 4294967295ul) + { + 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; + } + + Span buf = stackalloc byte[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.Slice(0, 3)); + BinaryPrimitives.WriteUInt32LittleEndian(buf, height - 1); + stream.Write(buf.Slice(0, 3)); + } } } diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs index b9d326ada..97d5cd38c 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs @@ -5,6 +5,7 @@ using System; using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Formats.Experimental.Webp.Lossy; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter { @@ -73,16 +74,10 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter /// The expected size in bytes. /// The Vp8Encoder. public Vp8BitWriter(int expectedSize, Vp8Encoder enc) - : this(expectedSize) - { - this.enc = enc; - } + : this(expectedSize) => this.enc = enc; /// - public override int NumBytes() - { - return (int)this.pos; - } + public override int NumBytes() => (int)this.pos; public int PutCoeffs(int ctx, Vp8Residual residual) { @@ -290,10 +285,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter } } - private bool PutBit(bool bit, int prob) - { - return this.PutBit(bit ? 1 : 0, prob); - } + private bool PutBit(bool bit, int prob) => this.PutBit(bit ? 1 : 0, prob); private bool PutBit(int bit, int prob) { @@ -408,8 +400,19 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter } /// - public override void WriteEncodedImageToStream(Stream stream) + public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height) { + bool isVp8X = false; + byte[] exifBytes = null; + uint riffSize = 0; + if (exifProfile != null) + { + isVp8X = true; + riffSize += WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize; + exifBytes = exifProfile.ToByteArray(); + riffSize += WebpConstants.ChunkHeaderSize + (uint)exifBytes.Length; + } + this.Finish(); uint numBytes = (uint)this.NumBytes(); int mbSize = this.enc.Mbw * this.enc.Mbh; @@ -427,10 +430,10 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter // Compute RIFF size // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. - var riffSize = WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; + riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebPHeaders(stream, size0, vp8Size, riffSize); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -439,6 +442,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter { stream.WriteByte(0); } + + if (exifProfile != null) + { + this.WriteExifProfile(stream, exifBytes); + } } private uint GeneratePartition0(Vp8BitWriter bitWriter) @@ -608,9 +616,16 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter while (it.Next()); } - private void WriteWebPHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize) + private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile) { this.WriteRiffHeader(stream, riffSize); + + // Write VP8X, header if necessary. + if (isVp8X) + { + this.WriteVp8XHeader(stream, exifProfile, width, height); + } + this.WriteVp8Header(stream, vp8Size); this.WriteFrameHeader(stream, size0); } diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs index 6fbb7e3b7..4a560940f 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs @@ -5,6 +5,7 @@ using System; using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Formats.Experimental.Webp.Lossless; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter { @@ -127,9 +128,19 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter } /// - public override void WriteEncodedImageToStream(Stream stream) + 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; + exifBytes = exifProfile.ToByteArray(); + riffSize += WebpConstants.ChunkHeaderSize + (uint)exifBytes.Length; + } this.Finish(); uint size = (uint)this.NumBytes(); @@ -137,8 +148,16 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter // Write RIFF header. uint pad = size & 1; - uint riffSize = WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + size + pad; + riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + size + pad; this.WriteRiffHeader(stream, riffSize); + + // Write VP8X, header if necessary. + if (isVp8X) + { + this.WriteVp8XHeader(stream, exifProfile, width, height); + } + + // Write magic bytes indicating its a lossless webp. stream.Write(WebpConstants.Vp8LMagicBytes); // Write Vp8 Header. @@ -146,11 +165,17 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.BitWriter stream.Write(buffer); stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); + // Write the encoded bytes of the image to the stream. this.WriteToStream(stream); if (pad == 1) { stream.WriteByte(0); } + + if (exifProfile != null) + { + this.WriteExifProfile(stream, exifBytes); + } } /// diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs index b2a254a61..49591c04a 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs @@ -185,7 +185,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.Lossless this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream); + this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height); } /// diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs index 0a51c006a..f404fc610 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs @@ -321,7 +321,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp.Lossy this.AdjustFilterStrength(); // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream); + this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height); } /// diff --git a/src/ImageSharp/Formats/WebP/WebpConstants.cs b/src/ImageSharp/Formats/WebP/WebpConstants.cs index 98c783a40..239e02d75 100644 --- a/src/ImageSharp/Formats/WebP/WebpConstants.cs +++ b/src/ImageSharp/Formats/WebP/WebpConstants.cs @@ -57,6 +57,17 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp 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. /// @@ -94,6 +105,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp /// 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/WebpDecoderCore.cs b/src/ImageSharp/Formats/WebP/WebpDecoderCore.cs index 879139a5d..06527aec0 100644 --- a/src/ImageSharp/Formats/WebP/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebpDecoderCore.cs @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp this.Metadata = new ImageMetadata(); this.currentStream = stream; - this.ReadImageHeader(); + var fileSize = this.ReadImageHeader(); using (this.webImageInfo = this.ReadVp8Info()) { @@ -131,7 +131,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp /// /// Reads and skips over the image header. /// - /// The chunk size in bytes. + /// The file size in bytes. private uint ReadImageHeader() { // Skip FourCC header, we already know its a RIFF file at this point. @@ -140,12 +140,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp // Read file size. // The size of the file in bytes starting at offset 8. // The file size in the header is the total size of the chunks that follow plus 4 bytes for the ‘WEBP’ FourCC. - uint chunkSize = this.ReadChunkSize(); + uint fileSize = this.ReadChunkSize(); // Skip 'WEBP' from the header. this.currentStream.Skip(4); - return chunkSize; + return fileSize; } /// @@ -185,7 +185,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp private WebpImageInfo ReadVp8XHeader() { var features = new WebpFeatures(); - uint chunkSize = this.ReadChunkSize(); + uint fileSize = this.ReadChunkSize(); // The first byte contains information about the image features used. byte imageFeatures = (byte)this.currentStream.ReadByte(); @@ -474,7 +474,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp WebpChunkType chunkType = this.ReadChunkType(); uint chunkLength = this.ReadChunkSize(); - if (chunkType == WebpChunkType.Exif) + if (chunkType == WebpChunkType.Exif && this.Metadata.ExifProfile == null) { var exifData = new byte[chunkLength]; this.currentStream.Read(exifData, 0, (int)chunkLength); @@ -482,7 +482,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp } else { - // Skip XMP chunk data for now. + // Skip XMP chunk data or any duplicate EXIF chunk. this.currentStream.Skip((int)chunkLength); } } diff --git a/src/ImageSharp/Formats/WebP/WebpThrowHelper.cs b/src/ImageSharp/Formats/WebP/WebpThrowHelper.cs index 4918e1ed3..13a62454b 100644 --- a/src/ImageSharp/Formats/WebP/WebpThrowHelper.cs +++ b/src/ImageSharp/Formats/WebP/WebpThrowHelper.cs @@ -13,19 +13,20 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Webp /// /// The error message for the exception. [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowImageFormatException(string errorMessage) - { - throw new ImageFormatException(errorMessage); - } + public static void ThrowImageFormatException(string errorMessage) => throw new ImageFormatException(errorMessage); /// /// Cold path optimization for throwing -s /// /// The error message for the exception. [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowNotSupportedException(string errorMessage) - { - throw new NotSupportedException(errorMessage); - } + public static void ThrowNotSupportedException(string errorMessage) => throw new NotSupportedException(errorMessage); + + /// + /// Cold path optimization for throwing -s + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidImageDimensions(string errorMessage) => throw new InvalidImageContentException(errorMessage); } }