diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs new file mode 100644 index 0000000000..cc81130dd7 --- /dev/null +++ b/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 +{ + /// + /// On-stack worker struct to efficiently encapsulate the TPixel -> L8 -> Y conversion chain of 8x8 pixel blocks. + /// + /// The pixel type to work on + internal ref struct LuminanceForwardConverter + where TPixel : unmanaged, IPixel + { + /// + /// The Y component + /// + public Block8x8F Y; + + /// + /// Temporal 8x8 block to hold TPixel data + /// + private GenericBlock8x8 pixelBlock; + + /// + /// Temporal RGB block + /// + private GenericBlock8x8 l8Block; + + public static LuminanceForwardConverter Create() + { + var result = default(LuminanceForwardConverter); + return result; + } + + /// + /// Converts a 8x8 image area inside 'pixels' at position (x,y) placing the result members of the structure () + /// + public void Convert(ImageFrame frame, int x, int y, ref RowOctet currentRows) + { + this.pixelBlock.LoadAndStretchEdges(frame.PixelBuffer, x, y, ref currentRows); + + Span l8Span = this.l8Block.AsSpanUnsafe(); + PixelOperations.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; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs index ecd64a7823..cceed407c2 100644 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ b/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 @@ internal interface IJpegEncoderOptions /// /// The subsample ratio of the jpg image. JpegSubsample? Subsample { get; } + + /// + /// Gets the color type. + /// + JpegColorType? ColorType { get; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegColorType.cs b/src/ImageSharp/Formats/Jpeg/JpegColorType.cs new file mode 100644 index 0000000000..73b3215d62 --- /dev/null +++ b/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 +{ + /// + /// Provides enumeration of available JPEG color types. + /// + public enum JpegColorType : byte + { + /// + /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. + /// + YCbCr = 0, + + /// + /// Single channel, luminance. + /// + Luminance = 1 + } +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index c4355cdbe1..8571cf0ec3 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -912,6 +912,7 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, 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); } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index b549bd8a32..8131f74d26 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -25,6 +25,11 @@ public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions /// public JpegSubsample? Subsample { get; set; } + /// + /// Gets or sets the color type, that will be used to encode the image. + /// + public JpegColorType? ColorType { get; set; } + /// /// Encodes the image to the specified stream from the . /// @@ -35,6 +40,7 @@ public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel { var encoder = new JpegEncoderCore(this); + this.InitializeColorType(image); encoder.Encode(image, stream); } @@ -50,7 +56,31 @@ public Task EncodeAsync(Image image, Stream stream, Cancellation where TPixel : unmanaged, IPixel { var encoder = new JpegEncoderCore(this); + this.InitializeColorType(image); return encoder.EncodeAsync(image, stream, cancellationToken); } + + /// + /// If ColorType was not set, set it based on the given image. + /// + private void InitializeColorType(Image image) + where TPixel : unmanaged, IPixel + { + // 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; + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index d26fbb936d..f5dc1c79fe 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -57,6 +57,11 @@ internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals /// private readonly int? quality; + /// + /// Gets or sets the subsampling method to use. + /// + private readonly JpegColorType? colorType; + /// /// The accumulated bits to write to the stream. /// @@ -90,6 +95,7 @@ public JpegEncoderCore(IJpegEncoderOptions options) { this.quality = options.Quality; this.subsample = options.Subsample; + this.colorType = options.ColorType; } /// @@ -115,42 +121,6 @@ public JpegEncoderCore(IJpegEncoderOptions options) 8, 8, 8, }; - /// - /// 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<<4 | 0x00. - /// - // The C# compiler emits this as a compile-time constant embedded in the PE file. - // This is effectively compiled down to: return new ReadOnlySpan(&data, length) - // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - private static ReadOnlySpan 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) - }; - /// /// 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 @@ public void Encode(Image image, Stream stream, CancellationToken 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 @@ public void Encode(Image image, Stream stream, CancellationToken // 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 @@ public void Encode(Image image, Stream stream, CancellationToken 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 @@ private void Encode444(Image pixels, CancellationToken cancellat } } + /// + /// Encodes the image with no chroma, just luminance. + /// + /// The pixel format. + /// The pixel accessor providing access to the image pixels. + /// The token to monitor for cancellation. + /// The reference to the emit buffer. + private void EncodeGrayscale(Image pixels, CancellationToken cancellationToken, ref byte emitBufferBase) + where TPixel : unmanaged, IPixel + { + // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) + // (Partially done with YCbCrForwardConverter) + Block8x8F temp1 = default; + Block8x8F temp2 = default; + + Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; + + var unzig = ZigZag.CreateUnzigTable(); + + // ReSharper disable once InconsistentNaming + int prevDCY = 0; + + var pixelConverter = LuminanceForwardConverter.Create(); + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; + RowOctet 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); + } + } + } + /// /// Writes the application header containing the JFIF identifier plus extra data. /// @@ -896,24 +918,36 @@ private void WriteStartOfFrame(int width, int height, int componentCount) 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 @@ private void WriteStartOfFrame(int width, int height, int componentCount) 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 @@ private void WriteStartOfFrame(int width, int height, int componentCount) /// /// The pixel format. /// The pixel accessor providing access to the image pixels. + /// The number of components in a pixel. /// The token to monitor for cancellation. - private void WriteStartOfScan(Image image, CancellationToken cancellationToken) + private void WriteStartOfScan(Image image, int componentCount, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) - // TODO: We should allow grayscale writing. - this.outputStream.Write(SosHeaderYCbCr); + Span componentId = stackalloc byte[] + { + 0x01, + 0x02, + 0x03 + }; + Span 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<<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(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. diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index c9dded6352..9670d167e0 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/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 @@ public JpegMetadata() /// Initializes a new instance of the class. /// /// The metadata to create an instance from. - private JpegMetadata(JpegMetadata other) => this.Quality = other.Quality; + private JpegMetadata(JpegMetadata other) + { + this.Quality = other.Quality; + this.ColorType = other.ColorType; + } /// /// Gets or sets the encoded quality. /// public int Quality { get; set; } = 75; + /// + /// Gets or sets the encoded quality. + /// + public JpegColorType? ColorType { get; set; } + /// public IDeepCloneable DeepClone() => new JpegMetadata(this); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs b/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs index 6597e0ccb8..16488f6d21 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs +++ b/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 diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 6481c711ff..9a1d423a6d 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -40,6 +40,14 @@ public class JpegEncoderTests { JpegSubsample.Ratio444, 100 }, }; + public static readonly TheoryData Grayscale_Quality = + new TheoryData + { + { 40 }, + { 60 }, + { 100 } + }; + public static readonly TheoryData RatioFiles = new TheoryData { @@ -80,9 +88,20 @@ public void Encode_PreserveQuality(string imagePath, int quality) [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(TestImageProvider provider, JpegSubsample subsample, int quality) where TPixel : unmanaged, IPixel => 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(TestImageProvider provider, int quality) + where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, null, quality, JpegColorType.Luminance); + [Theory] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)] public void EncodeBaseline_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegSubsample subsample, int quality) @@ -101,13 +120,13 @@ public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvid : ImageComparer.TolerantPercentage(5f); provider.LimitAllocatorBufferCapacity().InBytesSqrt(200); - TestJpegEncoderCore(provider, subsample, 100, comparer); + TestJpegEncoderCore(provider, subsample, 100, JpegColorType.YCbCr, comparer); } /// /// Anton's SUPER-SCIENTIFIC tolerance threshold calculation /// - 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 @@ private static ImageComparer GetComparer(int quality, JpegSubsample subsample) private static void TestJpegEncoderCore( TestImageProvider provider, - JpegSubsample subsample, + JpegSubsample? subsample, int quality = 100, + JpegColorType colorType = JpegColorType.YCbCr, ImageComparer comparer = null) where TPixel : unmanaged, IPixel { @@ -142,7 +162,8 @@ private static void TestJpegEncoderCore( var encoder = new JpegEncoder { Subsample = subsample, - Quality = quality + Quality = quality, + ColorType = colorType }; string info = $"{subsample}-Q{quality}"; @@ -298,7 +319,7 @@ public void Encode_PreservesIccProfile() public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs) { using var image = new Image(5000, 5000); - using MemoryStream stream = new MemoryStream(); + using var stream = new MemoryStream(); var cts = new CancellationTokenSource(); if (cancellationDelayMs == 0) { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs index e14ec81c67..503ede1299 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs @@ -12,12 +12,14 @@ public class JpegMetadataTests [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)); } } }