Browse Source

Normalize encoders and update refs

pull/2842/head
James Jackson-South 1 year ago
parent
commit
b4567a4ac7
  1. 2
      src/ImageSharp/Formats/FormatConnectingMetadata.cs
  2. 2
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  3. 94
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 43
      src/ImageSharp/Formats/IAnimatedImageEncoder.cs
  5. 50
      src/ImageSharp/Formats/IQuantizingImageEncoder.cs
  6. 3
      src/ImageSharp/Formats/Png/PngEncoder.cs
  7. 48
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  8. 22
      src/ImageSharp/Formats/QuantizingImageEncoder.cs
  9. 8
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  10. 4
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  11. 4
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  12. 2
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  13. 50
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  14. 22
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  15. 16
      tests/Directory.Build.targets
  16. 10
      tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs
  17. 25
      tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs
  18. 10
      tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs
  19. 4
      tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
  20. 63
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  21. 2
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  22. 4
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  23. 12
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  24. 42
      tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs
  25. 39
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs
  26. 4
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

2
src/ImageSharp/Formats/FormatConnectingMetadata.cs

@ -45,7 +45,7 @@ public class FormatConnectingMetadata
/// Gets the default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when the disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
/// <remarks>
/// Defaults to <see cref="Color.Transparent"/>.

2
src/ImageSharp/Formats/Gif/GifEncoder.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Image encoder for writing image data to a stream in gif format.
/// </summary>
public sealed class GifEncoder : QuantizingImageEncoder
public sealed class GifEncoder : QuantizingAnimatedImageEncoder
{
/// <summary>
/// Gets the color table mode: Global or local.

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

@ -54,6 +54,19 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;
/// <summary>
/// The number of times any animation is repeated.
/// </summary>
private readonly ushort? repeatCount;
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
@ -68,6 +81,8 @@ internal sealed class GifEncoderCore
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
}
/// <summary>
@ -141,9 +156,17 @@ internal sealed class GifEncoderCore
frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
}
byte backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
byte backgroundIndex;
if (this.backgroundColor.HasValue)
{
backgroundIndex = GetBackgroundIndex(quantized, this.backgroundColor.Value);
}
else
{
backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
}
// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
@ -161,7 +184,7 @@ internal sealed class GifEncoderCore
// Write application extensions.
XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile);
}
this.EncodeFirstFrame(stream, frameMetadata, quantized);
@ -169,7 +192,13 @@ internal sealed class GifEncoderCore
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode);
this.EncodeAdditionalFrames(
stream,
image,
globalPalette,
derivedTransparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
stream.WriteByte(GifConstants.EndIntroducer);
@ -194,7 +223,8 @@ internal sealed class GifEncoderCore
Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex,
FrameDisposalMode previousDisposalMode)
FrameDisposalMode previousDisposalMode,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
@ -213,6 +243,16 @@ internal sealed class GifEncoderCore
for (int i = 1; i < image.Frames.Count; i++)
{
if (cancellationToken.IsCancellationRequested)
{
if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
}
return;
}
// Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
@ -291,6 +331,10 @@ internal sealed class GifEncoderCore
ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
@ -299,7 +343,7 @@ internal sealed class GifEncoderCore
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
background,
true);
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
@ -428,14 +472,12 @@ internal sealed class GifEncoderCore
private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// Returns the index of the transparent color in the palette.
/// </summary>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The current gif frame metadata.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
/// <returns>The <see cref="int"/>.</returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -463,6 +505,36 @@ internal sealed class GifEncoderCore
return index;
}
/// <summary>
/// Returns the index of the background color in the palette.
/// </summary>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="background">The background color to match.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="byte"/>.</returns>
private static byte GetBackgroundIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, Color background)
where TPixel : unmanaged, IPixel<TPixel>
{
int index = -1;
if (quantized != null)
{
TPixel backgroundPixel = background.ToPixel<TPixel>();
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
for (int i = 0; i < palette.Length; i++)
{
if (!backgroundPixel.Equals(palette[i]))
{
continue;
}
index = i;
break;
}
}
return (byte)Numerics.Clamp(index, 0, 255);
}
/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>

