Skip to content

Commit 527e0fb

Browse files
Merge pull request #1798 from SixLabors/bp/webpexifwithpadding
Write exif profile with padding if needed
2 parents b401937 + 7f3c8ff commit 527e0fb

File tree

7 files changed

+104
-39
lines changed

7 files changed

+104
-39
lines changed

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

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,22 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
1010
{
1111
internal abstract class BitWriterBase
1212
{
13+
private const uint MaxDimension = 16777215;
14+
15+
private const ulong MaxCanvasPixels = 4294967295ul;
16+
17+
protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize;
18+
1319
/// <summary>
1420
/// Buffer to write to.
1521
/// </summary>
1622
private byte[] buffer;
1723

24+
/// <summary>
25+
/// A scratch buffer to reduce allocations.
26+
/// </summary>
27+
private readonly byte[] scratchBuffer = new byte[4];
28+
1829
/// <summary>
1930
/// Initializes a new instance of the <see cref="BitWriterBase"/> class.
2031
/// </summary>
@@ -52,15 +63,6 @@ internal abstract class BitWriterBase
5263
/// </summary>
5364
public abstract void Finish();
5465

55-
/// <summary>
56-
/// Writes the encoded image to the stream.
57-
/// </summary>
58-
/// <param name="stream">The stream to write to.</param>
59-
/// <param name="exifProfile">The exif profile.</param>
60-
/// <param name="width">The width of the image.</param>
61-
/// <param name="height">The height of the image.</param>
62-
public abstract void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height);
63-
6466
protected void ResizeBuffer(int maxBytes, int sizeRequired)
6567
{
6668
int newSize = (3 * maxBytes) >> 1;
@@ -81,13 +83,25 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired)
8183
/// <param name="riffSize">The block length.</param>
8284
protected void WriteRiffHeader(Stream stream, uint riffSize)
8385
{
84-
Span<byte> buf = stackalloc byte[4];
8586
stream.Write(WebpConstants.RiffFourCc);
86-
BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize);
87-
stream.Write(buf);
87+
BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer, riffSize);
88+
stream.Write(this.scratchBuffer.AsSpan(0, 4));
8889
stream.Write(WebpConstants.WebpHeader);
8990
}
9091

92+
/// <summary>
93+
/// Calculates the exif chunk size.
94+
/// </summary>
95+
/// <param name="exifBytes">The exif profile bytes.</param>
96+
/// <returns>The exif chunk size in bytes.</returns>
97+
protected uint ExifChunkSize(byte[] exifBytes)
98+
{
99+
uint exifSize = (uint)exifBytes.Length;
100+
uint exifChunkSize = WebpConstants.ChunkHeaderSize + exifSize + (exifSize & 1);
101+
102+
return exifChunkSize;
103+
}
104+
91105
/// <summary>
92106
/// Writes the Exif profile to the stream.
93107
/// </summary>
@@ -97,12 +111,19 @@ protected void WriteExifProfile(Stream stream, byte[] exifBytes)
97111
{
98112
DebugGuard.NotNull(exifBytes, nameof(exifBytes));
99113

100-
Span<byte> buf = stackalloc byte[4];
114+
uint size = (uint)exifBytes.Length;
115+
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
101116
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Exif);
102117
stream.Write(buf);
103-
BinaryPrimitives.WriteUInt32LittleEndian(buf, (uint)exifBytes.Length);
118+
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
104119
stream.Write(buf);
105120
stream.Write(exifBytes);
121+
122+
// Add padding byte if needed.
123+
if ((size & 1) == 1)
124+
{
125+
stream.WriteByte(0);
126+
}
106127
}
107128

108129
/// <summary>
@@ -112,16 +133,16 @@ protected void WriteExifProfile(Stream stream, byte[] exifBytes)
112133
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
113134
/// <param name="width">The width of the image.</param>
114135
/// <param name="height">The height of the image.</param>
115-
protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height)
136+
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
137+
protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha)
116138
{
117-
int maxDimension = 16777215;
118-
if (width > maxDimension || height > maxDimension)
139+
if (width > MaxDimension || height > MaxDimension)
119140
{
120-
WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {maxDimension}");
141+
WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {MaxDimension}");
121142
}
122143

123144
// The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1.
124-
if (width * height > 4294967295ul)
145+
if (width * height > MaxCanvasPixels)
125146
{
126147
WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1");
127148
}
@@ -133,7 +154,13 @@ protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint widt
133154
flags |= 8;
134155
}
135156

136-
Span<byte> buf = stackalloc byte[4];
157+
if (hasAlpha)
158+
{
159+
// Set alpha bit.
160+
flags |= 16;
161+
}
162+
163+
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
137164
stream.Write(WebpConstants.Vp8XMagicBytes);
138165
BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize);
139166
stream.Write(buf);

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -399,18 +399,25 @@ private void Flush()
399399
}
400400
}
401401

402-
/// <inheritdoc/>
403-
public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height)
402+
/// <summary>
403+
/// Writes the encoded image to the stream.
404+
/// </summary>
405+
/// <param name="stream">The stream to write to.</param>
406+
/// <param name="exifProfile">The exif profile.</param>
407+
/// <param name="width">The width of the image.</param>
408+
/// <param name="height">The height of the image.</param>
409+
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
410+
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha)
404411
{
405412
bool isVp8X = false;
406413
byte[] exifBytes = null;
407414
uint riffSize = 0;
408415
if (exifProfile != null)
409416
{
410417
isVp8X = true;
411-
riffSize += WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize;
418+
riffSize += ExtendedFileChunkSize;
412419
exifBytes = exifProfile.ToByteArray();
413-
riffSize += WebpConstants.ChunkHeaderSize + (uint)exifBytes.Length;
420+
riffSize += this.ExifChunkSize(exifBytes);
414421
}
415422

416423
this.Finish();
@@ -433,7 +440,7 @@ public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifPr
433440
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;
434441

435442
// Emit headers and partition #0
436-
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile);
443+
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, hasAlpha);
437444
bitWriterPartZero.WriteToStream(stream);
438445

