Skip to content

Commit ef8c79d

Browse files
Fix WEBP animation disposal and blending
1 parent a888544 commit ef8c79d

File tree

7 files changed

+128
-77
lines changed

7 files changed

+128
-77
lines changed

src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ protected BitReaderBase(Stream inputStream, int imageDataSize, MemoryAllocator m
3232
/// <param name="memoryAllocator">Used for allocating memory during reading data from the stream.</param>
3333
protected static IMemoryOwner<byte> ReadImageDataFromStream(Stream input, int bytesToRead, MemoryAllocator memoryAllocator)
3434
{
35-
IMemoryOwner<byte> data = memoryAllocator.Allocate<byte>(bytesToRead);
35+
IMemoryOwner<byte> data = memoryAllocator.Allocate<byte>(bytesToRead, AllocationOptions.Clean);
3636
Span<byte> dataSpan = data.Memory.Span;
3737
input.Read(dataSpan[..bytesToRead], 0, bytesToRead);
3838

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ public Vp8Decoder(Vp8FrameHeader frameHeader, Vp8PictureHeader pictureHeader, Vp
6767
int extraY = extraRows * this.CacheYStride;
6868
int extraUv = extraRows / 2 * this.CacheUvStride;
6969
this.YuvBuffer = memoryAllocator.Allocate<byte>((WebpConstants.Bps * 17) + (WebpConstants.Bps * 9) + extraY);
70-
this.CacheY = memoryAllocator.Allocate<byte>((16 * this.CacheYStride) + extraY);
70+
this.CacheY = memoryAllocator.Allocate<byte>((16 * this.CacheYStride) + extraY, AllocationOptions.Clean);
7171
int cacheUvSize = (16 * this.CacheUvStride) + extraUv;
72-
this.CacheU = memoryAllocator.Allocate<byte>(cacheUvSize);
73-
this.CacheV = memoryAllocator.Allocate<byte>(cacheUvSize);
74-
this.TmpYBuffer = memoryAllocator.Allocate<byte>((int)width);
75-
this.TmpUBuffer = memoryAllocator.Allocate<byte>((int)width);
76-
this.TmpVBuffer = memoryAllocator.Allocate<byte>((int)width);
77-
this.Pixels = memoryAllocator.Allocate<byte>((int)(width * height * 4));
72+
this.CacheU = memoryAllocator.Allocate<byte>(cacheUvSize, AllocationOptions.Clean);
73+
this.CacheV = memoryAllocator.Allocate<byte>(cacheUvSize, AllocationOptions.Clean);
74+
this.TmpYBuffer = memoryAllocator.Allocate<byte>((int)width, AllocationOptions.Clean);
75+
this.TmpUBuffer = memoryAllocator.Allocate<byte>((int)width, AllocationOptions.Clean);
76+
this.TmpVBuffer = memoryAllocator.Allocate<byte>((int)width, AllocationOptions.Clean);
77+
this.Pixels = memoryAllocator.Allocate<byte>((int)(width * height * 4), AllocationOptions.Clean);
7878

7979
#if DEBUG
8080
// Filling those buffers with 205, is only useful for debugging,

src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,29 @@ public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration confi
8181
/// <param name="width">The width of the image.</param>
8282
/// <param name="height">The height of the image.</param>
8383
/// <param name="completeDataSize">The size of the image data in bytes.</param>
84-
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, WebpFeatures features, uint width, uint height, uint completeDataSize)
84+
public Image<TPixel> Decode<TPixel>(
85+
BufferedReadStream stream,
86+
WebpFeatures features,
87+
uint width,
88+
uint height,
89+
uint completeDataSize)
8590
where TPixel : unmanaged, IPixel<TPixel>
8691
{
8792
Image<TPixel>? image = null;
8893
ImageFrame<TPixel>? previousFrame = null;
94+
WebpFrameData? prevFrameData = null;
8995

9096
this.metadata = new ImageMetadata();
9197
this.webpMetadata = this.metadata.GetWebpMetadata();
9298
this.webpMetadata.RepeatCount = features.AnimationLoopCount;
9399

100+
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
101+
? Color.Transparent
102+
: features.AnimationBackgroundColor!.Value;
103+
104+
this.webpMetadata.BackgroundColor = backgroundColor;
105+
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
106+
94107
Span<byte> buffer = stackalloc byte[4];
95108
uint frameCount = 0;
96109
int remainingBytes = (int)completeDataSize;
@@ -101,10 +114,15 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, WebpFeatures feat
101114
switch (chunkType)
102115
{
103116
case WebpChunkType.FrameData:
104-
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
105-
? new Color(new Bgra32(0, 0, 0, 0))
106-
: features.AnimationBackgroundColor!.Value;
107-
uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, backgroundColor);
117+
uint dataSize = this.ReadFrame(
118+
stream,
119+
ref image,
120+
ref previousFrame,
121+
ref prevFrameData,
122+
width,
123+
height,
124+
backgroundPixel);
125+
108126
remainingBytes -= (int)dataSize;
109127
break;
110128
case WebpChunkType.Xmp:
@@ -132,10 +150,18 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, WebpFeatures feat
132150
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
133151
/// <param name="image">The image to decode the information to.</param>
134152
/// <param name="previousFrame">The previous frame.</param>
153+
/// <param name="prevFrameData">The previous frame data.</param>
135154
/// <param name="width">The width of the image.</param>
136155
/// <param name="height">The height of the image.</param>
137156
/// <param name="backgroundColor">The default background color of the canvas in.</param>
138-
private uint ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame, uint width, uint height, Color backgroundColor)
157+
private uint ReadFrame<TPixel>(
158+
BufferedReadStream stream,
159+
ref Image<TPixel>? image,
160+
ref ImageFrame<TPixel>? previousFrame,
161+
ref WebpFrameData? prevFrameData,
162+
uint width,
163+
uint height,
164+
TPixel backgroundColor)
139165
where TPixel : unmanaged, IPixel<TPixel>
140166
{
141167
WebpFrameData frameData = WebpFrameData.Parse(stream);
@@ -174,40 +200,54 @@ private uint ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? ima
174200
break;
175201
}
176202

177-
ImageFrame<TPixel>? currentFrame = null;
178-
ImageFrame<TPixel> imageFrame;
203+
bool isKeyFrame = false;
204+
ImageFrame<TPixel> currentFrame;
179205
if (previousFrame is null)
180206
{
181-
image = new Image<TPixel>(this.configuration, (int)width, (int)height, backgroundColor.ToPixel<TPixel>(), this.metadata);
207+
image = new Image<TPixel>(this.configuration, (int)width, (int)height, backgroundColor, this.metadata);
182208

183209
SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData);
184210

185-
imageFrame = image.Frames.RootFrame;
211+
currentFrame = image.Frames.RootFrame;
212+
isKeyFrame = true;
186213
}
187214
else
188215
{
189-
currentFrame = image!.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection.
216+
// If the frame is a key frame we do not need to clone the frame or clear it.
217+
isKeyFrame = prevFrameData?.DisposalMethod is WebpDisposalMethod.RestoreToBackground
218+
&& this.restoreArea == image!.Bounds;
190219

191-
SetFrameMetadata(currentFrame.Metadata, frameData);
220+
if (isKeyFrame)
221+
{
222+
currentFrame = image!.Frames.CreateFrame(backgroundColor);
223+
}
224+
else
225+
{
226+
// This clones the frame and adds it the collection.
227+
currentFrame = image!.Frames.AddFrame(previousFrame);
228+
if (prevFrameData?.DisposalMethod is WebpDisposalMethod.RestoreToBackground)
229+
{
230+
this.RestoreToBackground(currentFrame, backgroundColor);
231+
}
232+
}
192233

193-
imageFrame = currentFrame;
234+
SetFrameMetadata(currentFrame.Metadata, frameData);
194235
}
195236

196-
Rectangle regionRectangle = frameData.Bounds;
237+
Rectangle interest = frameData.Bounds;
238+
bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendMethod.Over;
239+
using Buffer2D<TPixel> pixelData = this.DecodeImageFrameData<TPixel>(frameData, webpInfo);
240+
DrawDecodedImageFrameOnCanvas(pixelData, currentFrame, interest, blend);
241+
242+
webpInfo?.Dispose();
243+
previousFrame = currentFrame;
244+
prevFrameData = frameData;
197245

198246
if (frameData.DisposalMethod is WebpDisposalMethod.RestoreToBackground)
199247
{
200-
this.RestoreToBackground(imageFrame, backgroundColor);
248+
this.restoreArea = interest;
201249
}
202250

203-
using Buffer2D<TPixel> decodedImageFrame = this.DecodeImageFrameData<TPixel>(frameData, webpInfo);
204-
205-
bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendMethod.Over;
206-
DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend);
207-
208-
previousFrame = currentFrame ?? image.Frames.RootFrame;
209-
this.restoreArea = regionRectangle;
210-
211251
return (uint)(stream.Position - streamStartPosition);
212252
}
213253

