Skip to content
133 changes: 133 additions & 0 deletions src/ImageSharp/Formats/Webp/AlphaEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Buffers;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;

namespace SixLabors.ImageSharp.Formats.Webp
{
/// <summary>
/// Methods for encoding the alpha data of a VP8 image.
/// </summary>
internal class AlphaEncoder : IDisposable
{
private IMemoryOwner<byte> alphaData;

/// <summary>
/// Encodes the alpha channel data.
/// Data is either compressed as lossless webp image or uncompressed.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param>
/// <param name="compress">Indicates, if the data should be compressed with the lossless webp compression.</param>
/// <param name="size">The size in bytes of the alpha data.</param>
/// <returns>The encoded alpha data.</returns>
public IMemoryOwner<byte> EncodeAlpha<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator, bool compress, out int size)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
this.alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator);

if (compress)
{
WebpEncodingMethod effort = WebpEncodingMethod.Default;
int quality = 8 * (int)effort;
using var lossLessEncoder = new Vp8LEncoder(
memoryAllocator,
configuration,
width,
height,
quality,
effort,
WebpTransparentColorMode.Preserve,
false,
0);

// The transparency information will be stored in the green channel of the ARGB quadruplet.
// The green channel is allowed extra transformation steps in the specification -- unlike the other channels,
// that can improve compression.
using Image<Rgba32> alphaAsImage = DispatchAlphaToGreen(image, this.alphaData.GetSpan());

size = lossLessEncoder.EncodeAlphaImageData(alphaAsImage, this.alphaData);

return this.alphaData;
}

size = width * height;
return this.alphaData;
}

/// <summary>
/// Store the transparency in the green channel.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="alphaData">A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</param>
/// <returns>The transparency image.</returns>
private static Image<Rgba32> DispatchAlphaToGreen<TPixel>(Image<TPixel> image, Span<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
var alphaAsImage = new Image<Rgba32>(width, height);

for (int y = 0; y < height; y++)
{
Memory<Rgba32> rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y);
Span<Rgba32> pixelRow = rowBuffer.Span;
Span<byte> alphaRow = alphaData.Slice(y * width, width);
for (int x = 0; x < width; x++)
{
// Leave A/R/B channels zero'd.
pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0);
}
}

return alphaAsImage;
}

/// <summary>
/// Extract the alpha data of the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param>
/// <returns>A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</returns>
private static IMemoryOwner<byte> ExtractAlphaChannel<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
int height = image.Height;
int width = image.Width;
IMemoryOwner<byte> alphaDataBuffer = memoryAllocator.Allocate<byte>(width * height);
Span<byte> alphaData = alphaDataBuffer.GetSpan();

using IMemoryOwner<Rgba32> rowBuffer = memoryAllocator.Allocate<Rgba32>(width);
Span<Rgba32> rgbaRow = rowBuffer.GetSpan();

for (int y = 0; y < height; y++)
{
Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, rowSpan, rgbaRow);
int offset = y * width;
for (int x = 0; x < width; x++)
{
alphaData[offset + x] = rgbaRow[x].A;
}
}

return alphaDataBuffer;
}

/// <inheritdoc/>
public void Dispose() => this.alphaData?.Dispose();
}
}
52 changes: 51 additions & 1 deletion src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ internal abstract class BitWriterBase
/// <param name="stream">The stream to write to.</param>
public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes()));

/// <summary>
/// Writes the encoded bytes of the image to the given buffer. Call Finish() before this.
/// </summary>
/// <param name="dest">The destination buffer.</param>
public void WriteToBuffer(Span<byte> dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest);

/// <summary>
/// Resizes the buffer to write to.
/// </summary>
Expand Down Expand Up @@ -94,7 +100,7 @@ protected void WriteRiffHeader(Stream stream, uint riffSize)
/// Calculates the chunk size of EXIF or XMP metadata.
/// </summary>
/// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The exif chunk size in bytes.</returns>
/// <returns>The metadata chunk size in bytes.</returns>
protected uint MetadataChunkSize(byte[] metadataBytes)
{
uint metaSize = (uint)metadataBytes.Length;
Expand All @@ -103,6 +109,19 @@ protected uint MetadataChunkSize(byte[] metadataBytes)
return metaChunkSize;
}

/// <summary>
/// Calculates the chunk size of a alpha chunk.
/// </summary>
/// <param name="alphaBytes">The alpha chunk bytes.</param>
/// <returns>The alpha data chunk size in bytes.</returns>
protected uint AlphaChunkSize(Span<byte> alphaBytes)
{
uint alphaSize = (uint)alphaBytes.Length + 1;
uint alphaChunkSize = WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1);

