Browse Source

Complete Webp and add tests

pull/2588/head
James Jackson-South 2 years ago
parent
commit
a486558ed1
  1. 20
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  2. 13
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  3. 18
      src/ImageSharp/Formats/Png/MetadataExtensions.cs
  4. 4
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  5. 2
      src/ImageSharp/Formats/Png/PngDisposalMethod.cs
  6. 59
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  7. 18
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  8. 33
      src/ImageSharp/Formats/Png/PngMetadata.cs
  9. 2
      src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
  10. 4
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  11. 4
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  12. 2
      src/ImageSharp/Formats/Webp/MetadataExtensions.cs
  13. 56
      src/ImageSharp/Formats/Webp/WebpCommonUtils.cs
  14. 2
      src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
  15. 2
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  16. 8
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  17. 8
      src/ImageSharp/Formats/Webp/WebpMetadata.cs
  18. 6
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  19. 92
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  20. 109
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  21. 93
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

20
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -190,19 +190,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
return GifMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
private static GifFrameMetadata? GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex)
where TPixel : unmanaged, IPixel<TPixel>
{
if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata? gif))
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
{
return gif;
}
GifFrameMetadata? metadata = null;
if (frame.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? png))
if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
{
AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata();
metadata = GifFrameMetadata.FromAnimatedMetadata(ani);
@ -342,7 +343,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
// We can't deduplicate here as we need the background pixels to be present in the buffer.
if (metadata?.DisposalMethod == GifDisposalMethod.RestoreToBackground)
{
for (int y = 0; y < currentFrame.PixelBuffer.Height; y++)
{
Span<TPixel> sourceRow = currentFrame.PixelBuffer.DangerousGetRowSpan(y);
Span<TPixel> destinationRow = encodingFrame.PixelBuffer.DangerousGetRowSpan(y);
sourceRow.CopyTo(destinationRow);
}
}
else
{
this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
}
IndexedImageFrame<TPixel> quantized;
if (useLocal)

13
src/ImageSharp/Formats/Gif/MetadataExtensions.cs

@ -56,16 +56,25 @@ public static partial class MetadataExtensions
/// <returns>
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetGifFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source)
=> new()
{
Color background = Color.Transparent;
if (source.GlobalColorTable != null)
{
background = source.GlobalColorTable.Value.Span[source.BackgroundColorIndex];
}
return new()
{
ColorTable = source.GlobalColorTable,
ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
RepeatCount = source.RepeatCount,
BackgroundColor = background,
};
}
internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source)
=> new()

18
src/ImageSharp/Formats/Png/MetadataExtensions.cs

@ -36,7 +36,7 @@ public static partial class MetadataExtensions
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
public static PngFrameMetadata GetPngMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
/// <summary>
/// Gets the png format specific metadata for the image frame.
@ -46,7 +46,7 @@ public static partial class MetadataExtensions
/// <returns>
/// <see langword="true"/> if the png frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata)
public static bool TryGetPngMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata)
=> source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
internal static AnimatedImageMetadata ToAnimatedImageMetadata(this PngMetadata source)
@ -58,17 +58,25 @@ public static partial class MetadataExtensions
};
internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this PngFrameMetadata source)
=> new()
{
double delay = source.FrameDelay.ToDouble();
if (double.IsNaN(delay))
{
delay = 0;
}
return new()
{
ColorTableMode = FrameColorTableMode.Global,
Duration = TimeSpan.FromMilliseconds(source.FrameDelay.ToDouble() * 1000),
Duration = TimeSpan.FromMilliseconds(delay * 1000),
DisposalMode = GetMode(source.DisposalMethod),
BlendMode = source.BlendMethod == PngBlendMethod.Source ? FrameBlendMode.Source : FrameBlendMode.Over,
};
}
private static FrameDisposalMode GetMode(PngDisposalMethod method) => method switch
{
PngDisposalMethod.None => FrameDisposalMode.DoNotDispose,
PngDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose,
PngDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
PngDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
_ => FrameDisposalMode.Unspecified,

4
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -581,7 +581,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.header.Height,
metadata);
PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata();
PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngMetadata();
frameMetadata.FromChunk(in frameControl);
this.bytesPerPixel = this.CalculateBytesPerPixel();
@ -630,7 +630,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
pixelRegion.Clear();
}
PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata();
PngFrameMetadata frameMetadata = frame.Metadata.GetPngMetadata();
frameMetadata.FromChunk(currentFrameControl);
this.previousScanline?.Dispose();