@@ -257,31 +297,27 @@ private Buffer2D<TPixel> DecodeImageFrameData<TPixel>(WebpFrameData frameData, W
257297

258298
try
259299
{
260-
Buffer2D<TPixel> pixelBufferDecoded = decodedFrame.PixelBuffer;
300+
Buffer2D<TPixel> decodeBuffer = decodedFrame.PixelBuffer;
261301
if (webpInfo.IsLossless)
262302
{
263303
WebpLosslessDecoder losslessDecoder =
264304
new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
265-
losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height);
305+
losslessDecoder.Decode(decodeBuffer, (int)frameData.Width, (int)frameData.Height);
266306
}
267307
else
268308
{
269309
WebpLossyDecoder lossyDecoder =
270310
new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
271-
lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData);
311+
lossyDecoder.Decode(decodeBuffer, (int)frameData.Width, (int)frameData.Height, webpInfo, this.alphaData);
272312
}
273313

274-
return pixelBufferDecoded;
314+
return decodeBuffer;
275315
}
276316
catch
277317
{
278318
decodedFrame?.Dispose();
279319
throw;
280320
}
281-
finally
282-
{
283-
webpInfo.Dispose();
284-
}
285321
}
286322

287323
/// <summary>
@@ -290,17 +326,17 @@ private Buffer2D<TPixel> DecodeImageFrameData<TPixel>(WebpFrameData frameData, W
290326
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
291327
/// <param name="decodedImageFrame">The decoded image.</param>
292328
/// <param name="imageFrame">The image frame to draw into.</param>
293-
/// <param name="restoreArea">The area of the frame.</param>
329+
/// <param name="interest">The area of the frame to draw to.</param>
294330
/// <param name="blend">Whether to blend the decoded frame data onto the target frame.</param>
295331
private static void DrawDecodedImageFrameOnCanvas<TPixel>(
296332
Buffer2D<TPixel> decodedImageFrame,
297333
ImageFrame<TPixel> imageFrame,
298-
Rectangle restoreArea,
334+
Rectangle interest,
299335
bool blend)
300336
where TPixel : unmanaged, IPixel<TPixel>
301337
{
302338
// Trim the destination frame to match the restore area. The source frame is already trimmed.
303-
Buffer2DRegion<TPixel> imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea);
339+
Buffer2DRegion<TPixel> imageFramePixels = imageFrame.PixelBuffer.GetRegion(interest);
304340
if (blend)
305341
{
306342
// The destination frame has already been prepopulated with the pixel data from the previous frame
@@ -309,21 +345,21 @@ private static void DrawDecodedImageFrameOnCanvas<TPixel>(
309345
PixelBlender<TPixel> blender =
310346
PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
311347

312-
for (int y = 0; y < restoreArea.Height; y++)
348+
for (int y = 0; y < interest.Height; y++)
313349
{
314350
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
315-
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
351+
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y);
316352

317353
blender.Blend<TPixel>(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f);
318354
}
319355

320356
return;
321357
}
322358

323-
for (int y = 0; y < restoreArea.Height; y++)
359+
for (int y = 0; y < interest.Height; y++)
324360
{
325361
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
326-
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
362+
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y);
327363
decodedPixelRow.CopyTo(framePixelRow);
328364
}
329365
}
@@ -335,7 +371,7 @@ private static void DrawDecodedImageFrameOnCanvas<TPixel>(
335371
/// <typeparam name="TPixel">The pixel format.</typeparam>
336372
/// <param name="imageFrame">The image frame.</param>
337373
/// <param name="backgroundColor">Color of the background.</param>
338-
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> imageFrame, Color backgroundColor)
374+
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> imageFrame, TPixel backgroundColor)
339375
where TPixel : unmanaged, IPixel<TPixel>
340376
{
341377
if (!this.restoreArea.HasValue)
@@ -345,8 +381,7 @@ private void RestoreToBackground<TPixel>(ImageFrame<TPixel> imageFrame, Color ba
345381

346382
Rectangle interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value);
347383
Buffer2DRegion<TPixel> pixelRegion = imageFrame.PixelBuffer.GetRegion(interest);
348-
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
349-
pixelRegion.Fill(backgroundPixel);
384+
pixelRegion.Fill(backgroundColor);
350385
}
351386

