Skip to content

Commit ad6e16d

Browse files
Merge branch 'main' into js/colorspace-converter
2 parents 07e354d + 8546c74 commit ad6e16d

31 files changed

Lines changed: 292 additions & 73 deletions

File tree

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ jobs:
133133
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
134134

135135
- name: Export Failed Output
136-
uses: actions/upload-artifact@v3
136+
uses: actions/upload-artifact@v4
137137
if: failure()
138138
with:
139139
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip

.github/workflows/code-coverage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
7575

7676
- name: Export Failed Output
77-
uses: actions/upload-artifact@v3
77+
uses: actions/upload-artifact@v4
7878
if: failure()
7979
with:
8080
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip

src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ internal struct JpegBitReader
2222
// Whether there is no more good data to pull from the stream for the current mcu.
2323
private bool badData;
2424

25+
// How many times have we hit the eof.
26+
private int eofHitCount;
27+
2528
public JpegBitReader(BufferedReadStream stream)
2629
{
2730
this.stream = stream;
@@ -31,6 +34,7 @@ public JpegBitReader(BufferedReadStream stream)
3134
this.MarkerPosition = 0;
3235
this.badData = false;
3336
this.NoData = false;
37+
this.eofHitCount = 0;
3438
}
3539

3640
/// <summary>
@@ -219,11 +223,16 @@ private int ReadStream()
219223
// we know we have hit the EOI and completed decoding the scan buffer.
220224
if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length))
221225
{
222-
// We've encountered the end of the file stream which means there's no EOI marker
226+
// We've hit the end of the file stream more times than allowed which means there's no EOI marker
223227
// in the image or the SOS marker has the wrong dimensions set.
224-
this.badData = true;
225-
this.NoData = true;
226-
value = 0;
228+
if (this.eofHitCount > JpegConstants.Huffman.FetchLoop)
229+
{
230+
this.badData = true;
231+
this.NoData = true;
232+
value = 0;
233+
}
234+
235+
this.eofHitCount++;
227236
}
228237

229238
return value;

src/ImageSharp/Formats/Png/PngDecoderCore.cs

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

237-
previousFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
238-
this.InitializeFrame(previousFrameControl.Value, currentFrameControl.Value, image, previousFrame, out currentFrame);
237+
this.InitializeFrame(previousFrameControl, currentFrameControl.Value, image, previousFrame, out currentFrame);
239238

240239
this.currentStream.Position += 4;
241240
this.ReadScanlines(
@@ -246,11 +245,16 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
246245
currentFrameControl.Value,
247246
cancellationToken);
248247

249-
previousFrame = currentFrame;
250-
previousFrameControl = currentFrameControl;
248+
// if current frame dispose is restore to previous, then from future frame's perspective, it never happened
249+
if (currentFrameControl.Value.DisposeOperation != PngDisposalMethod.RestoreToPrevious)
250+
{
251+
previousFrame = currentFrame;
252+
previousFrameControl = currentFrameControl;
253+
}
254+
251255
break;
252256
case PngChunkType.Data:
253-
257+
pngMetadata.AnimateRootFrame = currentFrameControl != null;
254258
currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
255259
if (image is null)
256260
{
@@ -267,9 +271,12 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
267271
this.ReadNextDataChunk,
268272
currentFrameControl.Value,
269273
cancellationToken);
274+
if (pngMetadata.AnimateRootFrame)
275+
{
276+
previousFrame = currentFrame;
277+
previousFrameControl = currentFrameControl;
278+
}
270279

