Skip to content

Commit

Permalink
Merge pull request #2109 from SixLabors/bp/webp-iccprofile
Browse files Browse the repository at this point in the history
Preserve color profile when encoding webp images
  • Loading branch information
JimBobSquarePants authored May 9, 2022
2 parents 2aa150c + 587cec2 commit fcac74b
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 11 deletions.
36 changes: 34 additions & 2 deletions src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -97,7 +98,7 @@ protected void WriteRiffHeader(Stream stream, uint riffSize)
}

/// <summary>
/// Calculates the chunk size of EXIF or XMP metadata.
/// Calculates the chunk size of EXIF, XMP or ICCP metadata.
/// </summary>
/// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The metadata chunk size in bytes.</returns>
Expand Down Expand Up @@ -178,16 +179,41 @@ protected void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDa
}
}

/// <summary>
/// Writes the color profile to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param>
protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes)
{
uint size = (uint)iccProfileBytes.Length;

Span<byte> 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);
}
}

/// <summary>
/// Writes a VP8X header to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
/// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, 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)
{
Expand Down Expand Up @@ -219,6 +245,12 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfil
flags |= 16;
}

if (iccProfileBytes != null)
{
// Set iccp flag.
flags |= 32;
}

Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
stream.Write(WebpConstants.Vp8XMagicBytes);
BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize);
Expand Down
26 changes: 22 additions & 4 deletions src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -406,6 +407,7 @@ private void Flush()
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
Expand All @@ -415,6 +417,7 @@ public void WriteEncodedImageToStream(
Stream stream,
ExifProfile exifProfile,
XmpProfile xmpProfile,
IccProfile iccProfile,
uint width,
uint height,
bool hasAlpha,
Expand All @@ -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)
{
Expand All @@ -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;
Expand All @@ -457,20 +468,20 @@ 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;
vp8Size += numBytes;
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, iccProfileBytes, hasAlpha, alphaData, alphaDataIsCompressed);
bitWriterPartZero.WriteToStream(stream);

// Write the encoded image to the stream.
Expand Down Expand Up @@ -668,6 +679,7 @@ private void WriteWebpHeaders(
uint height,
ExifProfile exifProfile,
XmpProfile xmpProfile,
byte[] iccProfileBytes,
bool hasAlpha,
Span<byte> alphaData,
bool alphaDataIsCompressed)
Expand All @@ -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, iccProfileBytes, width, height, hasAlpha);

if (iccProfileBytes != null)
{
this.WriteColorProfile(stream, iccProfileBytes);
}

if (hasAlpha)
{
this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed);
Expand Down
26 changes: 22 additions & 4 deletions src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -134,31 +135,43 @@ public override void Finish()
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
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)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
exifBytes = exifProfile.ToByteArray();
riffSize += this.MetadataChunkSize(exifBytes);
}

if (xmpProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
xmpBytes = xmpProfile.Data;
riffSize += this.MetadataChunkSize(xmpBytes);
}

if (iccProfile != null)
{
isVp8X = true;
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.
Expand All @@ -171,7 +184,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, iccBytes, width, height, hasAlpha);

if (iccBytes != null)
{
this.WriteColorProfile(stream, iccBytes);
}
}

// Write magic bytes indicating its a lossless webp.
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ public void Encode<TPixel>(Image<TPixel> 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);
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
stream,
metadata.ExifProfile,
metadata.XmpProfile,
metadata.IccProfile,
(uint)width,
(uint)height,
hasAlpha,
Expand Down
31 changes: 31 additions & 0 deletions tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,37 @@ public void EncodeLosslessWebp_PreservesExif<TPixel>(TestImageProvider<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider, WebpFileFormatType fileFormat)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> 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<Rgba32>(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<TPixel>(TestImageProvider<TPixel> provider)
Expand Down

0 comments on commit fcac74b

Please sign in to comment.