Browse Source

Grayscale Jpeg encoding

pull/1586/head
Ynse Hoornenborg 5 years ago
parent
commit
7eaae92bbf
  1. 49
      src/ImageSharp/Formats/Jpeg/Components/Encoder/L8ToYConverter.cs
  2. 59
      src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs
  3. 9
      src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
  4. 21
      src/ImageSharp/Formats/Jpeg/JpegColorType.cs
  5. 20
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  6. 188
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  7. 7
      src/ImageSharp/Formats/Jpeg/JpegSubsample.cs
  8. 12
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

49
src/ImageSharp/Formats/Jpeg/Components/Encoder/L8ToYConverter.cs

@ -0,0 +1,49 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// Provides 8-bit lookup tables for converting from L8 to Y colorspace.
/// </summary>
internal unsafe struct L8ToYConverter
{
/// <summary>
/// Initializes
/// </summary>
/// <returns>The initialized <see cref="L8ToYConverter"/></returns>
public static L8ToYConverter Create()
{
L8ToYConverter converter = default;
return converter;
}
/// <summary>
/// Optimized method to allocates the correct y, cb, and cr values to the DCT blocks from the given r, g, b values.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ConvertPixelInto(
int l,
ref Block8x8F yResult,
int i) => yResult[i] = l;
public void Convert(Span<L8> l8Span, ref Block8x8F yBlock)
{
ref L8 l8Start = ref l8Span[0];
for (int i = 0; i < 64; i++)
{
ref L8 c = ref Unsafe.Add(ref l8Start, i);
this.ConvertPixelInto(
c.PackedValue,
ref yBlock,
i);
}
}
}
}

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 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>
/// The converter
/// </summary>
private L8ToYConverter converter;
/// <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>);
result.converter = L8ToYConverter.Create();
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;
this.converter.Convert(l8Span, ref yBlock);
}
}
}

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. // Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg namespace SixLabors.ImageSharp.Formats.Jpeg
@ -20,5 +20,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
/// <value>The subsample ratio of the jpg image.</value> /// <value>The subsample ratio of the jpg image.</value>
JpegSubsample? Subsample { get; } 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
}
}

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

@ -25,6 +25,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
public JpegSubsample? Subsample { get; set; } 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> /// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary> /// </summary>
@ -35,6 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new JpegEncoderCore(this); var encoder = new JpegEncoderCore(this);
this.EnrichColorType<TPixel>();
encoder.Encode(image, stream); encoder.Encode(image, stream);
} }
@ -50,7 +56,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new JpegEncoderCore(this); var encoder = new JpegEncoderCore(this);
this.EnrichColorType<TPixel>();
return encoder.EncodeAsync(image, stream, cancellationToken); return encoder.EncodeAsync(image, stream, cancellationToken);
} }
/// <summary>
/// If ColorType was not set, set it based on the given <typeparamref name="TPixel"/>.
/// </summary>
private void EnrichColorType<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.ColorType == null)
{
bool isGrayscale = typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16);
this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
}
}
} }
} }

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

