Browse Source

Merge pull request #1586 from ynse01/ynse01/jpeg-grayscale

Grayscale Jpeg encoding
pull/1593/head
James Jackson-South 5 years ago
committed by GitHub
parent
commit
bb72769968
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 59
      src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs
  2. 9
      src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
  3. 21
      src/ImageSharp/Formats/Jpeg/JpegColorType.cs
  4. 1
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  5. 30
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  6. 241
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  7. 15
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  8. 2
      src/ImageSharp/Formats/Jpeg/JpegSubsample.cs
  9. 31
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  10. 4
      tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs

59
src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs

@ -0,0 +1,59 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// On-stack worker struct to efficiently encapsulate the TPixel -> L8 -> Y conversion chain of 8x8 pixel blocks.
/// </summary>
/// <typeparam name="TPixel">The pixel type to work on</typeparam>
internal ref struct LuminanceForwardConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// The Y component
/// </summary>
public Block8x8F Y;
/// <summary>
/// Temporal 8x8 block to hold TPixel data
/// </summary>
private GenericBlock8x8<TPixel> pixelBlock;
/// <summary>
/// Temporal RGB block
/// </summary>
private GenericBlock8x8<L8> l8Block;
public static LuminanceForwardConverter<TPixel> Create()
{
var result = default(LuminanceForwardConverter<TPixel>);
return result;
}
/// <summary>
/// Converts a 8x8 image area inside 'pixels' at position (x,y) placing the result members of the structure (<see cref="Y"/>)
/// </summary>
public void Convert(ImageFrame<TPixel> frame, int x, int y, ref RowOctet<TPixel> currentRows)
{
this.pixelBlock.LoadAndStretchEdges(frame.PixelBuffer, x, y, ref currentRows);
Span<L8> l8Span = this.l8Block.AsSpanUnsafe();
PixelOperations<TPixel>.Instance.ToL8(frame.GetConfiguration(), this.pixelBlock.AsSpanUnsafe(), l8Span);
ref Block8x8F yBlock = ref this.Y;
ref L8 l8Start = ref l8Span[0];
for (int i = 0; i < 64; i++)
{
ref L8 c = ref Unsafe.Add(ref l8Start, i);
yBlock[i] = c.PackedValue;
}
}
}
}

9
src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -20,5 +20,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
/// <value>The subsample ratio of the jpg image.</value>
JpegSubsample? Subsample { get; }
/// <summary>
/// Gets the color type.
/// </summary>
JpegColorType? ColorType { get; }
}
}
}

21
src/ImageSharp/Formats/Jpeg/JpegColorType.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg
{
/// <summary>
/// Provides enumeration of available JPEG color types.
/// </summary>
public enum JpegColorType : byte
{
/// <summary>
/// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
/// </summary>
YCbCr = 0,
/// <summary>
/// Single channel, luminance.
/// </summary>
Luminance = 1
}
}

1
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -912,6 +912,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.Frame.MaxHorizontalFactor = maxH;
this.Frame.MaxVerticalFactor = maxV;
this.ColorSpace = this.DeduceJpegColorSpace();
this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
this.Frame.InitComponents();
this.ImageSizeInMCU = new Size(this.Frame.McusPerLine, this.Frame.McusPerColumn);
}

30
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

@ -25,6 +25,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
public JpegSubsample? Subsample { get; set; }
/// <summary>
/// Gets or sets the color type, that will be used to encode the image.
/// </summary>
public JpegColorType? ColorType { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
@ -35,6 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel>
{
var encoder = new JpegEncoderCore(this);
this.InitializeColorType<TPixel>(image);
encoder.Encode(image, stream);
}
@ -50,7 +56,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel>
{
var encoder = new JpegEncoderCore(this);
this.InitializeColorType<TPixel>(image);
return encoder.EncodeAsync(image, stream, cancellationToken);
}
/// <summary>
/// If ColorType was not set, set it based on the given image.
/// </summary>
private void InitializeColorType<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
// First inspect the image metadata.
if (this.ColorType == null)
{
JpegMetadata metadata = image.Metadata.GetJpegMetadata();
this.ColorType = metadata.ColorType;
}
// Secondly, inspect the pixel type.
if (this.ColorType == null)
{
bool isGrayscale =
typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) ||
typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32);
this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
}
}
}
}