352387
/// <inheritdoc/>

src/ImageSharp/Formats/Webp/WebpBlendMethod.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.Formats.Webp;
@@ -12,11 +12,11 @@ public enum WebpBlendMethod
1212
/// Do not blend. After disposing of the previous frame,
1313
/// render the current frame on the canvas by overwriting the rectangle covered by the current frame.
1414
/// </summary>
15-
Source = 0,
15+
Source = 1,
1616

1717
/// <summary>
1818
/// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending.
1919
/// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle.
2020
/// </summary>
21-
Over = 1,
21+
Over = 0,
2222
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,22 @@ public void WebpDecoder_CanDecode_Issue2670<TPixel>(TestImageProvider<TPixel> pr
450450
image.CompareToOriginal(provider, ReferenceDecoder);
451451
}
452452

453+
// https://github.com/SixLabors/ImageSharp/issues/2866
454+
[Theory]
455+
[WithFile(Lossy.Issue2866, PixelTypes.Rgba32)]
456+
public void WebpDecoder_CanDecode_Issue2866<TPixel>(TestImageProvider<TPixel> provider)
457+
where TPixel : unmanaged, IPixel<TPixel>
458+
{
459+
// Web
460+
using Image<TPixel> image = provider.GetImage(
461+
WebpDecoder.Instance,
462+
new WebpDecoderOptions() { BackgroundColorHandling = BackgroundColorHandling.Ignore });
463+
464+
// We can't use the reference decoder here.
465+
// It creates frames of different size without blending the frames.
466+
image.DebugSave(provider, extension: "webp", encoder: new WebpEncoder());
467+
}
468+
453469
[Theory]
454470
[WithFile(Lossless.LossLessCorruptImage3, PixelTypes.Rgba32)]
455471
public void WebpDecoder_ThrowImageFormatException_OnInvalidImages<TPixel>(TestImageProvider<TPixel> provider)

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

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -582,21 +582,6 @@ public async Task CanSave_NonSeekableStream_Async<TPixel>(TestImageProvider<TPix
582582
Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray()));
583583
}
584584

585-
// https://github.com/SixLabors/ImageSharp/issues/2866
586-
[Theory]
587-
[WithFile(Lossy.Issue2866, PixelTypes.Rgba32)]
588-
public void WebpDecoder_CanDecode_Issue2866<TPixel>(TestImageProvider<TPixel> provider)
589-
where TPixel : unmanaged, IPixel<TPixel>
590-
{
591-
WebpEncoder encoder = new()
592-
{
593-
Quality = 100
594-
};
595-
596-
using Image<TPixel> image = provider.GetImage();
597-
image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.TolerantPercentage(0.0994F));
598-
}
599-
600585
private static ImageComparer GetComparer(int quality)
601586
{
602587
float tolerance = 0.01f; // ~1.0%

0 commit comments

Comments
 (0)