43
src/ImageSharp/Formats/IAnimatedImageEncoder.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Defines the contract for all image encoders that allow encoding animation sequences.
/// </summary>
public interface IAnimatedImageEncoder
{
/// <summary>
/// Gets the default background color of the canvas when animating in supported encoders.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
Color? BackgroundColor { get; }
/// <summary>
/// Gets the number of times any animation is repeated in supported encoders.
/// </summary>
ushort? RepeatCount { get; }
/// <summary>
/// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders.
/// </summary>
bool? AnimateRootFrame { get; }
}
/// <summary>
/// Acts as a base class for all image encoders that allow encoding animation sequences.
/// </summary>
public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; init; }
/// <inheritdoc/>
public ushort? RepeatCount { get; init; }
/// <inheritdoc/>
public bool? AnimateRootFrame { get; init; } = true;
}

50
src/ImageSharp/Formats/IQuantizingImageEncoder.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Defines the contract for all image encoders that allow color palette generation via quantization.
/// </summary>
public interface IQuantizingImageEncoder
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
IQuantizer? Quantizer { get; }
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
IPixelSamplingStrategy PixelSamplingStrategy { get; }
}
/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder
{
/// <inheritdoc/>
public IQuantizer? Quantizer { get; init; }
/// <inheritdoc/>
public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
}
/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization when
/// encoding animation sequences.
/// </summary>
public abstract class QuantizingAnimatedImageEncoder : QuantizingImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; }
/// <inheritdoc/>
public ushort? RepeatCount { get; }
/// <inheritdoc/>
public bool? AnimateRootFrame { get; }
}

3
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -9,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Image encoder for writing image data to a stream in png format.
/// </summary>
public class PngEncoder : QuantizingImageEncoder
public class PngEncoder : QuantizingAnimatedImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.

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

@ -123,6 +123,24 @@ internal sealed class PngEncoderCore : IDisposable
/// </summary>
private int derivedTransparencyIndex = -1;
/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;
/// <summary>
/// The number of times any animation is repeated.
/// </summary>
private readonly ushort? repeatCount;
/// <summary>
/// Whether the root frame is shown as part of the animated sequence.
/// </summary>
private readonly bool? animateRootFrame;
/// <summary>
/// A reusable Crc32 hashing instance.
/// </summary>
@ -139,6 +157,9 @@ internal sealed class PngEncoderCore : IDisposable
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
this.quantizer = encoder.Quantizer;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
this.animateRootFrame = encoder.AnimateRootFrame;
}
/// <summary>
@ -171,7 +192,7 @@ internal sealed class PngEncoderCore : IDisposable
if (clearTransparency)
{
currentFrame = clonedFrame = currentFrame.Clone();
ClearTransparentPixels(currentFrame);
ClearTransparentPixels(currentFrame, Color.Transparent);
}
// Do not move this. We require an accurate bit depth for the header chunk.
@ -194,11 +215,15 @@ internal sealed class PngEncoderCore : IDisposable
if (image.Frames.Count > 1)
{
this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount);
this.WriteAnimationControlChunk(
stream,
(uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)),
this.repeatCount ?? pngMetadata.RepeatCount);
}
// If the first frame isn't animated, write it as usual and skip it when writing animated frames
if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1)
bool userAnimateRootFrame = this.animateRootFrame == true;
if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1)
{
FrameControl frameControl = new((uint)this.width, (uint)this.height);
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
@ -235,12 +260,20 @@ internal sealed class PngEncoderCore : IDisposable
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[currentFrameIndex];
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = currentFrame.Metadata.GetPngMetadata();
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
@ -249,12 +282,12 @@ internal sealed class PngEncoderCore : IDisposable
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
background,
blend);
if (clearTransparency)
{
ClearTransparentPixels(encodingFrame);
ClearTransparentPixels(encodingFrame, background);
}
// Each frame control sequence number must be incremented by the number of frame data chunks that follow.
@ -291,12 +324,13 @@ internal sealed class PngEncoderCore : IDisposable
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="clone">The cloned image frame where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone)
/// <param name="color">The color to replace transparent pixels with.</param>
private static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone, Color color)
where TPixel : unmanaged, IPixel<TPixel>
=> clone.ProcessPixelRows(accessor =>
{
// TODO: We should be able to speed this up with SIMD and masking.
Rgba32 transparent = Color.Transparent.ToPixel<Rgba32>();
Rgba32 transparent = color.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
Span<TPixel> span = accessor.GetRowSpan(y);

22
src/ImageSharp/Formats/QuantizingImageEncoder.cs

@ -1,22 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : ImageEncoder
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
public IQuantizer? Quantizer { get; init; }
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
}

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

@ -236,7 +236,7 @@ internal class Vp8LEncoder : IDisposable
/// </summary>
public Vp8LHashChain HashChain { get; }
public WebpVp8X EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation)
public WebpVp8X EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation, ushort? repeatCount)
where TPixel : unmanaged, IPixel<TPixel>
{
// Write bytes from the bit-writer buffer to the stream.
@ -258,7 +258,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata();
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, repeatCount ?? webpMetadata.RepeatCount);
}
return vp8x;
@ -315,8 +315,8 @@ internal class Vp8LEncoder : IDisposable
(uint)bounds.Width,
(uint)bounds.Height,
frameMetadata.FrameDelay,
frameMetadata.BlendMethod,
frameMetadata.DisposalMethod)
frameMetadata.BlendMode,
frameMetadata.DisposalMode)
.WriteHeaderTo(stream);
}

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