2
src/ImageSharp/Formats/Png/PngDisposalMethod.cs

@ -11,7 +11,7 @@ public enum PngDisposalMethod
/// <summary>
/// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
/// </summary>
None,
DoNotDispose,
/// <summary>
/// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.

59
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -7,8 +7,10 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -137,7 +139,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
@ -146,7 +148,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.height = image.Height;
ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
PngMetadata pngMetadata = GetPngMetadata(image);
this.SanitizeAndSetEncoderOptions<TPixel>(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
stream.Write(PngConstants.HeaderBytes);
@ -234,6 +236,54 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.currentScanline?.Dispose();
}
private static PngMetadata GetPngMetadata<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
{
return png;
}
if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
{
AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata();
return PngMetadata.FromAnimatedMetadata(ani);
}
if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
{
AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata();
return PngMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
private static PngFrameMetadata GetPngFrameMetadata<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
{
return png;
}
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
{
AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata();
return PngFrameMetadata.FromAnimatedMetadata(ani);
}
if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
{
AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata();
return PngFrameMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
/// <summary>
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
/// </summary>
@ -985,9 +1035,10 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="imageFrame">The image frame.</param>
/// <param name="sequenceNumber">The frame sequence number.</param>
private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber)
private FrameControl WriteFrameControlChunk<TPixel>(Stream stream, ImageFrame<TPixel> imageFrame, uint sequenceNumber)
where TPixel : unmanaged, IPixel<TPixel>
{
PngFrameMetadata frameMetadata = imageFrame.Metadata.GetPngFrameMetadata();
PngFrameMetadata frameMetadata = GetPngFrameMetadata(imageFrame);
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
FrameControl fcTL = new(

18
src/ImageSharp/Formats/Png/PngFrameMetadata.cs

@ -34,7 +34,7 @@ public class PngFrameMetadata : IDeepCloneable
/// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered.
/// </summary>
public Rational FrameDelay { get; set; }
public Rational FrameDelay { get; set; } = new(0);
/// <summary>
/// Gets or sets the type of frame area disposal to be done after rendering this frame
@ -59,4 +59,20 @@ public class PngFrameMetadata : IDeepCloneable
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngFrameMetadata(this);
internal static PngFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
=> new()
{
FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000),
DisposalMethod = GetMode(metadata.DisposalMode),
BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? PngBlendMethod.Source : PngBlendMethod.Over,
};
private static PngDisposalMethod GetMode(FrameDisposalMode mode) => mode switch
{
FrameDisposalMode.RestoreToBackground => PngDisposalMethod.RestoreToBackground,
FrameDisposalMode.RestoreToPrevious => PngDisposalMethod.RestoreToPrevious,
FrameDisposalMode.DoNotDispose => PngDisposalMethod.DoNotDispose,
_ => PngDisposalMethod.DoNotDispose,
};
}

33
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -85,4 +85,37 @@ public class PngMetadata : IDeepCloneable
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetadata(this);
internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
{
// Should the conversion be from a format that uses a 24bit palette entries (gif)
// we need to clone and adjust the color table to allow for transparency.
ReadOnlyMemory<Color>? colorTable = metadata.ColorTable;
if (metadata.ColorTable.HasValue)
{
Color[] clone = metadata.ColorTable.Value.ToArray();
for (int i = 0; i < clone.Length; i++)
{
ref Color c = ref clone[i];
if (c == metadata.BackgroundColor)
{
// Png treats background as fully empty
c = default;
break;
}
}
colorTable = clone;
}
return new()
{
ColorType = colorTable.HasValue ? PngColorType.Palette : null,
BitDepth = colorTable.HasValue
? (PngBitDepth)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(colorTable.Value.Length), 1, 8)
: null,
ColorTable = colorTable,
RepeatCount = metadata.RepeatCount,
};
}
}

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

