Skip to content

Commit b7e348f

Browse files
Merge pull request #1971 from SixLabors/bp/webpalpha
Add support for encoding lossy webp images with alpha channels
2 parents cb3896a + 2491b6a commit b7e348f

File tree

12 files changed

+345
-39
lines changed

12 files changed

+345
-39
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Buffers;
6+
using SixLabors.ImageSharp.Advanced;
7+
using SixLabors.ImageSharp.Formats.Webp.Lossless;
8+
using SixLabors.ImageSharp.Memory;
9+
using SixLabors.ImageSharp.PixelFormats;
10+
11+
namespace SixLabors.ImageSharp.Formats.Webp
12+
{
13+
/// <summary>
14+
/// Methods for encoding the alpha data of a VP8 image.
15+
/// </summary>
16+
internal class AlphaEncoder : IDisposable
17+
{
18+
private IMemoryOwner<byte> alphaData;
19+
20+
/// <summary>
21+
/// Encodes the alpha channel data.
22+
/// Data is either compressed as lossless webp image or uncompressed.
23+
/// </summary>
24+
/// <typeparam name="TPixel">The pixel format.</typeparam>
25+
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
26+
/// <param name="configuration">The global configuration.</param>
27+
/// <param name="memoryAllocator">The memory manager.</param>
28+
/// <param name="compress">Indicates, if the data should be compressed with the lossless webp compression.</param>
29+
/// <param name="size">The size in bytes of the alpha data.</param>
30+
/// <returns>The encoded alpha data.</returns>
31+
public IMemoryOwner<byte> EncodeAlpha<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator, bool compress, out int size)
32+
where TPixel : unmanaged, IPixel<TPixel>
33+
{
34+
int width = image.Width;
35+
int height = image.Height;
36+
this.alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator);
37+
38+
if (compress)
39+
{
40+
WebpEncodingMethod effort = WebpEncodingMethod.Default;
41+
int quality = 8 * (int)effort;
42+
using var lossLessEncoder = new Vp8LEncoder(
43+
memoryAllocator,
44+
configuration,
45+
width,
46+
height,
47+
quality,
48+
effort,
49+
WebpTransparentColorMode.Preserve,
50+
false,
51+
0);
52+
53+
// The transparency information will be stored in the green channel of the ARGB quadruplet.
54+
// The green channel is allowed extra transformation steps in the specification -- unlike the other channels,
55+
// that can improve compression.
56+
using Image<Rgba32> alphaAsImage = DispatchAlphaToGreen(image, this.alphaData.GetSpan());
57+
58+
size = lossLessEncoder.EncodeAlphaImageData(alphaAsImage, this.alphaData);
59+
60+
return this.alphaData;
61+
}
62+
63+
size = width * height;
64+
return this.alphaData;
65+
}
66+
67+
/// <summary>
68+
/// Store the transparency in the green channel.
69+
/// </summary>
70+
/// <typeparam name="TPixel">The pixel format.</typeparam>
71+
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
72+
/// <param name="alphaData">A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</param>
73+
/// <returns>The transparency image.</returns>
74+
private static Image<Rgba32> DispatchAlphaToGreen<TPixel>(Image<TPixel> image, Span<byte> alphaData)
75+
where TPixel : unmanaged, IPixel<TPixel>
76+
{
77+
int width = image.Width;
78+
int height = image.Height;
79+
var alphaAsImage = new Image<Rgba32>(width, height);
80+
81+
for (int y = 0; y < height; y++)
82+
{
83+
Memory<Rgba32> rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y);
84+
Span<Rgba32> pixelRow = rowBuffer.Span;
85+
Span<byte> alphaRow = alphaData.Slice(y * width, width);
86+
for (int x = 0; x < width; x++)
87+
{
88+
// Leave A/R/B channels zero'd.
89+
pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0);
90+
}
91+
}
92+
93+
return alphaAsImage;
94+
}
95+
96+
/// <summary>
97+
/// Extract the alpha data of the image.
98+
/// </summary>
99+
/// <typeparam name="TPixel">The pixel format.</typeparam>
100+
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
101+
/// <param name="configuration">The global configuration.</param>
102+
/// <param name="memoryAllocator">The memory manager.</param>
103+
/// <returns>A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</returns>
104+
private static IMemoryOwner<byte> ExtractAlphaChannel<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator)
105+
where TPixel : unmanaged, IPixel<TPixel>
106+
{
107+
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
108+
int height = image.Height;
109+
int width = image.Width;
110+
IMemoryOwner<byte> alphaDataBuffer = memoryAllocator.Allocate<byte>(width * height);
111+
Span<byte> alphaData = alphaDataBuffer.GetSpan();
112+
113+
using IMemoryOwner<Rgba32> rowBuffer = memoryAllocator.Allocate<Rgba32>(width);
114+
Span<Rgba32> rgbaRow = rowBuffer.GetSpan();
115+
116+
for (int y = 0; y < height; y++)
117+
{
118+
Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(y);
119+
PixelOperations<TPixel>.Instance.ToRgba32(configuration, rowSpan, rgbaRow);
120+
int offset = y * width;
121+
for (int x = 0; x < width; x++)
122+
{
123+
alphaData[offset + x] = rgbaRow[x].A;
124+
}
125+
}
126+
127+
return alphaDataBuffer;
128+
}
129+
130+
/// <inheritdoc/>
131+
public void Dispose() => this.alphaData?.Dispose();
132+
}
133+
}