@ -495,8 +495,8 @@ internal class Vp8Encoder : IDisposable
(uint)bounds.Width,
(uint)bounds.Height,
frameMetadata.FrameDelay,
frameMetadata.BlendMethod,
frameMetadata.DisposalMethod)
frameMetadata.BlendMode,
frameMetadata.DisposalMode)
.WriteHeaderTo(stream);
}

4
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -220,8 +220,8 @@ internal class WebpAnimationDecoder : IDisposable
{
WebpFrameMetadata frameMetadata = meta.GetWebpMetadata();
frameMetadata.FrameDelay = frameData.Duration;
frameMetadata.BlendMethod = frameData.BlendingMethod;
frameMetadata.DisposalMethod = frameData.DisposalMethod;
frameMetadata.BlendMode = frameData.BlendingMethod;
frameMetadata.DisposalMode = frameData.DisposalMethod;
}
/// <summary>

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Image encoder for writing an image to a stream in the Webp format.
/// </summary>
public sealed class WebpEncoder : ImageEncoder
public sealed class WebpEncoder : AnimatedImageEncoder
{
/// <summary>
/// Gets the webp file format used. Either lossless or lossy.

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

@ -5,7 +5,6 @@ using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@ -78,6 +77,19 @@ internal sealed class WebpEncoderCore
/// </summary>
private readonly WebpFileFormatType? fileFormat;
/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;
/// <summary>
/// The number of times any animation is repeated.
/// </summary>
private readonly ushort? repeatCount;
/// <summary>
/// The global configuration.
/// </summary>
@ -103,6 +115,8 @@ internal sealed class WebpEncoderCore
this.skipMetadata = encoder.SkipMetadata;
this.nearLossless = encoder.NearLossless;
this.nearLosslessQuality = encoder.NearLosslessQuality;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
}
/// <summary>
@ -147,7 +161,7 @@ internal sealed class WebpEncoderCore
long initialPosition = stream.Position;
bool hasAlpha = false;
WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation);
WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation, this.repeatCount);
// Encode the first frame.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
@ -156,7 +170,7 @@ internal sealed class WebpEncoderCore
if (hasAnimation)
{
FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod;
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
// Encode additional frames
// This frame is reused to store de-duplicated pixel buffers.
@ -164,12 +178,20 @@ internal sealed class WebpEncoderCore
for (int i = 1; i < image.Frames.Count; i++)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
frameMetadata = currentFrame.Metadata.GetWebpMetadata();
bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over;
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
@ -178,7 +200,7 @@ internal sealed class WebpEncoderCore
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
background,
blend,
ClampingMode.Even);
@ -197,7 +219,7 @@ internal sealed class WebpEncoderCore
hasAlpha |= animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation);
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod;
previousDisposal = frameMetadata.DisposalMode;
}
}
@ -229,7 +251,7 @@ internal sealed class WebpEncoderCore
// Encode the first frame.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod;
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata);
@ -239,12 +261,20 @@ internal sealed class WebpEncoderCore
for (int i = 1; i < image.Frames.Count; i++)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
frameMetadata = currentFrame.Metadata.GetWebpMetadata();
bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over;
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
@ -253,7 +283,7 @@ internal sealed class WebpEncoderCore
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
background,
blend,
ClampingMode.Even);
@ -273,7 +303,7 @@ internal sealed class WebpEncoderCore
hasAlpha |= animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata);
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod;
previousDisposal = frameMetadata.DisposalMode;
}
encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition);

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

