From 43c83dabc77d2a72453899783d520b93a378db7b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 14:36:30 +0200 Subject: [PATCH 1/2] Preserve color profile when encoding webp --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 34 ++++++++++++++++++- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 26 +++++++++++--- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 20 +++++++++-- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 1 + 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index fc1accfdee..3c503f5e0e 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -5,6 +5,7 @@ using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter @@ -178,16 +179,41 @@ protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDa } } + /// + /// Writes the color profile to the stream. + /// + /// The stream to write to. + /// The color profile bytes. + protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) + { + uint size = (uint)iccProfileBytes.Length; + + Span buf = this.scratchBuffer.AsSpan(0, 4); + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); + + stream.Write(iccProfileBytes); + + // Add padding byte if needed. + if ((size & 1) == 1) + { + stream.WriteByte(0); + } + } + /// /// Writes a VP8X header to the stream. /// /// 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. /// 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, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, IccProfile iccProfile, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -219,6 +245,12 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfil flags |= 16; } + if (iccProfile != null) + { + // Set iccp flag. + flags |= 32; + } + 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 fa6e09d875..cfc024624e 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -6,6 +6,7 @@ using System.IO; 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 @@ -406,6 +407,7 @@ private void Flush() /// 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. @@ -415,6 +417,7 @@ public void WriteEncodedImageToStream( Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, + IccProfile iccProfile, uint width, uint height, bool hasAlpha, @@ -424,6 +427,7 @@ public void WriteEncodedImageToStream( bool isVp8X = false; byte[] exifBytes = null; byte[] xmpBytes = null; + byte[] iccProfileBytes = null; uint riffSize = 0; if (exifProfile != null) { @@ -439,6 +443,13 @@ public void WriteEncodedImageToStream( riffSize += this.MetadataChunkSize(xmpBytes); } + if (iccProfile != null) + { + isVp8X = true; + iccProfileBytes = iccProfile.ToByteArray(); + riffSize += this.MetadataChunkSize(iccProfileBytes); + } + if (hasAlpha) { isVp8X = true; @@ -457,7 +468,7 @@ public void WriteEncodedImageToStream( var bitWriterPartZero = new Vp8BitWriter(expectedSize); - // Partition #0 with header and partition sizes + // Partition #0 with header and partition sizes. uint size0 = this.GeneratePartition0(bitWriterPartZero); uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; @@ -465,12 +476,12 @@ public void WriteEncodedImageToStream( uint pad = vp8Size & 1; vp8Size += pad; - // Compute RIFF size + // Compute RIFF size. // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha, alphaData, alphaDataIsCompressed); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfile, hasAlpha, alphaData, alphaDataIsCompressed); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -668,6 +679,7 @@ private void WriteWebpHeaders( uint height, ExifProfile exifProfile, XmpProfile xmpProfile, + IccProfile iccProfile, bool hasAlpha, Span alphaData, bool alphaDataIsCompressed) @@ -677,7 +689,13 @@ private void WriteWebpHeaders( // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + + if (iccProfile != null) + { + this.WriteColorProfile(stream, iccProfile.ToByteArray()); + } + if (hasAlpha) { this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index d41224f908..60f6b97c89 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -6,6 +6,7 @@ using System.IO; 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 @@ -134,14 +135,16 @@ public override void Finish() /// 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, uint width, uint height, bool hasAlpha) + public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, IccProfile iccProfile, uint width, uint height, bool hasAlpha) { bool isVp8X = false; byte[] exifBytes = null; byte[] xmpBytes = null; + byte[] iccBytes = null; uint riffSize = 0; if (exifProfile != null) { @@ -159,6 +162,14 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm riffSize += this.MetadataChunkSize(xmpBytes); } + if (iccProfile != null) + { + isVp8X = true; + riffSize += ExtendedFileChunkSize; + iccBytes = iccProfile.ToByteArray(); + riffSize += this.MetadataChunkSize(iccBytes); + } + this.Finish(); uint size = (uint)this.NumBytes(); size++; // One byte extra for the VP8L signature. @@ -171,7 +182,12 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + + if (iccBytes != null) + { + this.WriteColorProfile(stream, iccBytes); + } } // 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 30d65562ae..8de1ccc420 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -255,7 +255,7 @@ public void Encode(Image image, Stream stream) this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha); + this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 695359e5ea..f24fdb45d8 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -378,6 +378,7 @@ public void Encode(Image image, Stream stream) stream, metadata.ExifProfile, metadata.XmpProfile, + metadata.IccProfile, (uint)width, (uint)height, hasAlpha, From 5bac8b36bfb7909fc229f95d98cdb64c30309db5 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 7 May 2022 16:15:32 +0200 Subject: [PATCH 2/2] Add test for ICCP --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 8 ++--- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 10 +++--- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 10 +++--- .../Formats/WebP/WebpMetaDataTests.cs | 31 +++++++++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 3c503f5e0e..0e1c9f4d95 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -98,7 +98,7 @@ protected void WriteRiffHeader(Stream stream, uint riffSize) } /// - /// Calculates the chunk size of EXIF or XMP metadata. + /// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// /// The metadata profile bytes. /// The metadata chunk size in bytes. @@ -209,11 +209,11 @@ 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. + /// The color profile bytes. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, IccProfile iccProfile, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, byte[] iccProfileBytes, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -245,7 +245,7 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfil flags |= 16; } - if (iccProfile != null) + if (iccProfileBytes != null) { // Set iccp flag. flags |= 32; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index cfc024624e..7cc915e18f 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -481,7 +481,7 @@ public void WriteEncodedImageToStream( riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfile, hasAlpha, alphaData, alphaDataIsCompressed); + 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. @@ -679,7 +679,7 @@ private void WriteWebpHeaders( uint height, ExifProfile exifProfile, XmpProfile xmpProfile, - IccProfile iccProfile, + byte[] iccProfileBytes, bool hasAlpha, Span alphaData, bool alphaDataIsCompressed) @@ -689,11 +689,11 @@ private void WriteWebpHeaders( // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfileBytes, width, height, hasAlpha); - if (iccProfile != null) + if (iccProfileBytes != null) { - this.WriteColorProfile(stream, iccProfile.ToByteArray()); + this.WriteColorProfile(stream, iccProfileBytes); } if (hasAlpha) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 60f6b97c89..7bd6febeb6 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -149,7 +149,6 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm if (exifProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); riffSize += this.MetadataChunkSize(exifBytes); } @@ -157,7 +156,6 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm if (xmpProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; xmpBytes = xmpProfile.Data; riffSize += this.MetadataChunkSize(xmpBytes); } @@ -165,11 +163,15 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm if (iccProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; iccBytes = iccProfile.ToByteArray(); riffSize += this.MetadataChunkSize(iccBytes); } + if (isVp8X) + { + riffSize += ExtendedFileChunkSize; + } + this.Finish(); uint size = (uint)this.NumBytes(); size++; // One byte extra for the VP8L signature. @@ -182,7 +184,7 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccBytes, width, height, hasAlpha); if (iccBytes != null) { diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 456b9a3f52..d1f148be02 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -152,6 +152,37 @@ public void EncodeLosslessWebp_PreservesExif(TestImageProvider p Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count); } + [Theory] + [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] + [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] + public void Encode_PreservesColorProfile(TestImageProvider provider, WebpFileFormatType fileFormat) + where TPixel : unmanaged, IPixel + { + using (Image input = provider.GetImage(new WebpDecoder())) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; + byte[] expectedProfileBytes = expectedProfile.ToByteArray(); + + using (var memStream = new MemoryStream()) + { + input.Save(memStream, new WebpEncoder() + { + FileFormat = fileFormat + }); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile; + byte[] actualProfileBytes = actualProfile.ToByteArray(); + + Assert.NotNull(actualProfile); + Assert.Equal(expectedProfileBytes, actualProfileBytes); + } + } + } + } + [Theory] [WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)] public void WebpDecoder_IgnoresInvalidExifChunk(TestImageProvider provider)