241
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -57,6 +57,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private readonly int? quality;
/// <summary>
/// Gets or sets the subsampling method to use.
/// </summary>
private readonly JpegColorType? colorType;
/// <summary>
/// The accumulated bits to write to the stream.
/// </summary>
@ -90,6 +95,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{
this.quality = options.Quality;
this.subsample = options.Subsample;
this.colorType = options.ColorType;
}
/// <summary>
@ -115,42 +121,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
8, 8, 8,
};
/// <summary>
/// Gets the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
/// - the marker length "\x00\x0c",
/// - the number of components "\x03",
/// - component 1 uses DC table 0 and AC table 0 "\x01\x00",
/// - component 2 uses DC table 1 and AC table 1 "\x02\x11",
/// - component 3 uses DC table 1 and AC table 1 "\x03\x11",
/// - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for
/// sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
/// should be 0x00, 0x3f, 0x00&lt;&lt;4 | 0x00.
/// </summary>
// The C# compiler emits this as a compile-time constant embedded in the PE file.
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// More details can be found: https://github.com/dotnet/roslyn/pull/24621
private static ReadOnlySpan<byte> SosHeaderYCbCr => new byte[]
{
JpegConstants.Markers.XFF, JpegConstants.Markers.SOS,
// Marker
0x00, 0x0c,
// Length (high byte, low byte), must be 6 + 2 * (number of components in scan)
0x03, // Number of components in a scan, 3
0x01, // Component Id Y
0x00, // DC/AC Huffman table
0x02, // Component Id Cb
0x11, // DC/AC Huffman table
0x03, // Component Id Cr
0x11, // DC/AC Huffman table
0x00, // Ss - Start of spectral selection.
0x3f, // Se - End of spectral selection.
0x00
// Ah + Ah (Successive approximation bit position high + low)
};
/// <summary>
/// Gets the unscaled quantization tables in zig-zag order. Each
/// encoder copies and scales the tables according to its quality parameter.
@ -212,6 +182,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream = stream;
ImageMetadata metadata = image.Metadata;
// Compute number of components based on color type in options.
int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3;
// System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1.
int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100);
this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420;
@ -229,10 +202,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Initialize the quantization tables.
InitQuantizationTable(0, scale, ref this.luminanceQuantTable);
InitQuantizationTable(1, scale, ref this.chrominanceQuantTable);
// Compute number of components based on input image type.
const int componentCount = 3;
if (componentCount > 1)
{
InitQuantizationTable(1, scale, ref this.chrominanceQuantTable);
}
// Write the Start Of Image marker.
this.WriteApplicationHeader(metadata);
@ -250,7 +223,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteDefineHuffmanTables(componentCount);
// Write the image data.
this.WriteStartOfScan(image, cancellationToken);
this.WriteStartOfScan(image, componentCount, cancellationToken);
// Write the End Of Image marker.
this.buffer[0] = JpegConstants.Markers.XFF;
@ -468,6 +441,55 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
/// <summary>
/// Encodes the image with no chroma, just luminance.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
/// <param name="emitBufferBase">The reference to the emit buffer.</param>
private void EncodeGrayscale<TPixel>(Image<TPixel> pixels, CancellationToken cancellationToken, ref byte emitBufferBase)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
// (Partially done with YCbCrForwardConverter<TPixel>)
Block8x8F temp1 = default;
Block8x8F temp2 = default;
Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable;
var unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming
int prevDCY = 0;
var pixelConverter = LuminanceForwardConverter<TPixel>.Create();
ImageFrame<TPixel> frame = pixels.Frames.RootFrame;
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
RowOctet<TPixel> currentRows = default;
for (int y = 0; y < pixels.Height; y += 8)
{
cancellationToken.ThrowIfCancellationRequested();
currentRows.Update(pixelBuffer, y);
for (int x = 0; x < pixels.Width; x += 8)
{
pixelConverter.Convert(frame, x, y, ref currentRows);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.Y,
ref temp1,
ref temp2,
ref onStackLuminanceQuantTable,
ref unzig,
ref emitBufferBase);
}
}
}
/// <summary>
/// Writes the application header containing the JFIF identifier plus extra data.
/// </summary>
@ -896,24 +918,36 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
0x01
};
switch (this.subsample)
if (this.colorType == JpegColorType.Luminance)
{
case JpegSubsample.Ratio444:
subsamples = stackalloc byte[]
{
0x11,
0x11,
0x11
};
break;
case JpegSubsample.Ratio420:
subsamples = stackalloc byte[]
{
0x22,
0x11,
0x11
};
break;
subsamples = stackalloc byte[]
{
0x11,
0x00,
0x00
};
}
else
{
switch (this.subsample)
{
case JpegSubsample.Ratio444:
subsamples = stackalloc byte[]
{
0x11,
0x11,
0x11
};
break;
case JpegSubsample.Ratio420:
subsamples = stackalloc byte[]
{
0x22,
0x11,
0x11
};
break;
}
}
// Length (high byte, low byte), 8 + components * 3.
@ -926,26 +960,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
this.buffer[5] = (byte)componentCount;
// Number of components (1 byte), usually 1 = Gray scaled, 3 = color YCbCr or YIQ, 4 = color CMYK)
if (componentCount == 1)
for (int i = 0; i < componentCount; i++)
{
this.buffer[6] = 1;
int i3 = 3 * i;
this.buffer[i3 + 6] = (byte)(i + 1);
// No subsampling for grayscale images.
this.buffer[7] = 0x11;
this.buffer[8] = 0x00;
}
else
{
for (int i = 0; i < componentCount; i++)
{
int i3 = 3 * i;
this.buffer[i3 + 6] = (byte)(i + 1);
// We use 4:2:0 chroma subsampling by default.
this.buffer[i3 + 7] = subsamples[i];
this.buffer[i3 + 8] = chroma[i];
}
this.buffer[i3 + 7] = subsamples[i];
this.buffer[i3 + 8] = chroma[i];
}
this.outputStream.Write(this.buffer, 0, (3 * (componentCount - 1)) + 9);
@ -956,22 +977,70 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The pixel accessor providing access to the image pixels.</param>
/// <param name="componentCount">The number of components in a pixel.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
private void WriteStartOfScan<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
private void WriteStartOfScan<TPixel>(Image<TPixel> image, int componentCount, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
// TODO: We should allow grayscale writing.
this.outputStream.Write(SosHeaderYCbCr);
Span<byte> componentId = stackalloc byte[]
{
0x01,
0x02,
0x03
};
Span<byte> huffmanId = stackalloc byte[]
{
0x00,
0x11,
0x11
};
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c",
// - the number of components "\x03",
// - component 1 uses DC table 0 and AC table 0 "\x01\x00",
// - component 2 uses DC table 1 and AC table 1 "\x02\x11",
// - component 3 uses DC table 1 and AC table 1 "\x03\x11",
// - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for
// sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
// should be 0x00, 0x3f, 0x00&lt;&lt;4 | 0x00.
this.buffer[0] = JpegConstants.Markers.XFF;
this.buffer[1] = JpegConstants.Markers.SOS;
// Length (high byte, low byte), must be 6 + 2 * (number of components in scan)
int sosSize = 6 + (2 * componentCount);
this.buffer[2] = 0x00;
this.buffer[3] = (byte)sosSize;
this.buffer[4] = (byte)componentCount; // Number of components in a scan
for (int i = 0; i < componentCount; i++)
{
int i2 = 2 * i;
this.buffer[i2 + 5] = componentId[i]; // Component Id
this.buffer[i2 + 6] = huffmanId[i]; // DC/AC Huffman table
}
this.buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
this.buffer[sosSize] = 0x3f; // Se - End of spectral selection.
this.buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
this.outputStream.Write(this.buffer, 0, sosSize + 2);
ref byte emitBufferBase = ref MemoryMarshal.GetReference<byte>(this.emitBuffer);
switch (this.subsample)
if (this.colorType == JpegColorType.Luminance)
{
case JpegSubsample.Ratio444:
this.Encode444(image, cancellationToken, ref emitBufferBase);
break;
case JpegSubsample.Ratio420:
this.Encode420(image, cancellationToken, ref emitBufferBase);
break;
this.EncodeGrayscale(image, cancellationToken, ref emitBufferBase);
}
else
{
switch (this.subsample)
{
case JpegSubsample.Ratio444:
this.Encode444(image, cancellationToken, ref emitBufferBase);
break;
case JpegSubsample.Ratio420:
this.Encode420(image, cancellationToken, ref emitBufferBase);
break;
}
}
// Pad the last byte with 1's.

15
src/ImageSharp/Formats/Jpeg/JpegMetadata.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -19,14 +19,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Initializes a new instance of the <see cref="JpegMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private JpegMetadata(JpegMetadata other) => this.Quality = other.Quality;
private JpegMetadata(JpegMetadata other)
{
this.Quality = other.Quality;
this.ColorType = other.ColorType;
}
/// <summary>
/// Gets or sets the encoded quality.
/// </summary>
public int Quality { get; set; } = 75;
/// <summary>
/// Gets or sets the encoded quality.
/// </summary>
public JpegColorType? ColorType { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new JpegMetadata(this);
}
}
}