@ -24,19 +24,21 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
private WebpFrameMetadata(WebpFrameMetadata other)
{
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
this.BlendMethod = other.BlendMethod;
this.DisposalMode = other.DisposalMode;
this.BlendMode = other.BlendMode;
}
/// <summary>
/// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels
/// of the previous canvas.
/// </summary>
public FrameBlendMode BlendMethod { get; set; }
public FrameBlendMode BlendMode { get; set; }
/// <summary>
/// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// Gets or sets how the current frame is to be treated after it has been displayed
/// (before rendering the next frame) on the canvas.
/// </summary>
public FrameDisposalMode DisposalMethod { get; set; }
public FrameDisposalMode DisposalMode { get; set; }
/// <summary>
/// Gets or sets the frame duration. The time to wait before displaying the next frame,
@ -49,8 +51,8 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
=> new()
{
FrameDelay = (uint)metadata.Duration.TotalMilliseconds,
BlendMethod = metadata.BlendMode,
DisposalMethod = GetMode(metadata.DisposalMode)
BlendMode = metadata.BlendMode,
DisposalMode = GetMode(metadata.DisposalMode)
};
/// <inheritdoc/>
@ -59,8 +61,8 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
{
ColorTableMode = FrameColorTableMode.Global,
Duration = TimeSpan.FromMilliseconds(this.FrameDelay),
DisposalMode = this.DisposalMethod,
BlendMode = this.BlendMethod,
DisposalMode = this.DisposalMode,
BlendMode = this.BlendMode,
};
/// <inheritdoc/>

16
tests/Directory.Build.targets

@ -18,18 +18,18 @@
<ItemGroup>
<!-- Test Dependencies -->
<PackageReference Update="Colourful" Version="3.1.0" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="13.4.0" />
<PackageReference Update="Colourful" Version="3.2.0" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="14.2.0" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="8.0.0-beta.23580.1" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" />
<PackageReference Update="Moq" Version="4.20.70" />
<PackageReference Update="NetVips" Version="2.4.0" />
<PackageReference Update="NetVips.Native" Version="8.15.0" />
<PackageReference Update="PhotoSauce.MagicScaler" Version="0.14.0" />
<PackageReference Update="Pfim" Version="0.11.2" />
<PackageReference Update="Moq" Version="4.20.72" />
<PackageReference Update="NetVips" Version="3.0.0" />
<PackageReference Update="NetVips.Native" Version="8.16.0" />
<PackageReference Update="PhotoSauce.MagicScaler" Version="0.14.2" />
<PackageReference Update="Pfim" Version="0.11.3" />
<PackageReference Update="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="6.0.5.128" Condition="'$(IsOSX)'=='true'" />
<PackageReference Update="SharpZipLib" Version="1.4.2" />
<PackageReference Update="SkiaSharp" Version="2.88.6" />
<PackageReference Update="SkiaSharp" Version="2.88.9" />
<PackageReference Update="System.Drawing.Common" Version="6.0.0" />
<PackageReference Update="System.IO.Compression" Version="4.3.0" />
</ItemGroup>

10
tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs

@ -29,22 +29,22 @@ public class DecodeTga
[Benchmark(Baseline = true, Description = "ImageMagick Tga")]
public int TgaImageMagick()
{
var settings = new MagickReadSettings { Format = MagickFormat.Tga };
using var image = new MagickImage(new MemoryStream(this.data), settings);
return image.Width;
MagickReadSettings settings = new() { Format = MagickFormat.Tga };
using MagickImage image = new(new MemoryStream(this.data), settings);
return (int)image.Width;
}
[Benchmark(Description = "ImageSharp Tga")]
public int TgaImageSharp()
{
using var image = Image.Load<Bgr24>(this.data);
using Image<Bgr24> image = Image.Load<Bgr24>(this.data);
return image.Width;
}
[Benchmark(Description = "Pfim Tga")]
public int TgaPfim()
{
using var image = Targa.Create(this.data, this.pfimConfig);
using Targa image = Targa.Create(this.data, this.pfimConfig);
return image.Width;
}

25
tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs

@ -44,34 +44,35 @@ public class DecodeWebp
[Benchmark(Description = "Magick Lossy Webp")]
public int WebpLossyMagick()
{
var settings = new MagickReadSettings { Format = MagickFormat.WebP };
using var memoryStream = new MemoryStream(this.webpLossyBytes);
using var image = new MagickImage(memoryStream, settings);
return image.Width;
MagickReadSettings settings = new() { Format = MagickFormat.WebP };
using MemoryStream memoryStream = new(this.webpLossyBytes);
using MagickImage image = new(memoryStream, settings);
return (int)image.Width;
}
[Benchmark(Description = "ImageSharp Lossy Webp")]
public int WebpLossy()
{
using var memoryStream = new MemoryStream(this.webpLossyBytes);
using var image = Image.Load<Rgba32>(memoryStream);
using MemoryStream memoryStream = new(this.webpLossyBytes);
using Image<Rgba32> image = Image.Load<Rgba32>(memoryStream);
return image.Height;
}
[Benchmark(Description = "Magick Lossless Webp")]
public int WebpLosslessMagick()
{
var settings = new MagickReadSettings { Format = MagickFormat.WebP };
using var memoryStream = new MemoryStream(this.webpLossyBytes);
using var image = new MagickImage(memoryStream, settings);
return image.Width;
MagickReadSettings settings = new()
{ Format = MagickFormat.WebP };
using MemoryStream memoryStream = new(this.webpLossyBytes);
using MagickImage image = new(memoryStream, settings);
return (int)image.Width;
}
[Benchmark(Description = "ImageSharp Lossless Webp")]
public int WebpLossless()
{
using var memoryStream = new MemoryStream(this.webpLosslessBytes);
using var image = Image.Load<Rgba32>(memoryStream);
using MemoryStream memoryStream = new(this.webpLosslessBytes);
using Image<Rgba32> image = Image.Load<Rgba32>(memoryStream);
return image.Height;
}

10
tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs

@ -13,7 +13,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs;
[MarkdownExporter]
[HtmlExporter]
[Config(typeof(Config.Short))]
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
public class EncodeWebp
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
{
private MagickImage webpMagick;
private Image<Rgba32> webp;
@ -52,10 +54,7 @@ public class EncodeWebp
AlphaCompression = WebPAlphaCompression.None,
FilterStrength = 60,
SnsStrength = 50,
Pass = 1,
// 100 means off.
NearLossless = 100
Pass = 1
};
this.webpMagick.Quality = 75;
@ -85,9 +84,6 @@ public class EncodeWebp
{
Lossless = true,
Method = 4,
// 100 means off.
NearLossless = 100
};
this.webpMagick.Quality = 75;

4
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj

@ -41,8 +41,8 @@
<ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.10" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.10" Condition="'$(IsWindows)'=='true'" />
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" Condition="'$(IsWindows)'=='true'" />
<PackageReference Include="Colourful" />
<PackageReference Include="NetVips" />
<PackageReference Include="NetVips.Native" />

63
tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs

@ -19,6 +19,7 @@ using SystemDrawingImage = System.Drawing.Image;
namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave;
[Flags]
public enum JpegKind
{
Baseline = 1,
@ -30,7 +31,7 @@ public class LoadResizeSaveStressRunner
{
private const int Quality = 75;
// Set the quality for ImagSharp
// Set the quality for ImageSharp
private readonly JpegEncoder imageSharpJpegEncoder = new() { Quality = Quality };
private readonly ImageCodecInfo systemDrawingJpegCodec =
ImageCodecInfo.GetImageEncoders().First(codec => codec.FormatID == ImageFormat.Jpeg.Guid);
@ -126,7 +127,7 @@ public class LoadResizeSaveStressRunner
: Environment.ProcessorCount;
int partitionSize = (int)Math.Ceiling((double)this.Images.Length / maxDegreeOfParallelism);
List<Task> tasks = new();
List<Task> tasks = [];
for (int i = 0; i < this.Images.Length; i += partitionSize)
{
int end = Math.Min(i + partitionSize, this.Images.Length);
@ -176,13 +177,13 @@ public class LoadResizeSaveStressRunner
public void SystemDrawingResize(string input)
{
using var image = SystemDrawingImage.FromFile(input, true);
using SystemDrawingImage image = SystemDrawingImage.FromFile(input, true);
this.LogImageProcessed(image.Width, image.Height);
(int Width, int Height) scaled = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize);
var resized = new Bitmap(scaled.Width, scaled.Height);
using var graphics = Graphics.FromImage(resized);
using var attributes = new ImageAttributes();
(int width, int height) = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize);
Bitmap resized = new(width, height);
using Graphics graphics = Graphics.FromImage(resized);
using ImageAttributes attributes = new();
attributes.SetWrapMode(WrapMode.TileFlipXY);
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.CompositingMode = CompositingMode.SourceCopy;
@ -191,8 +192,8 @@ public class LoadResizeSaveStressRunner
graphics.DrawImage(image, System.Drawing.Rectangle.FromLTRB(0, 0, resized.Width, resized.Height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes);
// Save the results
using var encoderParams = new EncoderParameters(1);
using var qualityParam = new EncoderParameter(Encoder.Quality, (long)Quality);
using EncoderParameters encoderParams = new(1);
using EncoderParameter qualityParam = new(Encoder.Quality, (long)Quality);
encoderParams.Param[0] = qualityParam;
resized.Save(this.OutputPath(input), this.systemDrawingJpegCodec, encoderParams);
}
@ -223,7 +224,7 @@ public class LoadResizeSaveStressRunner
public async Task ImageSharpResizeAsync(string input)
{
using FileStream output = File.Open(this.OutputPath(input), FileMode.Create);
await using FileStream output = File.Open(this.OutputPath(input), FileMode.Create);
// Resize it to fit a 150x150 square.
DecoderOptions options = new()
@ -246,11 +247,11 @@ public class LoadResizeSaveStressRunner
public void MagickResize(string input)
{
using var image = new MagickImage(input);
this.LogImageProcessed(image.Width, image.Height);
using MagickImage image = new(input);
this.LogImageProcessed((int)image.Width, (int)image.Height);
// Resize it to fit a 150x150 square
image.Resize(this.ThumbnailSize, this.ThumbnailSize);
image.Resize((uint)this.ThumbnailSize, (uint)this.ThumbnailSize);
// Reduce the size of the file
image.Strip();
@ -264,7 +265,7 @@ public class LoadResizeSaveStressRunner
public void MagicScalerResize(string input)
{
var settings = new ProcessImageSettings()
ProcessImageSettings settings = new()
{
Width = this.ThumbnailSize,
Height = this.ThumbnailSize,
@ -273,19 +274,19 @@ public class LoadResizeSaveStressRunner
};
// TODO: Is there a way to capture input dimensions for IncreaseTotalMegapixels?
using var output = new FileStream(this.OutputPath(input), FileMode.Create);
using FileStream output = new(this.OutputPath(input), FileMode.Create);
MagicImageProcessor.ProcessImage(input, output, settings);
}
public void SkiaCanvasResize(string input)
{
using var original = SKBitmap.Decode(input);
using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height);
(int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using var surface = SKSurface.Create(new SKImageInfo(scaled.Width, scaled.Height, original.ColorType, original.AlphaType));
using var paint = new SKPaint() { FilterQuality = SKFilterQuality.High };
(int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, original.ColorType, original.AlphaType));
using SKPaint paint = new() { FilterQuality = SKFilterQuality.High };
SKCanvas canvas = surface.Canvas;
canvas.Scale((float)scaled.Width / original.Width);
canvas.Scale((float)width / original.Width);
canvas.DrawBitmap(original, 0, 0, paint);
canvas.Flush();
@ -297,16 +298,16 @@ public class LoadResizeSaveStressRunner
public void SkiaBitmapResize(string input)
{
using var original = SKBitmap.Decode(input);
using SKBitmap original = SKBitmap.Decode(input);
this.LogImageProcessed(original.Width, original.Height);
(int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using var resized = original.Resize(new SKImageInfo(scaled.Width, scaled.Height), SKFilterQuality.High);
(int width, int height) = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize);
using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High);
if (resized == null)
{
return;
}
using var image = SKImage.FromBitmap(resized);
using SKImage image = SKImage.FromBitmap(resized);
using FileStream output = File.OpenWrite(this.OutputPath(input));
image.Encode(SKEncodedImageFormat.Jpeg, Quality)
.SaveTo(output);
@ -314,21 +315,21 @@ public class LoadResizeSaveStressRunner
public void SkiaBitmapDecodeToTargetSize(string input)
{
using var codec = SKCodec.Create(input);
using SKCodec codec = SKCodec.Create(input);
SKImageInfo info = codec.Info;
this.LogImageProcessed(info.Width, info.Height);
(int Width, int Height) scaled = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize);
SKSizeI supportedScale = codec.GetScaledDimensions((float)scaled.Width / info.Width);
(int width, int height) = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize);
SKSizeI supportedScale = codec.GetScaledDimensions((float)width / info.Width);
using var original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height));
using SKBitmap resized = original.Resize(new SKImageInfo(scaled.Width, scaled.Height), SKFilterQuality.High);
using SKBitmap original = SKBitmap.Decode(codec, new SKImageInfo(supportedScale.Width, supportedScale.Height));
using SKBitmap resized = original.Resize(new SKImageInfo(width, height), SKFilterQuality.High);
if (resized == null)
{
return;
}
using var image = SKImage.FromBitmap(resized);
using SKImage image = SKImage.FromBitmap(resized);
using FileStream output = File.OpenWrite(this.OutputPath(input, nameof(this.SkiaBitmapDecodeToTargetSize)));
image.Encode(SKEncodedImageFormat.Jpeg, Quality)
@ -338,7 +339,7 @@ public class LoadResizeSaveStressRunner
public void NetVipsResize(string input)
{
// Thumbnail to fit a 150x150 square
using var thumb = NetVipsImage.Thumbnail(input, this.ThumbnailSize, this.ThumbnailSize);
using NetVipsImage thumb = NetVipsImage.Thumbnail(input, this.ThumbnailSize, this.ThumbnailSize);
// Save the results
thumb.Jpegsave(this.OutputPath(input), q: Quality, keep: NetVips.Enums.ForeignKeep.None);

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

@ -334,7 +334,7 @@ public class GifEncoderTests
Assert.Equal(webpF.FrameDelay, (uint)(gifF.FrameDelay * 10));
switch (webpF.DisposalMethod)
switch (webpF.DisposalMode)
{
case FrameDisposalMode.RestoreToBackground:
Assert.Equal(FrameDisposalMode.RestoreToBackground, gifF.DisposalMode);

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

@ -524,7 +524,7 @@ public partial class PngEncoderTests
Assert.Equal(webpF.FrameDelay, (uint)(pngF.FrameDelay.ToDouble() * 1000));
switch (webpF.BlendMethod)
switch (webpF.BlendMode)
{
case FrameBlendMode.Source:
Assert.Equal(FrameBlendMode.Source, pngF.BlendMode);
@ -535,7 +535,7 @@ public partial class PngEncoderTests
break;
}
switch (webpF.DisposalMethod)
switch (webpF.DisposalMode)
{
case FrameDisposalMode.RestoreToBackground:
Assert.Equal(FrameDisposalMode.RestoreToBackground, pngF.DisposalMode);

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

@ -98,13 +98,13 @@ public class WebpEncoderTests
switch (gifF.DisposalMode)
{
case FrameDisposalMode.RestoreToBackground:
Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMethod);
Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMode);
break;
case FrameDisposalMode.RestoreToPrevious:
case FrameDisposalMode.Unspecified:
case FrameDisposalMode.DoNotDispose:
default:
Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMethod);
Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMode);
break;
}
}
@ -147,22 +147,22 @@ public class WebpEncoderTests
switch (pngF.BlendMode)
{
case FrameBlendMode.Source:
Assert.Equal(FrameBlendMode.Source, webpF.BlendMethod);
Assert.Equal(FrameBlendMode.Source, webpF.BlendMode);
break;
case FrameBlendMode.Over:
default:
Assert.Equal(FrameBlendMode.Over, webpF.BlendMethod);
Assert.Equal(FrameBlendMode.Over, webpF.BlendMode);
break;
}
switch (pngF.DisposalMode)
{
case FrameDisposalMode.RestoreToBackground:
Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMethod);
Assert.Equal(FrameDisposalMode.RestoreToBackground, webpF.DisposalMode);
break;
case FrameDisposalMode.DoNotDispose:
default:
Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMethod);
Assert.Equal(FrameDisposalMode.DoNotDispose, webpF.DisposalMode);
break;
}
}

