Skip to content

Commit eec9718

Browse files
Merge branch 'release/3.1.x' into af/memlimit-01
2 parents 63d4b20 + 8e6532a commit eec9718

File tree

26 files changed

+262
-67
lines changed

26 files changed

+262
-67
lines changed

src/ImageSharp/Common/Helpers/RiffHelper.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Buffers.Binary;
55
using System.Text;
6+
using SixLabors.ImageSharp.Formats.Webp.Chunks;
67

78
namespace SixLabors.ImageSharp.Common.Helpers;
89

@@ -107,6 +108,7 @@ public static void EndWriteChunk(Stream stream, long sizePosition)
107108
position++;
108109
}
109110

111+
// Add the size of the encoded file to the Riff header.
110112
BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize);
111113
stream.Position = sizePosition;
112114
stream.Write(buffer);
@@ -120,5 +122,18 @@ public static long BeginWriteRiffFile(Stream stream, string formType)
120122
return sizePosition;
121123
}
122124

123-
public static void EndWriteRiffFile(Stream stream, long sizePosition) => EndWriteChunk(stream, sizePosition);
125+
public static void EndWriteRiffFile(Stream stream, in WebpVp8X vp8x, bool updateVp8x, long sizePosition)
126+
{
127+
EndWriteChunk(stream, sizePosition + 4);
128+
129+
// Write the VP8X chunk if necessary.
130+
if (updateVp8x)
131+
{
132+
long position = stream.Position;
133+
134+
stream.Position = sizePosition + 12;
135+
vp8x.WriteTo(stream);
136+
stream.Position = position;
137+
}
138+
}
124139
}

src/ImageSharp/Formats/Png/PngDecoderCore.cs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,7 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
228228
PngThrowHelper.ThrowMissingFrameControl();
229229
}
230230

231-
previousFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
232-
this.InitializeFrame(previousFrameControl.Value, currentFrameControl.Value, image, previousFrame, out currentFrame);
231+
this.InitializeFrame(previousFrameControl, currentFrameControl.Value, image, previousFrame, out currentFrame);
233232

234233
this.currentStream.Position += 4;
235234
this.ReadScanlines(
@@ -240,11 +239,16 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
240239
currentFrameControl.Value,
241240
cancellationToken);
242241

243-
previousFrame = currentFrame;
244-
previousFrameControl = currentFrameControl;
242+
// if current frame dispose is restore to previous, then from future frame's perspective, it never happened
243+
if (currentFrameControl.Value.DisposeOperation != PngDisposalMethod.RestoreToPrevious)
244+
{
245+
previousFrame = currentFrame;
246+
previousFrameControl = currentFrameControl;
247+
}
248+
245249
break;
246250
case PngChunkType.Data:
247-
251+
pngMetadata.AnimateRootFrame = currentFrameControl != null;
248252
currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
249253
if (image is null)
250254
{
@@ -261,9 +265,12 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
261265
this.ReadNextDataChunk,
262266
currentFrameControl.Value,
263267
cancellationToken);
268+
if (pngMetadata.AnimateRootFrame)
269+
{
270+
previousFrame = currentFrame;
271+
previousFrameControl = currentFrameControl;
272+
}
264273

265-
previousFrame = currentFrame;
266-
previousFrameControl = currentFrameControl;
267274
break;
268275
case PngChunkType.Palette:
269276
this.palette = chunk.Data.GetSpan().ToArray();
@@ -632,7 +639,7 @@ private void InitializeImage<TPixel>(ImageMetadata metadata, FrameControl frameC
632639
/// <param name="previousFrame">The previous frame.</param>
633640
/// <param name="frame">The created frame</param>
634641
private void InitializeFrame<TPixel>(
635-
FrameControl previousFrameControl,
642+
FrameControl? previousFrameControl,
636643
FrameControl currentFrameControl,
637644
Image<TPixel> image,
638645
ImageFrame<TPixel>? previousFrame,
@@ -645,12 +652,16 @@ private void InitializeFrame<TPixel>(
645652
frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame);
646653

647654
// If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
648-
if (previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToBackground
649-
|| (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToPrevious))
655+
// So, if restoring to before first frame, clear entire area. Same if first frame (previousFrameControl null).
656+
if (previousFrameControl == null || (previousFrame is null && previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToPrevious))
657+
{
658+
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion();
659+
pixelRegion.Clear();
660+
}
661+
else if (previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToBackground)
650662
{
651-
Rectangle restoreArea = previousFrameControl.Bounds;
652-
Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea);
653-
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
663+
Rectangle restoreArea = previousFrameControl.Value.Bounds;
664+
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(restoreArea);
654665
pixelRegion.Clear();
655666
}
656667

src/ImageSharp/Formats/Png/PngEncoderCore.cs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
161161

162162
ImageFrame<TPixel>? clonedFrame = null;
163163
ImageFrame<TPixel> currentFrame = image.Frames.RootFrame;
164+
int currentFrameIndex = 0;
164165

165166
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
166167
if (clearTransparency)
@@ -189,29 +190,50 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
189190

190191
if (image.Frames.Count > 1)
191192
{
192-
this.WriteAnimationControlChunk(stream, (uint)image.Frames.Count, pngMetadata.RepeatCount);
193+
this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount);
194+
}
195+
196+
// If the first frame isn't animated, write it as usual and skip it when writing animated frames
197+
if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1)
198+
{
199+
FrameControl frameControl = new((uint)this.width, (uint)this.height);
200+
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
201+
currentFrameIndex++;
202+
}
193203

