Skip to content

Commit a486558

Browse files
Complete Webp and add tests
1 parent 328e046 commit a486558

21 files changed

Lines changed: 515 additions & 40 deletions

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,19 +190,20 @@ private static GifMetadata GetGifMetadata<TPixel>(Image<TPixel> image)
190190
return GifMetadata.FromAnimatedMetadata(ani);
191191
}
192192

193+
// Return explicit new instance so we do not mutate the original metadata.
193194
return new();
194195
}
195196

196197
private static GifFrameMetadata? GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex)
197198
where TPixel : unmanaged, IPixel<TPixel>
198199
{
199-
if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata? gif))
200+
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
200201
{
201202
return gif;
202203
}
203204

204205
GifFrameMetadata? metadata = null;
205-
if (frame.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? png))
206+
if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
206207
{
207208
AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata();
208209
metadata = GifFrameMetadata.FromAnimatedMetadata(ani);
@@ -342,7 +343,20 @@ private void EncodeAdditionalFrame<TPixel>(
342343
}
343344
}
344345

345-
this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
346+
// We can't deduplicate here as we need the background pixels to be present in the buffer.
347+
if (metadata?.DisposalMethod == GifDisposalMethod.RestoreToBackground)
348+
{
349+
for (int y = 0; y < currentFrame.PixelBuffer.Height; y++)
350+
{
351+
Span<TPixel> sourceRow = currentFrame.PixelBuffer.DangerousGetRowSpan(y);
352+
Span<TPixel> destinationRow = encodingFrame.PixelBuffer.DangerousGetRowSpan(y);
353+
sourceRow.CopyTo(destinationRow);
354+
}
355+
}
356+
else
357+
{
358+
this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
359+
}
346360

347361
IndexedImageFrame<TPixel> quantized;
348362
if (useLocal)

src/ImageSharp/Formats/Gif/MetadataExtensions.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,25 @@ public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source)
5656
/// <returns>
5757
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
5858
/// </returns>
59-
public static bool TryGetGifFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
59+
public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
6060
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
6161

6262
internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source)
63-
=> new()
63+
{
64+
Color background = Color.Transparent;
65+
if (source.GlobalColorTable != null)
66+
{
67+
background = source.GlobalColorTable.Value.Span[source.BackgroundColorIndex];
68+
}
69+
70+
return new()
6471
{
6572
ColorTable = source.GlobalColorTable,
6673
ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
6774
RepeatCount = source.RepeatCount,
75+
BackgroundColor = background,
6876
};
77+
}
6978

7079
internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source)
7180
=> new()