42
tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs

@ -1,13 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using ImageMagick;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Normalization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using ImageMagick;
namespace SixLabors.ImageSharp.Tests.Processing.Normalization;
// ReSharper disable InconsistentNaming
@ -22,46 +21,43 @@ public class MagickCompareTests
Image<TPixel> imageFromMagick;
using (Stream stream = LoadAsStream(provider))
{
var magickImage = new MagickImage(stream);
using MagickImage magickImage = new(stream);
// Apply Auto Level using the Grey (BT.709) channel.
magickImage.AutoLevel(Channels.Gray);
imageFromMagick = ConvertImageFromMagick<TPixel>(magickImage);
}
using (Image<TPixel> image = provider.GetImage())
using Image<TPixel> image = provider.GetImage();
HistogramEqualizationOptions options = new()
{
var options = new HistogramEqualizationOptions
{
Method = HistogramEqualizationMethod.AutoLevel,
LuminanceLevels = 256,
SyncChannels = true
};
image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider);
ExactImageComparer.Instance.CompareImages(imageFromMagick, image);
}
Method = HistogramEqualizationMethod.AutoLevel,
LuminanceLevels = 256,
SyncChannels = true
};
image.Mutate(x => x.HistogramEqualization(options));
image.DebugSave(provider);
ExactImageComparer.Instance.CompareImages(imageFromMagick, image);
imageFromMagick.Dispose();
}
private Stream LoadAsStream<TPixel>(TestImageProvider<TPixel> provider)
private static FileStream LoadAsStream<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
string path = TestImageProvider<TPixel>.GetFilePathOrNull(provider);
if (path == null)
{
throw new InvalidOperationException("CompareToMagick() works only with file providers!");
}
string path = TestImageProvider<TPixel>.GetFilePathOrNull(provider)
?? throw new InvalidOperationException("CompareToMagick() works only with file providers!");
var testFile = TestFile.Create(path);
TestFile testFile = TestFile.Create(path);
return new FileStream(testFile.FullPath, FileMode.Open);
}
private Image<TPixel> ConvertImageFromMagick<TPixel>(MagickImage magickImage)
private static Image<TPixel> ConvertImageFromMagick<TPixel>(MagickImage magickImage)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
Configuration configuration = Configuration.Default.Clone();
configuration.PreferContiguousImageBuffers = true;
var result = new Image<TPixel>(configuration, magickImage.Width, magickImage.Height);
Image<TPixel> result = new(configuration, (int)magickImage.Width, (int)magickImage.Height);
Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory<TPixel> resultPixels));