src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ internal abstract class BitWriterBase
4747
/// <param name="stream">The stream to write to.</param>
4848
public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes()));
4949

50+
/// <summary>
51+
/// Writes the encoded bytes of the image to the given buffer. Call Finish() before this.
52+
/// </summary>
53+
/// <param name="dest">The destination buffer.</param>
54+
public void WriteToBuffer(Span<byte> dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest);
55+
5056
/// <summary>
5157
/// Resizes the buffer to write to.
5258
/// </summary>
@@ -94,7 +100,7 @@ protected void WriteRiffHeader(Stream stream, uint riffSize)
94100
/// Calculates the chunk size of EXIF or XMP metadata.
95101
/// </summary>
96102
/// <param name="metadataBytes">The metadata profile bytes.</param>
97-
/// <returns>The exif chunk size in bytes.</returns>
103+
/// <returns>The metadata chunk size in bytes.</returns>
98104
protected uint MetadataChunkSize(byte[] metadataBytes)
99105
{
100106
uint metaSize = (uint)metadataBytes.Length;
@@ -103,6 +109,19 @@ protected uint MetadataChunkSize(byte[] metadataBytes)
103109
return metaChunkSize;
104110
}
105111

112+
/// <summary>
113+
/// Calculates the chunk size of a alpha chunk.
114+
/// </summary>
115+
/// <param name="alphaBytes">The alpha chunk bytes.</param>
116+
/// <returns>The alpha data chunk size in bytes.</returns>
117+
protected uint AlphaChunkSize(Span<byte> alphaBytes)
118+
{
119+
uint alphaSize = (uint)alphaBytes.Length + 1;
120+
uint alphaChunkSize = WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1);
121+
122+
return alphaChunkSize;
123+
}
124+
106125
/// <summary>
107126
/// Writes a metadata profile (EXIF or XMP) to the stream.
108127
/// </summary>
@@ -128,6 +147,37 @@ protected void WriteMetadataProfile(Stream stream, byte[] metadataBytes, WebpChu
128147
}
129148
}
130149

150+
/// <summary>
151+
/// Writes the alpha chunk to the stream.
152+
/// </summary>
153+
/// <param name="stream">The stream to write to.</param>
154+
/// <param name="dataBytes">The alpha channel data bytes.</param>
155+
/// <param name="alphaDataIsCompressed">Indicates, if the alpha channel data is compressed.</param>
156+
protected void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed)
157+
{
158+
uint size = (uint)dataBytes.Length + 1;
159+
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
160+
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha);
161+
stream.Write(buf);
162+
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
163+
stream.Write(buf);
164+
165+
byte flags = 0;
166+
if (alphaDataIsCompressed)
167+
{
168+
flags |= 1;
169+
}
170+
171+
stream.WriteByte(flags);
172+
stream.Write(dataBytes);
173+
174+
// Add padding byte if needed.
175+
if ((size & 1) == 1)
176+
{
177+
stream.WriteByte(0);
178+
}
179+
}
180+
131181
/// <summary>
132182
/// Writes a VP8X header to the stream.
133183
/// </summary>