194-
// Write the first frame.
204+
if (image.Frames.Count > 1)
205+
{
206+
// Write the first animated frame.
207+
currentFrame = image.Frames[currentFrameIndex];
195208
PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame);
196209
PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod;
197210
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
198-
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
211+
uint sequenceNumber = 1;
212+
if (pngMetadata.AnimateRootFrame)
213+
{
214+
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
215+
}
216+
else
217+
{
218+
sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
219+
}
220+
221+
currentFrameIndex++;
199222

200223
// Capture the global palette for reuse on subsequent frames.
201224
ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
202225

203226
// Write following frames.
204-
uint increment = 0;
205227
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
206228

207229
// This frame is reused to store de-duplicated pixel buffers.
208230
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size());
209231

210-
for (int i = 1; i < image.Frames.Count; i++)
232+
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
211233
{
212234
ImageFrame<TPixel>? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame;
213-
currentFrame = image.Frames[i];
214-
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
235+
currentFrame = image.Frames[currentFrameIndex];
236+
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
215237

216238
frameMetadata = GetPngFrameMetadata(currentFrame);
217239
bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over;
@@ -232,22 +254,17 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
232254
}
233255

234256
// Each frame control sequence number must be incremented by the number of frame data chunks that follow.
235-
frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, (uint)i + increment);
257+
frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber);
236258

237259
// Dispose of previous quantized frame and reassign.
238260
quantized?.Dispose();
239261
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
240-
increment += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true);
262+
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
241263

242264
previousFrame = currentFrame;
243265
previousDisposal = frameMetadata.DisposalMethod;
244266
}
245267
}
246-
else
247-
{
248-
FrameControl frameControl = new((uint)this.width, (uint)this.height);
249-
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
250-
}
251268

252269
this.WriteEndChunk(stream);
253270

src/ImageSharp/Formats/Png/PngMetadata.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ private PngMetadata(PngMetadata other)
2929
this.InterlaceMethod = other.InterlaceMethod;
3030
this.TransparentColor = other.TransparentColor;
3131
this.RepeatCount = other.RepeatCount;
32+
this.AnimateRootFrame = other.AnimateRootFrame;
3233

3334
if (other.ColorTable?.Length > 0)
3435
{
@@ -83,6 +84,11 @@ private PngMetadata(PngMetadata other)
8384
/// </summary>
8485
public uint RepeatCount { get; set; } = 1;
8586

87+
/// <summary>
88+
/// Gets or sets a value indicating whether the root frame is shown as part of the animated sequence
89+
/// </summary>
90+
public bool AnimateRootFrame { get; set; } = true;
91+
8692
/// <inheritdoc/>
8793
public IDeepCloneable DeepClone() => new PngMetadata(this);
8894

src/ImageSharp/Formats/Png/PngScanlineProcessor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,9 @@ public static void ProcessInterlacedPaletteScanline<TPixel>(
198198
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
199199
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
200200
ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
201+
uint offset = pixelOffset + frameControl.XOffset;
201202

202-
for (nuint x = pixelOffset, o = 0; x < frameControl.XMax; x += increment, o++)
203+
for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++)
203204
{
204205
uint index = Unsafe.Add(ref scanlineSpanRef, o);
205206
pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32());

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired)
8888
/// <param name="iccProfile">The color profile.</param>
8989
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
9090
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
91-
public static void WriteTrunksBeforeData(
91+
/// <returns>A <see cref="WebpVp8X"/> or a default instance.</returns>
92+
public static WebpVp8X WriteTrunksBeforeData(
9293
Stream stream,
9394
uint width,
9495
uint height,
@@ -102,16 +103,19 @@ public static void WriteTrunksBeforeData(
102103
RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc);
103104

104105
// Write VP8X, header if necessary.
106+
WebpVp8X vp8x = default;
105107
bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation;
106108
if (isVp8X)
107109
{
108-
WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation);
110+
vp8x = WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation);
109111

110112
if (iccProfile != null)
111113
{
112114
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray());
113115
}
114116
}
117+
118+
return vp8x;
115119
}
116120