@ -33,7 +33,7 @@ internal readonly struct WebpFrameData
height,
duration,
(flags & 2) == 0 ? WebpBlendingMethod.Over : WebpBlendingMethod.Source,
(flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.None)
(flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose)
{
}

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

@ -259,7 +259,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
WebpMetadata webpMetadata = metadata.GetWebpMetadata();
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}
}
@ -307,7 +307,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(frame);
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
prevPosition = new WebpFrameData(

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

@ -333,7 +333,7 @@ internal class Vp8Encoder : IDisposable
if (hasAnimation)
{
WebpMetadata webpMetadata = metadata.GetWebpMetadata();
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}
}
@ -477,7 +477,7 @@ internal class Vp8Encoder : IDisposable
if (hasAnimation)
{
WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(frame);
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
prevPosition = new WebpFrameData(

2
src/ImageSharp/Formats/Webp/MetadataExtensions.cs

@ -69,7 +69,7 @@ public static partial class MetadataExtensions
private static FrameDisposalMode GetMode(WebpDisposalMethod method) => method switch
{
WebpDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
WebpDisposalMethod.None => FrameDisposalMode.DoNotDispose,
WebpDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose,
_ => FrameDisposalMode.DoNotDispose,
};
}

56
src/ImageSharp/Formats/Webp/WebpCommonUtils.cs

@ -4,6 +4,8 @@
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@ -13,6 +15,54 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// </summary>
internal static class WebpCommonUtils
{
public static WebpMetadata GetWebpMetadata<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
{
return webp;
}
if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
{
AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata();
return WebpMetadata.FromAnimatedMetadata(ani);
}
if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
{
AnimatedImageMetadata ani = png.ToAnimatedImageMetadata();
return WebpMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
public static WebpFrameMetadata GetWebpFrameMetadata<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
{
return webp;
}
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
{
AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata();
return WebpFrameMetadata.FromAnimatedMetadata(ani);
}
if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
{
AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata();
return WebpFrameMetadata.FromAnimatedMetadata(ani);
}
// Return explicit new instance so we do not mutate the original metadata.
return new();
}
/// <summary>
/// Checks if the pixel row is not opaque.
/// </summary>
@ -27,7 +77,7 @@ internal static class WebpCommonUtils
int length = (row.Length * 4) - 3;
fixed (byte* src = rowBytes)
{
var alphaMaskVector256 = Vector256.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
Vector256<byte> alphaMaskVector256 = Vector256.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
Vector256<byte> all0x80Vector256 = Vector256.Create((byte)0x80).AsByte();
for (; i + 128 <= length; i += 128)
@ -124,7 +174,7 @@ internal static class WebpCommonUtils
private static unsafe bool IsNoneOpaque64Bytes(byte* src, int i)
{
var alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
Vector128<byte> alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
Vector128<byte> a0 = Sse2.LoadVector128(src + i).AsByte();
Vector128<byte> a1 = Sse2.LoadVector128(src + i + 16).AsByte();
@ -144,7 +194,7 @@ internal static class WebpCommonUtils
private static unsafe bool IsNoneOpaque32Bytes(byte* src, int i)
{
var alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
Vector128<byte> alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
Vector128<byte> a0 = Sse2.LoadVector128(src + i).AsByte();
Vector128<byte> a1 = Sse2.LoadVector128(src + i + 16).AsByte();

2
src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs

@ -11,7 +11,7 @@ public enum WebpDisposalMethod
/// <summary>
/// Do not dispose. Leave the canvas as is.
/// </summary>
None = 0,
DoNotDispose = 0,
/// <summary>
/// Dispose to background color. Fill the rectangle on the canvas covered by the current frame with background color specified in the ANIM chunk.

2
src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

@ -123,7 +123,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
}
else
{
WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata();
WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
lossless = webpMetadata.FileFormat == WebpFileFormatType.Lossless;
}

8
src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs

@ -44,4 +44,12 @@ public class WebpFrameMetadata : IDeepCloneable
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new WebpFrameMetadata(this);
internal static WebpFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
=> new()
{
FrameDelay = (uint)metadata.Duration.Milliseconds,
BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? WebpBlendingMethod.Source : WebpBlendingMethod.Over,
DisposalMethod = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose
};
}

8
src/ImageSharp/Formats/Webp/WebpMetadata.cs

@ -46,4 +46,12 @@ public class WebpMetadata : IDeepCloneable
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new WebpMetadata(this);
internal static WebpMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
=> new()
{
FileFormat = WebpFileFormatType.Lossless,
BackgroundColor = metadata.BackgroundColor,
RepeatCount = metadata.RepeatCount
};
}

6
src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs

@ -200,7 +200,7 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Add(Rgba32 rgba, byte index)
public readonly void Add(Rgba32 rgba, byte index)
{
int r = rgba.R >> RgbShift;
int g = rgba.G >> RgbShift;
@ -211,7 +211,7 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
}
[MethodImpl(InliningOptions.ShortMethod)]
public bool TryGetValue(Rgba32 rgba, out short match)
public readonly bool TryGetValue(Rgba32 rgba, out short match)
{
int r = rgba.R >> RgbShift;
int g = rgba.G >> RgbShift;
@ -226,7 +226,7 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// Clears the cache resetting each entry to empty.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Clear() => this.table.GetSpan().Fill(-1);
public readonly void Clear() => this.table.GetSpan().Fill(-1);
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetPaletteIndex(int r, int g, int b, int a)

92
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -2,6 +2,8 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -245,7 +247,7 @@ public class GifEncoderTests
int count = 0;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata _))
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _))
{
count++;
}
@ -261,7 +263,7 @@ public class GifEncoderTests
count = 0;
foreach (ImageFrame<TPixel> frame in image2.Frames)
{
if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata _))
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _))
{
count++;
}
@ -269,4 +271,90 @@ public class GifEncoderTests
Assert.Equal(image2.Frames.Count, count);
}
[Theory]
[WithFile(TestImages.Png.APng, PixelTypes.Rgba32)]
public void Encode_AnimatedFormatTransform_FromPng<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, new GifEncoder());
memStream.Position = 0;
using Image<TPixel> output = Image.Load<TPixel>(memStream);
// TODO: Find a better way to compare.
// The image has been visually checked but the quantization and frame trimming pattern used in the gif encoder
// means we cannot use an exact comparison nor replicate using the quantizing processor.
ImageComparer.TolerantPercentage(1.51f).VerifySimilarity(output, image);
PngMetadata png = image.Metadata.GetPngMetadata();
GifMetadata gif = output.Metadata.GetGifMetadata();
Assert.Equal(png.RepeatCount, gif.RepeatCount);
for (int i = 0; i < image.Frames.Count; i++)
{
PngFrameMetadata pngF = image.Frames[i].Metadata.GetPngMetadata();
GifFrameMetadata gifF = output.Frames[i].Metadata.GetGifMetadata();
Assert.Equal((int)(pngF.FrameDelay.ToDouble() * 100), gifF.FrameDelay);
switch (pngF.DisposalMethod)
{
case PngDisposalMethod.RestoreToBackground:
Assert.Equal(GifDisposalMethod.RestoreToBackground, gifF.DisposalMethod);
break;
case PngDisposalMethod.DoNotDispose:
default:
Assert.Equal(GifDisposalMethod.NotDispose, gifF.DisposalMethod);
break;
}
}
}
[Theory]
[WithFile(TestImages.Webp.Lossless.Animated, PixelTypes.Rgba32)]
public void Encode_AnimatedFormatTransform_FromWebp<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(WebpDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, new GifEncoder());
memStream.Position = 0;
using Image<TPixel> output = Image.Load<TPixel>(memStream);
// TODO: Find a better way to compare.
// The image has been visually checked but the quantization and frame trimming pattern used in the gif encoder
// means we cannot use an exact comparison nor replicate using the quantizing processor.
ImageComparer.TolerantPercentage(0.776f).VerifySimilarity(output, image);
WebpMetadata webp = image.Metadata.GetWebpMetadata();
GifMetadata gif = output.Metadata.GetGifMetadata();
Assert.Equal(webp.RepeatCount, gif.RepeatCount);
for (int i = 0; i < image.Frames.Count; i++)
{
WebpFrameMetadata webpF = image.Frames[i].Metadata.GetWebpMetadata();
GifFrameMetadata gifF = output.Frames[i].Metadata.GetGifMetadata();
Assert.Equal(webpF.FrameDelay, (uint)(gifF.FrameDelay * 10));
switch (webpF.DisposalMethod)
{
case WebpDisposalMethod.RestoreToBackground:
Assert.Equal(GifDisposalMethod.RestoreToBackground, gifF.DisposalMethod);
break;
case WebpDisposalMethod.DoNotDispose:
default:
Assert.Equal(GifDisposalMethod.NotDispose, gifF.DisposalMethod);
break;
}
}
}
}