src/ImageSharp/Formats/Png/MetadataExtensions.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public static bool TryGetPngMetadata(this ImageMetadata source, [NotNullWhen(tru
3636
/// </summary>
3737
/// <param name="source">The metadata this method extends.</param>
3838
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
39-
public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
39+
public static PngFrameMetadata GetPngMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
4040

4141
/// <summary>
4242
/// Gets the png format specific metadata for the image frame.
@@ -46,7 +46,7 @@ public static bool TryGetPngMetadata(this ImageMetadata source, [NotNullWhen(tru
4646
/// <returns>
4747
/// <see langword="true"/> if the png frame metadata exists; otherwise, <see langword="false"/>.
4848
/// </returns>
49-
public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata)
49+
public static bool TryGetPngMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata)
5050
=> source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
5151

5252
internal static AnimatedImageMetadata ToAnimatedImageMetadata(this PngMetadata source)
@@ -58,17 +58,25 @@ internal static AnimatedImageMetadata ToAnimatedImageMetadata(this PngMetadata s
5858
};
5959

6060
internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this PngFrameMetadata source)
61-
=> new()
61+
{
62+
double delay = source.FrameDelay.ToDouble();
63+
if (double.IsNaN(delay))
64+
{
65+
delay = 0;
66+
}
67+
68+
return new()
6269
{
6370
ColorTableMode = FrameColorTableMode.Global,
64-
Duration = TimeSpan.FromMilliseconds(source.FrameDelay.ToDouble() * 1000),
71+
Duration = TimeSpan.FromMilliseconds(delay * 1000),
6572
DisposalMode = GetMode(source.DisposalMethod),
6673
BlendMode = source.BlendMethod == PngBlendMethod.Source ? FrameBlendMode.Source : FrameBlendMode.Over,
6774
};
75+
}
6876

6977
private static FrameDisposalMode GetMode(PngDisposalMethod method) => method switch
7078
{
71-
PngDisposalMethod.None => FrameDisposalMode.DoNotDispose,
79+
PngDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose,
7280
PngDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
7381
PngDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
7482
_ => FrameDisposalMode.Unspecified,

src/ImageSharp/Formats/Png/PngDecoderCore.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ private void InitializeImage<TPixel>(ImageMetadata metadata, FrameControl frameC
581581
this.header.Height,
582582
metadata);
583583

584-
PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata();
584+
PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngMetadata();
585585
frameMetadata.FromChunk(in frameControl);
586586

587587
this.bytesPerPixel = this.CalculateBytesPerPixel();
@@ -630,7 +630,7 @@ private void InitializeFrame<TPixel>(
630630
pixelRegion.Clear();
631631
}
632632

633-
PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata();
633+
PngFrameMetadata frameMetadata = frame.Metadata.GetPngMetadata();
634634
frameMetadata.FromChunk(currentFrameControl);
635635

636636
this.previousScanline?.Dispose();

src/ImageSharp/Formats/Png/PngDisposalMethod.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public enum PngDisposalMethod
1111
/// <summary>
1212
/// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
1313
/// </summary>
14-
None,
14+
DoNotDispose,
1515

1616
/// <summary>
1717
/// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.

src/ImageSharp/Formats/Png/PngEncoderCore.cs

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
using System.Runtime.InteropServices;
88
using SixLabors.ImageSharp.Common.Helpers;
99
using SixLabors.ImageSharp.Compression.Zlib;
10+
using SixLabors.ImageSharp.Formats.Gif;
1011
using SixLabors.ImageSharp.Formats.Png.Chunks;
1112
using SixLabors.ImageSharp.Formats.Png.Filters;
13+
using SixLabors.ImageSharp.Formats.Webp;
1214
using SixLabors.ImageSharp.Memory;
1315
using SixLabors.ImageSharp.Metadata;
1416
using SixLabors.ImageSharp.PixelFormats;
@@ -137,7 +139,7 @@ public PngEncoderCore(Configuration configuration, PngEncoder encoder)
137139
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
138140
/// <param name="cancellationToken">The token to request cancellation.</param>
139141
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
140-
where TPixel : unmanaged, IPixel<TPixel>
142+
where TPixel : unmanaged, IPixel<TPixel>
141143
{
142144
Guard.NotNull(image, nameof(image));
143145
Guard.NotNull(stream, nameof(stream));
@@ -146,7 +148,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
146148
this.height = image.Height;
147149

148150
ImageMetadata metadata = image.Metadata;
149-
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
151+
PngMetadata pngMetadata = GetPngMetadata(image);
150152
this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
151153

152154
stream.Write(PngConstants.HeaderBytes);
@@ -234,6 +236,54 @@ public void Dispose()
234236
this.currentScanline?.Dispose();
235237
}
236238

239+
private static PngMetadata GetPngMetadata<TPixel>(Image<TPixel> image)
240+
where TPixel : unmanaged, IPixel<TPixel>
241+
{
242+
if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
243+
{
244+
return png;
245+
}
246+
247+
if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
248+
{
249+
AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata();
250+
return PngMetadata.FromAnimatedMetadata(ani);
251+
}
252+
253+
if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
254+
{
255+
AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata();
256+
return PngMetadata.FromAnimatedMetadata(ani);
257+
}
258+
259+
// Return explicit new instance so we do not mutate the original metadata.
260+
return new();
261+
}
262+
263+
private static PngFrameMetadata GetPngFrameMetadata<TPixel>(ImageFrame<TPixel> frame)
264+
where TPixel : unmanaged, IPixel<TPixel>
265+
{
266+
if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
267+
{
268+
return png;
269+
}
270+
271+
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
272+
{
273+
AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata();
274+
return PngFrameMetadata.FromAnimatedMetadata(ani);
275+
}
276+
277+
if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
278+
{
279+
AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata();
280+
return PngFrameMetadata.FromAnimatedMetadata(ani);
281+
}
282+
283+
// Return explicit new instance so we do not mutate the original metadata.
284+
return new();
285+
}
286+
237287
/// <summary>
238288
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
239289
/// </summary>
@@ -985,9 +1035,10 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
9851035
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
9861036
/// <param name="imageFrame">The image frame.</param>
9871037
/// <param name="sequenceNumber">The frame sequence number.</param>
988-
private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber)
1038+
private FrameControl WriteFrameControlChunk<TPixel>(Stream stream, ImageFrame<TPixel> imageFrame, uint sequenceNumber)
1039+
where TPixel : unmanaged, IPixel<TPixel>
9891040
{
990-
PngFrameMetadata frameMetadata = imageFrame.Metadata.GetPngFrameMetadata();
1041+
PngFrameMetadata frameMetadata = GetPngFrameMetadata(imageFrame);
9911042

9921043
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
9931044
FrameControl fcTL = new(

src/ImageSharp/Formats/Png/PngFrameMetadata.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private PngFrameMetadata(PngFrameMetadata other)
3434
/// wait before continuing with the processing of the Data Stream.
3535
/// The clock starts ticking immediately after the graphic is rendered.
3636
/// </summary>
37-
public Rational FrameDelay { get; set; }
37+
public Rational FrameDelay { get; set; } = new(0);
3838

3939
/// <summary>
4040
/// Gets or sets the type of frame area disposal to be done after rendering this frame
@@ -59,4 +59,20 @@ internal void FromChunk(in FrameControl frameControl)
5959

6060
/// <inheritdoc/>
6161
public IDeepCloneable DeepClone() => new PngFrameMetadata(this);
62+
63+
internal static PngFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
64+
=> new()
65+
{
66+
FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000),
67+
DisposalMethod = GetMode(metadata.DisposalMode),
68+
BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? PngBlendMethod.Source : PngBlendMethod.Over,
69+
};
70+
71+
private static PngDisposalMethod GetMode(FrameDisposalMode mode) => mode switch
72+
{
73+
FrameDisposalMode.RestoreToBackground => PngDisposalMethod.RestoreToBackground,
74+
FrameDisposalMode.RestoreToPrevious => PngDisposalMethod.RestoreToPrevious,
75+
FrameDisposalMode.DoNotDispose => PngDisposalMethod.DoNotDispose,
76+
_ => PngDisposalMethod.DoNotDispose,
77+
};
6278
}

src/ImageSharp/Formats/Png/PngMetadata.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,37 @@ private PngMetadata(PngMetadata other)
8585

8686
/// <inheritdoc/>
8787
public IDeepCloneable DeepClone() => new PngMetadata(this);
88+
89+
internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
90+
{
91+
// Should the conversion be from a format that uses a 24bit palette entries (gif)
92+
// we need to clone and adjust the color table to allow for transparency.
93+
ReadOnlyMemory<Color>? colorTable = metadata.ColorTable;
94+
if (metadata.ColorTable.HasValue)
95+
{
96+
Color[] clone = metadata.ColorTable.Value.ToArray();
97+
for (int i = 0; i < clone.Length; i++)
98+
{
99+
ref Color c = ref clone[i];
100+
if (c == metadata.BackgroundColor)
101+
{
102+
// Png treats background as fully empty
103+
c = default;
104+
break;
105+
}
106+
}
107+
108+
colorTable = clone;
109+
}
110+
111+
return new()
112+
{
113+
ColorType = colorTable.HasValue ? PngColorType.Palette : null,
114+
BitDepth = colorTable.HasValue
115+
? (PngBitDepth)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(colorTable.Value.Length), 1, 8)
116+
: null,
117+
ColorTable = colorTable,
118+
RepeatCount = metadata.RepeatCount,
119+
};
120+
}
88121
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uin
3333
height,
3434
duration,
3535
(flags & 2) == 0 ? WebpBlendingMethod.Over : WebpBlendingMethod.Source,
36-
(flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.None)
36+
(flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose)
3737
{
3838
}
3939

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAni
259259

260260
if (hasAnimation)
261261
{
262-
WebpMetadata webpMetadata = metadata.GetWebpMetadata();
262+
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
263263
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
264264
}
265265
}
@@ -307,7 +307,7 @@ public void Encode<TPixel>(ImageFrame<TPixel> frame, Stream stream, bool hasAnim
307307

308308
if (hasAnimation)
309309
{
310-
WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
310+
WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(frame);
311311

312312
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
313313
prevPosition = new WebpFrameData(

0 commit comments

Comments
 (0)