39
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs

@ -15,13 +15,10 @@ public static class ImageComparingUtils
float compareTolerance = 0.01f)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
string path = TestImageProvider<TPixel>.GetFilePathOrNull(provider);
if (path == null)
{
throw new InvalidOperationException("CompareToOriginal() works only with file providers!");
}
string path = TestImageProvider<TPixel>.GetFilePathOrNull(provider)
?? throw new InvalidOperationException("CompareToOriginal() works only with file providers!");
var testFile = TestFile.Create(path);
TestFile testFile = TestFile.Create(path);
using Image<Rgba32> magickImage = DecodeWithMagick<Rgba32>(new FileInfo(testFile.FullPath));
if (useExactComparer)
{
@ -38,25 +35,23 @@ public static class ImageComparingUtils
{
Configuration configuration = Configuration.Default.Clone();
configuration.PreferContiguousImageBuffers = true;
using (var magickImage = new MagickImage(fileInfo))
{
magickImage.AutoOrient();
var result = new Image<TPixel>(configuration, magickImage.Width, magickImage.Height);
using MagickImage magickImage = new(fileInfo);
magickImage.AutoOrient();
Image<TPixel> result = new(configuration, (int)magickImage.Width, (int)magickImage.Height);
Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory<TPixel> resultPixels));
Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory<TPixel> resultPixels));
using (IUnsafePixelCollection<ushort> pixels = magickImage.GetPixelsUnsafe())
{
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);
PixelOperations<TPixel>.Instance.FromRgba32Bytes(
configuration,
data,
resultPixels.Span,
resultPixels.Length);
}
using (IUnsafePixelCollection<ushort> pixels = magickImage.GetPixelsUnsafe())
{
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);
return result;
PixelOperations<TPixel>.Instance.FromRgba32Bytes(
configuration,
data,
resultPixels.Span,
resultPixels.Length);
}
return result;
}
}

4
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -58,7 +58,7 @@ public class MagickReferenceDecoder : ImageDecoder
MagickReadSettings settings = new()
{
FrameCount = (int)options.MaxFrames
FrameCount = options.MaxFrames
};
settings.SetDefines(bmpReadDefines);
settings.SetDefines(pngReadDefines);
@ -67,7 +67,7 @@ public class MagickReferenceDecoder : ImageDecoder
List<ImageFrame<TPixel>> framesList = [];
foreach (IMagickImage<ushort> magicFrame in magickImageCollection)
{
ImageFrame<TPixel> frame = new(configuration, magicFrame.Width, magicFrame.Height);
ImageFrame<TPixel> frame = new(configuration, (int)magicFrame.Width, (int)magicFrame.Height);
framesList.Add(frame);
MemoryGroup<TPixel> framePixels = frame.PixelBuffer.FastMemoryGroup;

Loading…
Cancel
Save