Skip to content

Commit

Permalink
Merge pull request #2699 from SixLabors/js/fix-webp-alpha-flags
Browse files Browse the repository at this point in the history
Ensure VP8X alpha flag is updated correctly.
  • Loading branch information
JimBobSquarePants committed Apr 1, 2024
2 parents a29c6fd + a197f22 commit 326f76d
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 35 deletions.
17 changes: 16 additions & 1 deletion src/ImageSharp/Common/Helpers/RiffHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Buffers.Binary;
using System.Text;
using SixLabors.ImageSharp.Formats.Webp.Chunks;

namespace SixLabors.ImageSharp.Common.Helpers;

Expand Down Expand Up @@ -107,6 +108,7 @@ public static void EndWriteChunk(Stream stream, long sizePosition)
position++;
}

// Add the size of the encoded file to the Riff header.
BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize);
stream.Position = sizePosition;
stream.Write(buffer);
Expand All @@ -120,5 +122,18 @@ public static long BeginWriteRiffFile(Stream stream, string formType)
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;
}
}
}
26 changes: 19 additions & 7 deletions src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired)
/// <param name="iccProfile">The color profile.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
public static void WriteTrunksBeforeData(
/// <returns>A <see cref="WebpVp8X"/> or a default instance.</returns>
public static WebpVp8X WriteTrunksBeforeData(
Stream stream,
uint width,
uint height,
Expand All @@ -102,16 +103,19 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired)
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;
}

/// <summary>
Expand All @@ -124,10 +128,16 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired)
/// Write the trunks after data trunk.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="vp8x">The VP8X chunk.</param>
/// <param name="updateVp8x">Whether to update the chunk.</param>
/// <param name="initialPosition">The initial position of the stream before encoding.</param>
/// <param name="exifProfile">The EXIF profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
public static void WriteTrunksAfterData(
Stream stream,
in WebpVp8X vp8x,
bool updateVp8x,
long initialPosition,
ExifProfile? exifProfile,
XmpProfile? xmpProfile)
{
Expand All @@ -141,7 +151,7 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired)
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data);
}

RiffHelper.EndWriteRiffFile(stream, 4);
RiffHelper.EndWriteRiffFile(stream, in vp8x, updateVp8x, initialPosition);
}

/// <summary>
Expand Down Expand Up @@ -186,19 +196,21 @@ public static void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alp
/// 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="exifProfile">An EXIF profile or null, if it does not exist.</param>
/// <param name="xmpProfile">An XMP profile or null, if it does not exist.</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>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
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;
}
}
23 changes: 22 additions & 1 deletion src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace SixLabors.ImageSharp.Formats.Webp.Chunks;

internal readonly struct WebpVp8X
internal readonly struct WebpVp8X : IEquatable<WebpVp8X>
{
public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height)
{
Expand Down Expand Up @@ -53,6 +53,24 @@ public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, boo
/// </summary>
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)
Expand All @@ -67,6 +85,9 @@ public void Validate(uint maxDimension, ulong maxCanvasPixels)
}
}

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;
Expand Down
20 changes: 14 additions & 6 deletions src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ internal class Vp8LEncoder : IDisposable
/// </summary>
public Vp8LHashChain HashChain { get; }

public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation)
public WebpVp8X EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
// Write bytes from the bit-writer buffer to the stream.
Expand All @@ -247,7 +247,8 @@ public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAni
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,
Expand All @@ -262,9 +263,11 @@ public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAni
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}

return vp8x;
}

public void EncodeFooter<TPixel>(Image<TPixel> image, Stream stream)
public void EncodeFooter<TPixel>(Image<TPixel> image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition)
where TPixel : unmanaged, IPixel<TPixel>
{
// Write bytes from the bit-writer buffer to the stream.
Expand All @@ -273,7 +276,9 @@ public void EncodeFooter<TPixel>(Image<TPixel> image, Stream stream)
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);
}