117121
/// <summary>
@@ -124,10 +128,16 @@ public static void WriteTrunksBeforeData(
124128
/// Write the trunks after data trunk.
125129
/// </summary>
126130
/// <param name="stream">The stream to write to.</param>
127-
/// <param name="exifProfile">The exif profile.</param>
131+
/// <param name="vp8x">The VP8X chunk.</param>
132+
/// <param name="updateVp8x">Whether to update the chunk.</param>
133+
/// <param name="initialPosition">The initial position of the stream before encoding.</param>
134+
/// <param name="exifProfile">The EXIF profile.</param>
128135
/// <param name="xmpProfile">The XMP profile.</param>
129136
public static void WriteTrunksAfterData(
130137
Stream stream,
138+
in WebpVp8X vp8x,
139+
bool updateVp8x,
140+
long initialPosition,
131141
ExifProfile? exifProfile,
132142
XmpProfile? xmpProfile)
133143
{
@@ -141,7 +151,7 @@ public static void WriteTrunksAfterData(
141151
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data);
142152
}
143153

144-
RiffHelper.EndWriteRiffFile(stream, 4);
154+
RiffHelper.EndWriteRiffFile(stream, in vp8x, updateVp8x, initialPosition);
145155
}
146156

147157
/// <summary>
@@ -186,19 +196,21 @@ public static void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alp
186196
/// Writes a VP8X header to the stream.
187197
/// </summary>
188198
/// <param name="stream">The stream to write to.</param>
189-
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
190-
/// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param>
199+
/// <param name="exifProfile">An EXIF profile or null, if it does not exist.</param>
200+
/// <param name="xmpProfile">An XMP profile or null, if it does not exist.</param>
191201
/// <param name="iccProfile">The color profile.</param>
192202
/// <param name="width">The width of the image.</param>
193203
/// <param name="height">The height of the image.</param>
194204
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
195205
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
196-
protected static void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation)
206+
protected static WebpVp8X WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation)
197207
{
198208
WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height);
199209

200210
chunk.Validate(MaxDimension, MaxCanvasPixels);
201211

202212
chunk.WriteTo(stream);
213+
214+
return chunk;
203215
}
204216
}

src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace SixLabors.ImageSharp.Formats.Webp.Chunks;
77

8-
internal readonly struct WebpVp8X
8+
internal readonly struct WebpVp8X : IEquatable<WebpVp8X>
99
{
1010
public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height)
1111
{
@@ -53,6 +53,24 @@ public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, boo
5353
/// </summary>
5454
public uint Height { get; }
5555

56+
public static bool operator ==(WebpVp8X left, WebpVp8X right) => left.Equals(right);
57+
58+
public static bool operator !=(WebpVp8X left, WebpVp8X right) => !(left == right);
59+
60+
public override bool Equals(object? obj) => obj is WebpVp8X x && this.Equals(x);
61+
62+
public bool Equals(WebpVp8X other)
63+
=> this.HasAnimation == other.HasAnimation
64+
&& this.HasXmp == other.HasXmp
65+
&& this.HasExif == other.HasExif
66+
&& this.HasAlpha == other.HasAlpha
67+
&& this.HasIcc == other.HasIcc
68+
&& this.Width == other.Width
69+
&& this.Height == other.Height;
70+
71+
public override int GetHashCode()
72+
=> HashCode.Combine(this.HasAnimation, this.HasXmp, this.HasExif, this.HasAlpha, this.HasIcc, this.Width, this.Height);
73+
5674
public void Validate(uint maxDimension, ulong maxCanvasPixels)
5775
{
5876
if (this.Width > maxDimension || this.Height > maxDimension)
@@ -67,6 +85,9 @@ public void Validate(uint maxDimension, ulong maxCanvasPixels)
6785
}
6886
}
6987

88+
public WebpVp8X WithAlpha(bool hasAlpha)
89+
=> new(this.HasAnimation, this.HasXmp, this.HasExif, hasAlpha, this.HasIcc, this.Width, this.Height);
90+
7091
public void WriteTo(Stream stream)
7192
{
7293
byte flags = 0;

0 commit comments

Comments
 (0)