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));
}
}
}