109
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -5,6 +5,7 @@
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -454,10 +455,6 @@ public partial class PngEncoderTests
memStream.Position = 0;
image.DebugSave(provider: provider, encoder: PngEncoder, null, false);
image.DebugSave(provider: provider, encoder: new GifEncoder(), "gif", false);
string path = provider.Utility.GetTestOutputFileName("gif");
image.Save(path);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageComparer.Exact.VerifySimilarity(output, image);
@ -472,8 +469,8 @@ public partial class PngEncoderTests
for (int i = 0; i < image.Frames.Count; i++)
{
PngFrameMetadata originalFrameMetadata = image.Frames[i].Metadata.GetPngFrameMetadata();
PngFrameMetadata outputFrameMetadata = output.Frames[i].Metadata.GetPngFrameMetadata();
PngFrameMetadata originalFrameMetadata = image.Frames[i].Metadata.GetPngMetadata();
PngFrameMetadata outputFrameMetadata = output.Frames[i].Metadata.GetPngMetadata();
Assert.Equal(originalFrameMetadata.FrameDelay, outputFrameMetadata.FrameDelay);
Assert.Equal(originalFrameMetadata.BlendMethod, outputFrameMetadata.BlendMethod);
@ -481,6 +478,106 @@ public partial class PngEncoderTests
}
}
[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void Encode_AnimatedFormatTransform_FromGif<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(GifDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, PngEncoder);
memStream.Position = 0;
image.Save(provider.Utility.GetTestOutputFileName("png"), new PngEncoder());
image.Save(provider.Utility.GetTestOutputFileName("gif"), new GifEncoder());
using Image<TPixel> output = Image.Load<TPixel>(memStream);
// TODO: Find a better way to compare.
// The image has been visually checked but the quantization pattern used in the png encoder
// means we cannot use an exact comparison nor replicate using the quantizing processor.
ImageComparer.TolerantPercentage(0.12f).VerifySimilarity(output, image);
GifMetadata gif = image.Metadata.GetGifMetadata();
PngMetadata png = output.Metadata.GetPngMetadata();
Assert.Equal(gif.RepeatCount, png.RepeatCount);
for (int i = 0; i < image.Frames.Count; i++)
{
GifFrameMetadata gifF = image.Frames[i].Metadata.GetGifMetadata();
PngFrameMetadata pngF = output.Frames[i].Metadata.GetPngMetadata();
Assert.Equal(gifF.FrameDelay, (int)(pngF.FrameDelay.ToDouble() * 100));
switch (gifF.DisposalMethod)
{
case GifDisposalMethod.RestoreToBackground:
Assert.Equal(PngDisposalMethod.RestoreToBackground, pngF.DisposalMethod);
break;
case GifDisposalMethod.RestoreToPrevious:
Assert.Equal(PngDisposalMethod.RestoreToPrevious, pngF.DisposalMethod);
break;
case GifDisposalMethod.Unspecified:
case GifDisposalMethod.NotDispose:
default:
Assert.Equal(PngDisposalMethod.DoNotDispose, pngF.DisposalMethod);
break;
}
}
}
[Theory]
[WithFile(TestImages.Webp.Lossless.Animated, PixelTypes.Rgba32)]
public void Encode_AnimatedFormatTransform_FromWebp<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(WebpDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, PngEncoder);
memStream.Position = 0;
using Image<TPixel> output = Image.Load<TPixel>(memStream);
ImageComparer.Exact.VerifySimilarity(output, image);
WebpMetadata webp = image.Metadata.GetWebpMetadata();
PngMetadata png = output.Metadata.GetPngMetadata();
Assert.Equal(webp.RepeatCount, png.RepeatCount);
for (int i = 0; i < image.Frames.Count; i++)
{
WebpFrameMetadata webpF = image.Frames[i].Metadata.GetWebpMetadata();
PngFrameMetadata pngF = output.Frames[i].Metadata.GetPngMetadata();
Assert.Equal(webpF.FrameDelay, (uint)(pngF.FrameDelay.ToDouble() * 1000));
switch (webpF.BlendMethod)
{
case WebpBlendingMethod.Source:
Assert.Equal(PngBlendMethod.Source, pngF.BlendMethod);
break;
case WebpBlendingMethod.Over:
default:
Assert.Equal(PngBlendMethod.Over, pngF.BlendMethod);
break;
}
switch (webpF.DisposalMethod)
{
case WebpDisposalMethod.RestoreToBackground:
Assert.Equal(PngDisposalMethod.RestoreToBackground, pngF.DisposalMethod);
break;
case WebpDisposalMethod.DoNotDispose:
default:
Assert.Equal(PngDisposalMethod.DoNotDispose, pngF.DisposalMethod);
break;
}
}
}
[Theory]
[MemberData(nameof(PngTrnsFiles))]
public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType)

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