return alphaChunkSize;
}

/// <summary>
/// Writes a metadata profile (EXIF or XMP) to the stream.
/// </summary>
Expand All @@ -128,6 +147,37 @@ protected void WriteMetadataProfile(Stream stream, byte[] metadataBytes, WebpChu
}
}

/// <summary>
/// Writes the alpha chunk to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="dataBytes">The alpha channel data bytes.</param>
/// <param name="alphaDataIsCompressed">Indicates, if the alpha channel data is compressed.</param>
protected void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed)
{
uint size = (uint)dataBytes.Length + 1;
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
stream.Write(buf);

byte flags = 0;
if (alphaDataIsCompressed)
{
flags |= 1;
}

stream.WriteByte(flags);
stream.Write(dataBytes);

// Add padding byte if needed.
if ((size & 1) == 1)
{
stream.WriteByte(0);
}
}

/// <summary>
/// Writes a VP8X header to the stream.
/// </summary>
Expand Down
45 changes: 40 additions & 5 deletions src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,17 @@ private void Flush()
/// <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)
/// <param name="alphaData">The alpha channel data.</param>
/// <param name="alphaDataIsCompressed">Indicates, if the alpha data is compressed.</param>
public void WriteEncodedImageToStream(
Stream stream,
ExifProfile exifProfile,
XmpProfile xmpProfile,
uint width,
uint height,
bool hasAlpha,
Span<byte> alphaData,
bool alphaDataIsCompressed)
{
bool isVp8X = false;
byte[] exifBytes = null;
Expand All @@ -418,19 +428,28 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm
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 (hasAlpha)
{
isVp8X = true;
riffSize += this.AlphaChunkSize(alphaData);
}

if (isVp8X)
{
riffSize += ExtendedFileChunkSize;
}

this.Finish();
uint numBytes = (uint)this.NumBytes();
int mbSize = this.enc.Mbw * this.enc.Mbh;
Expand All @@ -451,7 +470,7 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;

// Emit headers and partition #0
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha);
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha, alphaData, alphaDataIsCompressed);
bitWriterPartZero.WriteToStream(stream);

// Write the encoded image to the stream.
Expand Down Expand Up @@ -639,14 +658,30 @@ 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, XmpProfile xmpProfile, bool hasAlpha)
private void WriteWebpHeaders(
Stream stream,
uint size0,
uint vp8Size,
uint riffSize,
bool isVp8X,
uint width,
uint height,
ExifProfile exifProfile,
XmpProfile xmpProfile,
bool hasAlpha,
Span<byte> alphaData,
bool alphaDataIsCompressed)
{
this.WriteRiffHeader(stream, riffSize);

// Write VP8X, header if necessary.
if (isVp8X)
{
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha);
if (hasAlpha)
{
this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed);
}
}

this.WriteVp8Header(stream, vp8Size);
Expand Down
1 change: 1 addition & 0 deletions src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal interface IWebpEncoderOptions

/// <summary>
/// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format.
/// Defaults to true.
/// </summary>
bool UseAlphaCompression { get; }

Expand Down
41 changes: 37 additions & 4 deletions src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,18 +228,20 @@ public Vp8LEncoder(
public Vp8LHashChain HashChain { get; }

/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// Encodes the image as lossless webp to the specified stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
image.Metadata.SyncProfiles();
int width = image.Width;
int height = image.Height;

ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();

// Convert image pixels to bgra array.
bool hasAlpha = this.ConvertPixelsToBgra(image, width, height);

Expand All @@ -253,11 +255,42 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
this.EncodeStream(image);

// Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha);
}

/// <summary>
/// Encodes the alpha image data using the webp lossless compression.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="alphaData">The destination buffer to write the encoded alpha data to.</param>
/// <returns>The size of the compressed data in bytes.
/// If the size of the data is the same as the pixel count, the compression would not yield in smaller data and is left uncompressed.
/// </returns>
public int EncodeAlphaImageData<TPixel>(Image<TPixel> image, IMemoryOwner<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
int pixelCount = width * height;

// Convert image pixels to bgra array.
this.ConvertPixelsToBgra(image, width, height);

// The image-stream will NOT contain any headers describing the image dimension, the dimension is already known.
this.EncodeStream(image);
this.bitWriter.Finish();
int size = this.bitWriter.NumBytes();
if (size >= pixelCount)
{
// Compressing would not yield in smaller data -> leave the data uncompressed.
return pixelCount;
}

this.bitWriter.WriteToBuffer(alphaData.GetSpan());
return size;
}

/// <summary>
/// Writes the image size to the bitwriter buffer.
/// </summary>
Expand Down
Loading