@ -57,6 +57,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
private readonly int? quality; private readonly int? quality;
/// <summary>
/// Gets or sets the subsampling method to use.
/// </summary>
private readonly JpegColorType? colorType;
/// <summary> /// <summary>
/// The accumulated bits to write to the stream. /// The accumulated bits to write to the stream.
/// </summary> /// </summary>
@ -90,6 +95,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{ {
this.quality = options.Quality; this.quality = options.Quality;
this.subsample = options.Subsample; this.subsample = options.Subsample;
this.colorType = options.ColorType;
} }
/// <summary> /// <summary>
@ -115,42 +121,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
8, 8, 8, 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> /// <summary>
/// Gets the unscaled quantization tables in zig-zag order. Each /// Gets the unscaled quantization tables in zig-zag order. Each
/// encoder copies and scales the tables according to its quality parameter. /// encoder copies and scales the tables according to its quality parameter.
@ -212,10 +182,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream = stream; this.outputStream = stream;
ImageMetadata metadata = image.Metadata; 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. // 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); int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100);
this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420;
// Force SubSample into Grayscale for single component ColorType.
this.subsample = (componentCount == 1) ? JpegSubsample.Grayscale : this.subsample;
// Convert from a quality rating to a scaling factor. // Convert from a quality rating to a scaling factor.
int scale; int scale;
if (qlty < 50) if (qlty < 50)
@ -229,10 +205,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Initialize the quantization tables. // Initialize the quantization tables.
InitQuantizationTable(0, scale, ref this.luminanceQuantTable); InitQuantizationTable(0, scale, ref this.luminanceQuantTable);
InitQuantizationTable(1, scale, ref this.chrominanceQuantTable); if (componentCount > 1)
{
// Compute number of components based on input image type. InitQuantizationTable(1, scale, ref this.chrominanceQuantTable);
const int componentCount = 3; }
// Write the Start Of Image marker. // Write the Start Of Image marker.
this.WriteApplicationHeader(metadata); this.WriteApplicationHeader(metadata);
@ -250,7 +226,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteDefineHuffmanTables(componentCount); this.WriteDefineHuffmanTables(componentCount);
// Write the image data. // Write the image data.
this.WriteStartOfScan(image, cancellationToken); this.WriteStartOfScan(image, componentCount, cancellationToken);
// Write the End Of Image marker. // Write the End Of Image marker.
this.buffer[0] = JpegConstants.Markers.XFF; this.buffer[0] = JpegConstants.Markers.XFF;
@ -468,6 +444,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> /// <summary>
/// Writes the application header containing the JFIF identifier plus extra data. /// Writes the application header containing the JFIF identifier plus extra data.
/// </summary> /// </summary>
@ -898,6 +923,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
switch (this.subsample) switch (this.subsample)
{ {
case JpegSubsample.Grayscale:
subsamples = stackalloc byte[]
{
0x11,
0x00,
0x00
};
break;
case JpegSubsample.Ratio444: case JpegSubsample.Ratio444:
subsamples = stackalloc byte[] subsamples = stackalloc byte[]
{ {
@ -926,26 +959,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[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
this.buffer[5] = (byte)componentCount; this.buffer[5] = (byte)componentCount;
// Number of components (1 byte), usually 1 = Gray scaled, 3 = color YCbCr or YIQ, 4 = color CMYK) for (int i = 0; i < componentCount; i++)
if (componentCount == 1)
{ {
this.buffer[6] = 1; int i3 = 3 * i;
this.buffer[i3 + 6] = (byte)(i + 1);
// No subsampling for grayscale images. this.buffer[i3 + 7] = subsamples[i];
this.buffer[7] = 0x11; this.buffer[i3 + 8] = chroma[i];
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.outputStream.Write(this.buffer, 0, (3 * (componentCount - 1)) + 9); this.outputStream.Write(this.buffer, 0, (3 * (componentCount - 1)) + 9);
@ -956,16 +976,60 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The pixel accessor providing access to the image pixels.</param> /// <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> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
// TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) // TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
// TODO: We should allow grayscale writing. Span<byte> componentId = stackalloc byte[]
this.outputStream.Write(SosHeaderYCbCr); {
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); ref byte emitBufferBase = ref MemoryMarshal.GetReference<byte>(this.emitBuffer);
switch (this.subsample) switch (this.subsample)
{ {
case JpegSubsample.Grayscale:
this.EncodeGrayscale(image, cancellationToken, ref emitBufferBase);
break;
case JpegSubsample.Ratio444: case JpegSubsample.Ratio444:
this.Encode444(image, cancellationToken, ref emitBufferBase); this.Encode444(image, cancellationToken, ref emitBufferBase);
break; break;

7
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. // Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg namespace SixLabors.ImageSharp.Formats.Jpeg
@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
public enum JpegSubsample public enum JpegSubsample
{ {
/// <summary>
/// Only luminance - No chrome channels.
/// </summary>
Grayscale,
/// <summary> /// <summary>
/// High Quality - Each of the three Y'CbCr components have the same sample rate, /// High Quality - Each of the three Y'CbCr components have the same sample rate,
/// thus there is no chroma subsampling. /// thus there is no chroma subsampling.

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

@ -83,6 +83,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality) public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality); where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality);
[Theory]
[WithFile(TestImages.Png.BikeGrayscale, nameof(BitsPerPixel_Quality), PixelTypes.L8)]
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 100, 100, 100, 255, PixelTypes.L8)]
public void EncodeBaseline_GrayscaleWorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality, JpegColorType.Luminance);
[Theory] [Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)]
public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality) public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
@ -101,7 +107,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
: ImageComparer.TolerantPercentage(5f); : ImageComparer.TolerantPercentage(5f);
provider.LimitAllocatorBufferCapacity().InBytesSqrt(200); provider.LimitAllocatorBufferCapacity().InBytesSqrt(200);
TestJpegEncoderCore(provider, subsample, 100, comparer); TestJpegEncoderCore(provider, subsample, 100, JpegColorType.YCbCr, comparer);
} }
/// <summary> /// <summary>
@ -131,6 +137,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImageProvider<TPixel> provider, TestImageProvider<TPixel> provider,
JpegSubsample subsample, JpegSubsample subsample,
int quality = 100, int quality = 100,
JpegColorType colorType = JpegColorType.YCbCr,
ImageComparer comparer = null) ImageComparer comparer = null)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -142,7 +149,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var encoder = new JpegEncoder var encoder = new JpegEncoder
{ {
Subsample = subsample, Subsample = subsample,
Quality = quality Quality = quality,
ColorType = colorType
}; };
string info = $"{subsample}-Q{quality}"; string info = $"{subsample}-Q{quality}";

Loading…
Cancel
Save