src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,17 @@ private void Flush()
409409
/// <param name="width">The width of the image.</param>
410410
/// <param name="height">The height of the image.</param>
411411
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
412-
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha)
412+
/// <param name="alphaData">The alpha channel data.</param>
413+
/// <param name="alphaDataIsCompressed">Indicates, if the alpha data is compressed.</param>
414+
public void WriteEncodedImageToStream(
415+
Stream stream,
416+
ExifProfile exifProfile,
417+
XmpProfile xmpProfile,
418+
uint width,
419+
uint height,
420+
bool hasAlpha,
421+
Span<byte> alphaData,
422+
bool alphaDataIsCompressed)
413423
{
414424
bool isVp8X = false;
415425
byte[] exifBytes = null;
@@ -418,19 +428,28 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm
418428
if (exifProfile != null)
419429
{
420430
isVp8X = true;
421-
riffSize += ExtendedFileChunkSize;
422431
exifBytes = exifProfile.ToByteArray();
423432
riffSize += this.MetadataChunkSize(exifBytes);
424433
}
425434

426435
if (xmpProfile != null)
427436
{
428437
isVp8X = true;
429-
riffSize += ExtendedFileChunkSize;
430438
xmpBytes = xmpProfile.Data;
431439
riffSize += this.MetadataChunkSize(xmpBytes);
432440
}
433441

442+
if (hasAlpha)
443+
{
444+
isVp8X = true;
445+
riffSize += this.AlphaChunkSize(alphaData);
446+
}
447+
448+
if (isVp8X)
449+
{
450+
riffSize += ExtendedFileChunkSize;
451+
}
452+
434453
this.Finish();
435454
uint numBytes = (uint)this.NumBytes();
436455
int mbSize = this.enc.Mbw * this.enc.Mbh;
@@ -451,7 +470,7 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, Xm
451470
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;
452471

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

457476
// Write the encoded image to the stream.
@@ -639,14 +658,30 @@ private void CodeIntraModes(Vp8BitWriter bitWriter)
639658
while (it.Next());
640659
}
641660

642-
private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, XmpProfile xmpProfile, bool hasAlpha)
661+
private void WriteWebpHeaders(
662+
Stream stream,
663+
uint size0,
664+
uint vp8Size,
665+
uint riffSize,
666+
bool isVp8X,
667+
uint width,
668+
uint height,
669+
ExifProfile exifProfile,
670+
XmpProfile xmpProfile,
671+
bool hasAlpha,
672+
Span<byte> alphaData,
673+
bool alphaDataIsCompressed)
643674
{
644675
this.WriteRiffHeader(stream, riffSize);
645676

646677
// Write VP8X, header if necessary.
647678
if (isVp8X)
648679
{
649680
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha);
681+
if (hasAlpha)
682+
{
683+
this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed);
684+
}
650685
}
651686

652687
this.WriteVp8Header(stream, vp8Size);

src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal interface IWebpEncoderOptions
3131

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

src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,18 +228,20 @@ public Vp8LEncoder(
228228
public Vp8LHashChain HashChain { get; }
229229

230230
/// <summary>
231-
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
231+
/// Encodes the image as lossless webp to the specified stream.
232232
/// </summary>
233233
/// <typeparam name="TPixel">The pixel format.</typeparam>
234234
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
235235
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
236236
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
237237
where TPixel : unmanaged, IPixel<TPixel>
238238
{
239-
image.Metadata.SyncProfiles();
240239
int width = image.Width;
241240
int height = image.Height;
242241

242+
ImageMetadata metadata = image.Metadata;
243+
metadata.SyncProfiles();
244+
243245
// Convert image pixels to bgra array.
244246
bool hasAlpha = this.ConvertPixelsToBgra(image, width, height);
245247

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

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

261+
/// <summary>
262+
/// Encodes the alpha image data using the webp lossless compression.
263+
/// </summary>
264+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
265+
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
266+
/// <param name="alphaData">The destination buffer to write the encoded alpha data to.</param>
267+
/// <returns>The size of the compressed data in bytes.
268+
/// 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.
269+
/// </returns>
270+
public int EncodeAlphaImageData<TPixel>(Image<TPixel> image, IMemoryOwner<byte> alphaData)
271+
where TPixel : unmanaged, IPixel<TPixel>
272+
{
273+
int width = image.Width;
274+
int height = image.Height;
275+
int pixelCount = width * height;
276+
277+
// Convert image pixels to bgra array.
278+
this.ConvertPixelsToBgra(image, width, height);
279+
280+
// The image-stream will NOT contain any headers describing the image dimension, the dimension is already known.
281+
this.EncodeStream(image);
282+
this.bitWriter.Finish();
283+
int size = this.bitWriter.NumBytes();
284+
if (size >= pixelCount)
285+
{
286+
// Compressing would not yield in smaller data -> leave the data uncompressed.
287+
return pixelCount;
288+
}
289+
290+
this.bitWriter.WriteToBuffer(alphaData.GetSpan());
291+
return size;
292+
}
293+
261294
/// <summary>
262295
/// Writes the image size to the bitwriter buffer.
263296
/// </summary>

0 commit comments

Comments
 (0)