/// <summary>
Expand All @@ -285,7 +290,8 @@ public void EncodeFooter<TPixel>(Image<TPixel> image, Stream stream)
/// <param name="frameMetadata">The frame metadata.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
public void Encode<TPixel>(ImageFrame<TPixel> frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation)
/// <returns>A <see cref="bool"/> indicating whether the frame contains an alpha channel.</returns>
public bool Encode<TPixel>(ImageFrame<TPixel> frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
// Convert image pixels to bgra array.
Expand Down Expand Up @@ -324,6 +330,8 @@ public void Encode<TPixel>(ImageFrame<TPixel> frame, Rectangle bounds, WebpFrame
{
RiffHelper.EndWriteChunk(stream, prevPosition);
}

return hasAlpha;
}

/// <summary>
Expand Down Expand Up @@ -502,7 +510,7 @@ private void EncodeStream(int width, int height)
/// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="pixels">The frame pixel buffer to convert.</param>
/// <returns>true, if the image is non opaque.</returns>
private bool ConvertPixelsToBgra<TPixel>(Buffer2DRegion<TPixel> pixels)
public bool ConvertPixelsToBgra<TPixel>(Buffer2DRegion<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
bool nonOpaque = false;
Expand Down
24 changes: 16 additions & 8 deletions src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ internal class Vp8Encoder : IDisposable
/// </summary>
private int MbHeaderLimit { get; }

public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAlpha, bool hasAnimation)
public WebpVp8X EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAlpha, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
// Write bytes from the bitwriter buffer to the stream.
Expand All @@ -321,7 +321,7 @@ public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAlp
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,
Expand All @@ -336,9 +336,11 @@ public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAlp
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}

return vp8x;
}

public void EncodeFooter<TPixel>(Image<TPixel> image, Stream stream)
public void EncodeFooter<TPixel>(Image<TPixel> image, in WebpVp8X vp8x, bool hasAlpha, Stream stream, long initialPosition)
where TPixel : unmanaged, IPixel<TPixel>
{
// Write bytes from the bitwriter buffer to the stream.
Expand All @@ -347,7 +349,9 @@ public void EncodeFooter<TPixel>(Image<TPixel> image, Stream stream)
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);
}

/// <summary>
Expand All @@ -358,9 +362,10 @@ public void EncodeFooter<TPixel>(Image<TPixel> image, Stream stream)
/// <param name="stream">The stream to encode the image data to.</param>
/// <param name="bounds">The region of interest within the frame to encode.</param>
/// <param name="frameMetadata">The frame metadata.</param>
public void EncodeAnimation<TPixel>(ImageFrame<TPixel> frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata)
where TPixel : unmanaged, IPixel<TPixel> =>
this.Encode(stream, frame, bounds, frameMetadata, true, null);
/// <returns>A <see cref="bool"/> indicating whether the frame contains an alpha channel.</returns>
public bool EncodeAnimation<TPixel>(ImageFrame<TPixel> frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata)
where TPixel : unmanaged, IPixel<TPixel>
=> this.Encode(stream, frame, bounds, frameMetadata, true, null);

/// <summary>
/// Encodes the static image frame to the specified stream.
Expand All @@ -385,7 +390,8 @@ public void EncodeStatic<TPixel>(Stream stream, Image<TPixel> image)
/// <param name="frameMetadata">The frame metadata.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
/// <param name="image">The image to encode from.</param>
private void Encode<TPixel>(Stream stream, ImageFrame<TPixel> frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image<TPixel> image)
/// <returns>A <see cref="bool"/> indicating whether the frame contains an alpha channel.</returns>
private bool Encode<TPixel>(Stream stream, ImageFrame<TPixel> frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = bounds.Width;
Expand Down Expand Up @@ -515,6 +521,8 @@ private void Encode<TPixel>(Stream stream, ImageFrame<TPixel> frame, Rectangle b
{
encodedAlphaData?.Dispose();
}

return hasAlpha;
}

/// <inheritdoc/>
Expand Down
1 change: 0 additions & 1 deletion src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
/// <summary>
/// The flag to decide how to handle the background color in the Animation Chunk.
/// </summary>
private BackgroundColorHandling backgroundColorHandling;
private readonly BackgroundColorHandling backgroundColorHandling;

/// <summary>
/// Initializes a new instance of the <see cref="WebpDecoderCore"/> class.
Expand Down

0 comments on commit 326f76d

Please sign in to comment.