271-
previousFrame = currentFrame;
272-
previousFrameControl = currentFrameControl;
273280
break;
274281
case PngChunkType.Palette:
275282
this.palette = chunk.Data.GetSpan().ToArray();
@@ -638,7 +645,7 @@ private void InitializeImage<TPixel>(ImageMetadata metadata, FrameControl frameC
638645
/// <param name="previousFrame">The previous frame.</param>
639646
/// <param name="frame">The created frame</param>
640647
private void InitializeFrame<TPixel>(
641-
FrameControl previousFrameControl,
648+
FrameControl? previousFrameControl,
642649
FrameControl currentFrameControl,
643650
Image<TPixel> image,
644651
ImageFrame<TPixel>? previousFrame,
@@ -651,12 +658,16 @@ private void InitializeFrame<TPixel>(
651658
frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame);
652659

653660
// If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
654-
if (previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToBackground
655-
|| (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToPrevious))
661+
// So, if restoring to before first frame, clear entire area. Same if first frame (previousFrameControl null).
662+
if (previousFrameControl == null || (previousFrame is null && previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToPrevious))
663+
{
664+
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion();
665+
pixelRegion.Clear();
666+
}
667+
else if (previousFrameControl.Value.DisposeOperation == PngDisposalMethod.RestoreToBackground)
656668
{
657-
Rectangle restoreArea = previousFrameControl.Bounds;
658-
Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea);
659-
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
669+
Rectangle restoreArea = previousFrameControl.Value.Bounds;
670+
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(restoreArea);
660671
pixelRegion.Clear();
661672
}
662673

src/ImageSharp/Formats/Png/PngEncoderCore.cs

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

168168
ImageFrame<TPixel>? clonedFrame = null;
169169
ImageFrame<TPixel> currentFrame = image.Frames.RootFrame;
170+
int currentFrameIndex = 0;
170171

171172
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
172173
if (clearTransparency)
@@ -195,29 +196,50 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
195196

196197
if (image.Frames.Count > 1)
197198
{
198-
this.WriteAnimationControlChunk(stream, (uint)image.Frames.Count, pngMetadata.RepeatCount);
199+
this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount);
200+
}
201+
202+
// If the first frame isn't animated, write it as usual and skip it when writing animated frames
203+
if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1)
204+
{
205+
FrameControl frameControl = new((uint)this.width, (uint)this.height);
206+
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
207+
currentFrameIndex++;
208+
}
199209

200-
// Write the first frame.
210+
if (image.Frames.Count > 1)
211+
{
212+
// Write the first animated frame.
213+
currentFrame = image.Frames[currentFrameIndex];
201214
PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame);
202215
PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod;
203216
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
204-
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
217+
uint sequenceNumber = 1;
218+
if (pngMetadata.AnimateRootFrame)
219+
{
220+
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
221+
}
222+
else
223+
{
224+
sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
225+
}
226+
227+
currentFrameIndex++;
205228

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

209232
// Write following frames.
210-
uint increment = 0;
211233
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
212234

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

216-
for (int i = 1; i < image.Frames.Count; i++)
238+
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
217239
{
218240
ImageFrame<TPixel>? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame;
219-
currentFrame = image.Frames[i];
220-
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
241+
currentFrame = image.Frames[currentFrameIndex];
242+
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
221243

222244
frameMetadata = GetPngFrameMetadata(currentFrame);
223245
bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over;
@@ -238,22 +260,17 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
238260
}
239261

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

243265
// Dispose of previous quantized frame and reassign.
244266
quantized?.Dispose();
245267
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
246-
increment += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true);
268+
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
247269

248270
previousFrame = currentFrame;
249271
previousDisposal = frameMetadata.DisposalMethod;
250272
}
251273
}
252-
else
253-
{
254-
FrameControl frameControl = new((uint)this.width, (uint)this.height);
255-
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
256-
}
257274

258275
this.WriteEndChunk(stream);
259276

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
@@ -180,8 +180,9 @@ public static void ProcessInterlacedPaletteScanline<TPixel>(
180180
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
181181
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
182182
ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span);
183+
uint offset = pixelOffset + frameControl.XOffset;
183184

184-
for (nuint x = pixelOffset, o = 0; x < frameControl.XMax; x += increment, o++)
185+
for (nuint x = offset, o = 0; x < frameControl.XMax; x += increment, o++)
185186
{
186187
uint index = Unsafe.Add(ref scanlineSpanRef, o);
187188
Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToPixel<Rgba32>());

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
@@ -3,7 +3,7 @@
33

44
namespace SixLabors.ImageSharp.Formats.Webp.Chunks;
55

6-
internal readonly struct WebpVp8X
6+
internal readonly struct WebpVp8X : IEquatable<WebpVp8X>
77
{
88
public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height)
99
{
@@ -51,6 +51,24 @@ public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, boo
5151
/// </summary>
5252
public uint Height { get; }
5353

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

86+
public WebpVp8X WithAlpha(bool hasAlpha)
87+
=> new(this.HasAnimation, this.HasXmp, this.HasExif, hasAlpha, this.HasIcc, this.Width, this.Height);
88+
6889
public void WriteTo(Stream stream)
6990
{
7091
byte flags = 0;

0 commit comments

Comments
 (0)