@ -2,6 +2,8 @@
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -60,6 +62,97 @@ public class WebpEncoderTests
encoded.CompareToReferenceOutput(ImageComparer.Tolerant(0.01f), provider, null, "webp");
}
[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void Encode_AnimatedFormatTransform_FromGif<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(GifDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, new WebpEncoder());
memStream.Position = 0;
using Image<TPixel> output = Image.Load<TPixel>(memStream);
ImageComparer.Exact.VerifySimilarity(output, image);
GifMetadata gif = image.Metadata.GetGifMetadata();
WebpMetadata webp = output.Metadata.GetWebpMetadata();
Assert.Equal(gif.RepeatCount, webp.RepeatCount);
for (int i = 0; i < image.Frames.Count; i++)
{
GifFrameMetadata gifF = image.Frames[i].Metadata.GetGifMetadata();
WebpFrameMetadata webpF = output.Frames[i].Metadata.GetWebpMetadata();
Assert.Equal(gifF.FrameDelay, (int)(webpF.FrameDelay / 10));
switch (gifF.DisposalMethod)
{
case GifDisposalMethod.RestoreToBackground:
Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod);
break;
case GifDisposalMethod.RestoreToPrevious:
case GifDisposalMethod.Unspecified:
case GifDisposalMethod.NotDispose:
default:
Assert.Equal(WebpDisposalMethod.DoNotDispose, webpF.DisposalMethod);
break;
}
}
}
[Theory]
[WithFile(TestImages.Png.APng, PixelTypes.Rgba32)]
public void Encode_AnimatedFormatTransform_FromPng<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, new WebpEncoder());
memStream.Position = 0;
using Image<TPixel> output = Image.Load<TPixel>(memStream);
ImageComparer.Exact.VerifySimilarity(output, image);
PngMetadata png = image.Metadata.GetPngMetadata();
WebpMetadata webp = output.Metadata.GetWebpMetadata();
Assert.Equal(png.RepeatCount, webp.RepeatCount);
for (int i = 0; i < image.Frames.Count; i++)
{
PngFrameMetadata pngF = image.Frames[i].Metadata.GetPngMetadata();
WebpFrameMetadata webpF = output.Frames[i].Metadata.GetWebpMetadata();
Assert.Equal((uint)(pngF.FrameDelay.ToDouble() * 1000), webpF.FrameDelay);
switch (pngF.BlendMethod)
{
case PngBlendMethod.Source:
Assert.Equal(WebpBlendingMethod.Source, webpF.BlendMethod);
break;
case PngBlendMethod.Over:
default:
Assert.Equal(WebpBlendingMethod.Over, webpF.BlendMethod);
break;
}
switch (pngF.DisposalMethod)
{
case PngDisposalMethod.RestoreToBackground:
Assert.Equal(WebpDisposalMethod.RestoreToBackground, webpF.DisposalMethod);
break;
case PngDisposalMethod.DoNotDispose:
default:
Assert.Equal(WebpDisposalMethod.DoNotDispose, webpF.DisposalMethod);
break;
}
}
}
[Theory]
[WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy.
[WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]

Loading…
Cancel
Save