439446
// Write the encoded image to the stream.
@@ -616,14 +623,14 @@ private void CodeIntraModes(Vp8BitWriter bitWriter)
616623
while (it.Next());
617624
}
618625

619-
private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile)
626+
private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, bool hasAlpha)
620627
{
621628
this.WriteRiffHeader(stream, riffSize);
622629

623630
// Write VP8X, header if necessary.
624631
if (isVp8X)
625632
{
626-
this.WriteVp8XHeader(stream, exifProfile, width, height);
633+
this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha);
627634
}
628635

629636
this.WriteVp8Header(stream, vp8Size);

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,25 @@ public override void Finish()
127127
this.used = 0;
128128
}
129129

130-
/// <inheritdoc/>
131-
public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height)
130+
/// <summary>
131+
/// Writes the encoded image to the stream.
132+
/// </summary>
133+
/// <param name="stream">The stream to write to.</param>
134+
/// <param name="exifProfile">The exif profile.</param>
135+
/// <param name="width">The width of the image.</param>
136+
/// <param name="height">The height of the image.</param>
137+
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
138+
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha)
132139
{
133-
Span<byte> buffer = stackalloc byte[4];
134140
bool isVp8X = false;
135141
byte[] exifBytes = null;
136142
uint riffSize = 0;
137143
if (exifProfile != null)
138144
{
139145
isVp8X = true;
140-
riffSize += WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize;
146+
riffSize += ExtendedFileChunkSize;
141147
exifBytes = exifProfile.ToByteArray();
142-
riffSize += WebpConstants.ChunkHeaderSize + (uint)exifBytes.Length;
148+
riffSize += this.ExifChunkSize(exifBytes);
143149
}
144150

145151
this.Finish();
@@ -154,15 +160,15 @@ public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifPr
154160
// Write VP8X, header if necessary.
155161
if (isVp8X)
156162
{
157-
this.WriteVp8XHeader(stream, exifProfile, width, height);
163+
this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha);
158164
}
159165

160166
// Write magic bytes indicating its a lossless webp.
161167
stream.Write(WebpConstants.Vp8LMagicBytes);
162168

163169
// Write Vp8 Header.
164-
BinaryPrimitives.WriteUInt32LittleEndian(buffer, size);
165-
stream.Write(buffer);
170+
BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer, size);
171+
stream.Write(this.scratchBuffer.AsSpan(0, 4));
166172
stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte);
167173

168174
// Write the encoded bytes of the image to the stream.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
234234
this.EncodeStream(image);
235235

236236
// Write bytes from the bitwriter buffer to the stream.
237-
this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height);
237+
this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha);
238238
}
239239

240240
/// <summary>

src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
317317
this.bitWriter = new Vp8BitWriter(expectedSize, this);
318318

319319
// TODO: EncodeAlpha();
320+
bool hasAlpha = false;
321+
320322
// Stats-collection loop.
321323
this.StatLoop(width, height, yStride, uvStride);
322324
it.Init();
@@ -348,7 +350,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
348350

349351
// Write bytes from the bitwriter buffer to the stream.
350352
image.Metadata.SyncProfiles();
351-
this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height);
353+
this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha);
352354
}
353355

354356
/// <inheritdoc/>

src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
using System.IO;
55
using System.Threading;
66
using SixLabors.ImageSharp.Advanced;
7-
using SixLabors.ImageSharp.Formats.Bmp;
87
using SixLabors.ImageSharp.Formats.Webp.Lossless;
98
using SixLabors.ImageSharp.Formats.Webp.Lossy;
109
using SixLabors.ImageSharp.Memory;
11-
using SixLabors.ImageSharp.Metadata;
1210
using SixLabors.ImageSharp.PixelFormats;
1311

1412
namespace SixLabors.ImageSharp.Formats.Webp

tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,31 @@ public void IgnoreMetadata_ControlsWhetherIccpIsParsed<TPixel>(TestImageProvider
6363
}
6464
}
6565

66+
[Theory]
67+
[InlineData(WebpFileFormatType.Lossy)]
68+
[InlineData(WebpFileFormatType.Lossless)]
69+
public void Encode_WritesExifWithPadding(WebpFileFormatType fileFormatType)
70+
{
71+
// arrange
72+
using var input = new Image<Rgba32>(25, 25);
73+
using var memoryStream = new MemoryStream();
74+
var expectedExif = new ExifProfile();
75+
string expectedSoftware = "ImageSharp";
76+
expectedExif.SetValue(ExifTag.Software, expectedSoftware);
77+
input.Metadata.ExifProfile = expectedExif;
78+
79+
// act
80+
input.Save(memoryStream, new WebpEncoder() { FileFormat = fileFormatType });
81+
memoryStream.Position = 0;
82+
83+
// assert
84+
using var image = Image.Load<Rgba32>(memoryStream);
85+
ExifProfile actualExif = image.Metadata.ExifProfile;
86+
Assert.NotNull(actualExif);
87+
Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count);
88+
Assert.Equal(expectedSoftware, actualExif.GetValue(ExifTag.Software).Value);
89+
}
90+
6691
[Theory]
6792
[WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32)]
6893
public void EncodeLossyWebp_PreservesExif<TPixel>(TestImageProvider<TPixel> provider)

0 commit comments

Comments
 (0)