2
src/ImageSharp/Formats/Jpeg/JpegSubsample.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg

31
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

@ -40,6 +40,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ JpegSubsample.Ratio444, 100 },
};
public static readonly TheoryData<int> Grayscale_Quality =
new TheoryData<int>
{
{ 40 },
{ 60 },
{ 100 }
};
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
{
@ -80,9 +88,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 600, 400, PixelTypes.Rgba32)]
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 100, 100, 100, 255, PixelTypes.L8)]
public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality);
[Theory]
[WithFile(TestImages.Png.BikeGrayscale, nameof(Grayscale_Quality), PixelTypes.L8)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.Rgba32, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.L8, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.L16, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La16, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La32, 100)]
public void EncodeBaseline_Grayscale<TPixel>(TestImageProvider<TPixel> provider, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, null, quality, JpegColorType.Luminance);
[Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)]
public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
@ -101,13 +120,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
: ImageComparer.TolerantPercentage(5f);
provider.LimitAllocatorBufferCapacity().InBytesSqrt(200);
TestJpegEncoderCore(provider, subsample, 100, comparer);
TestJpegEncoderCore(provider, subsample, 100, JpegColorType.YCbCr, comparer);
}
/// <summary>
/// Anton's SUPER-SCIENTIFIC tolerance threshold calculation
/// </summary>
private static ImageComparer GetComparer(int quality, JpegSubsample subsample)
private static ImageComparer GetComparer(int quality, JpegSubsample? subsample)
{
float tolerance = 0.015f; // ~1.5%
@ -129,8 +148,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private static void TestJpegEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
JpegSubsample subsample,
JpegSubsample? subsample,
int quality = 100,
JpegColorType colorType = JpegColorType.YCbCr,
ImageComparer comparer = null)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -142,7 +162,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var encoder = new JpegEncoder
{
Subsample = subsample,
Quality = quality
Quality = quality,
ColorType = colorType
};
string info = $"{subsample}-Q{quality}";
@ -298,7 +319,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs)
{
using var image = new Image<Rgba32>(5000, 5000);
using MemoryStream stream = new MemoryStream();
using var stream = new MemoryStream();
var cts = new CancellationTokenSource();
if (cancellationDelayMs == 0)
{

4
tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs

@ -12,12 +12,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Fact]
public void CloneIsDeep()
{
var meta = new JpegMetadata { Quality = 50 };
var meta = new JpegMetadata { Quality = 50, ColorType = JpegColorType.Luminance };
var clone = (JpegMetadata)meta.DeepClone();
clone.Quality = 99;
clone.ColorType = JpegColorType.YCbCr;
Assert.False(meta.Quality.Equals(clone.Quality));
Assert.False(meta.ColorType.Equals(clone.ColorType));
}
}
}

Loading…
Cancel
Save