diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
index 70a4465121..b5a51c5a4a 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
@@ -38,10 +38,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
private int restartInterval;
- // How many mcu's are left to do.
+ ///
+ /// How many mcu's are left to do.
+ ///
private int todo;
- // The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero.
+ ///
+ /// The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero.
+ ///
private int eobrun;
///
@@ -54,14 +58,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
///
private readonly HuffmanTable[] acHuffmanTables;
- // The unzig data.
+ ///
+ /// The unzig data.
+ ///
private ZigZag dctZigZag;
private HuffmanScanBuffer scanBuffer;
private readonly SpectralConverter spectralConverter;
- private CancellationToken cancellationToken;
+ private readonly CancellationToken cancellationToken;
///
/// Initializes a new instance of the class.
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
index e84d13ff16..23bb01409c 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
+
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
///
@@ -30,5 +32,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// Actual stride height depends on the subsampling factor of the given component.
///
public abstract void ConvertStrideBaseline();
+
+ ///
+ /// Gets the color converter.
+ ///
+ /// The jpeg frame with the color space to convert to.
+ /// The raw JPEG data.
+ /// The color converter.
+ public virtual JpegColorConverter GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverter.GetConverter(jpegData.ColorSpace, frame.Precision);
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
index 50cfa0188a..313a132b81 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
@@ -11,12 +11,12 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
- internal sealed class SpectralConverter : SpectralConverter, IDisposable
+ internal class SpectralConverter : SpectralConverter, IDisposable
where TPixel : unmanaged, IPixel
{
private readonly Configuration configuration;
- private CancellationToken cancellationToken;
+ private readonly CancellationToken cancellationToken;
private JpegComponentPostProcessor[] componentProcessors;
@@ -59,6 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
}
+ ///
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
MemoryAllocator allocator = this.configuration.MemoryAllocator;
@@ -85,9 +86,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.rgbaBuffer = allocator.Allocate(frame.PixelWidth);
// color converter from Rgba32 to TPixel
- this.colorConverter = JpegColorConverter.GetConverter(jpegData.ColorSpace, frame.Precision);
+ this.colorConverter = this.GetColorConverter(frame, jpegData);
}
+ ///
public override void ConvertStrideBaseline()
{
// Convert next pixel stride using single spectral `stride'
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
index 331da275cc..4b74400cac 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
@@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
private int emitLen = 0;
///
- /// Emmited bits 'micro buffer' before being transfered to the .
+ /// Emitted bits 'micro buffer' before being transferred to the .
///
private int accumulatedBits;
@@ -58,18 +58,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
///
private readonly Stream target;
- public HuffmanScanEncoder(Stream outputStream)
- {
- this.target = outputStream;
- }
+ public HuffmanScanEncoder(Stream outputStream) => this.target = outputStream;
///
/// Encodes the image with no subsampling.
///
/// The pixel format.
/// The pixel accessor providing access to the image pixels.
- /// Luminance quantization table provided by the callee
- /// Chrominance quantization table provided by the callee
+ /// Luminance quantization table provided by the callee.
+ /// Chrominance quantization table provided by the callee.
/// The token to monitor for cancellation.
public void Encode444(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
@@ -128,8 +125,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
///
/// The pixel format.
/// The pixel accessor providing access to the image pixels.
- /// Luminance quantization table provided by the callee
- /// Chrominance quantization table provided by the callee
+ /// Luminance quantization table provided by the callee.
+ /// Chrominance quantization table provided by the callee.
/// The token to monitor for cancellation.
public void Encode420(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
@@ -196,7 +193,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
///
/// The pixel format.
/// The pixel accessor providing access to the image pixels.
- /// Luminance quantization table provided by the callee
+ /// Luminance quantization table provided by the callee.
/// The token to monitor for cancellation.
public void EncodeGrayscale(Image pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
@@ -234,6 +231,64 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
this.FlushInternalBuffer();
}
+ ///
+ /// Encodes the image with no subsampling and keeps the pixel data as Rgb24.
+ ///
+ /// The pixel format.
+ /// The pixel accessor providing access to the image pixels.
+ /// Luminance quantization table provided by the callee.
+ /// The token to monitor for cancellation.
+ public void EncodeRgb(Image pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ this.huffmanTables = HuffmanLut.TheHuffmanLut;
+
+ var unzig = ZigZag.CreateUnzigTable();
+
+ // ReSharper disable once InconsistentNaming
+ int prevDCR = 0, prevDCG = 0, prevDCB = 0;
+
+ ImageFrame frame = pixels.Frames.RootFrame;
+ Buffer2D pixelBuffer = frame.PixelBuffer;
+ RowOctet currentRows = default;
+
+ var pixelConverter = new RgbForwardConverter(frame);
+
+ 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(x, y, ref currentRows);
+
+ prevDCR = this.WriteBlock(
+ QuantIndex.Luminance,
+ prevDCR,
+ ref pixelConverter.R,
+ ref luminanceQuantTable,
+ ref unzig);
+
+ prevDCG = this.WriteBlock(
+ QuantIndex.Luminance,
+ prevDCG,
+ ref pixelConverter.G,
+ ref luminanceQuantTable,
+ ref unzig);
+
+ prevDCB = this.WriteBlock(
+ QuantIndex.Luminance,
+ prevDCB,
+ ref pixelConverter.B,
+ ref luminanceQuantTable,
+ ref unzig);
+ }
+ }
+
+ this.FlushInternalBuffer();
+ }
+
///
/// Writes a block of pixel data using the given quantization table,
/// returning the post-quantized DC value of the DCT-transformed block.
@@ -437,7 +492,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
DebugGuard.IsTrue(value <= (1 << 16), "Huffman encoder is supposed to encode a value of 16bit size max");
#if SUPPORTS_BITOPERATIONS
// This should have been implemented as (BitOperations.Log2(value) + 1) as in non-intrinsic implementation
- // But internal log2 is implementated like this: (31 - (int)Lzcnt.LeadingZeroCount(value))
+ // But internal log2 is implemented like this: (31 - (int)Lzcnt.LeadingZeroCount(value))
// BitOperations.Log2 implementation also checks if input value is zero for the convention 0->0
// Lzcnt would return 32 for input value of 0 - no need to check that with branching
@@ -449,7 +504,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
// if 0 - return 0 in this case
// else - return log2(value) + 1
//
- // Hack based on input value constaint:
+ // Hack based on input value constraint:
// We know that input values are guaranteed to be maximum 16 bit large for huffman encoding
// We can safely shift input value for one bit -> log2(value << 1)
// Because of the 16 bit value constraint it won't overflow
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs
index 5eee5dfde4..f9d0fba57f 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs
@@ -1,21 +1,21 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
///
- /// Enumerates the quantization tables
+ /// Enumerates the quantization tables.
///
internal enum QuantIndex
{
///
- /// The luminance quantization table index
+ /// The luminance quantization table index.
///
Luminance = 0,
///
- /// The chrominance quantization table index
+ /// The chrominance quantization table index.
///
Chrominance = 1,
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs
new file mode 100644
index 0000000000..7fad63e117
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
+{
+ ///
+ /// On-stack worker struct to convert TPixel -> Rgb24 of 8x8 pixel blocks.
+ ///
+ /// The pixel type to work on.
+ internal ref struct RgbForwardConverter
+ where TPixel : unmanaged, IPixel
+ {
+ ///
+ /// Number of pixels processed per single call
+ ///
+ private const int PixelsPerSample = 8 * 8;
+
+ ///
+ /// Total byte size of processed pixels converted from TPixel to
+ ///
+ private const int RgbSpanByteSize = PixelsPerSample * 3;
+
+ ///
+ /// of sampling area from given frame pixel buffer.
+ ///
+ private static readonly Size SampleSize = new Size(8, 8);
+
+ ///
+ /// The Red component.
+ ///
+ public Block8x8F R;
+
+ ///
+ /// The Green component.
+ ///
+ public Block8x8F G;
+
+ ///
+ /// The Blue component.
+ ///
+ public Block8x8F B;
+
+ ///
+ /// Temporal 64-byte span to hold unconverted TPixel data.
+ ///
+ private readonly Span pixelSpan;
+
+ ///
+ /// Temporal 64-byte span to hold converted Rgb24 data.
+ ///
+ private readonly Span rgbSpan;
+
+ ///
+ /// Sampled pixel buffer size.
+ ///
+ private readonly Size samplingAreaSize;
+
+ ///
+ /// for internal operations.
+ ///
+ private readonly Configuration config;
+
+ public RgbForwardConverter(ImageFrame frame)
+ {
+ this.R = default;
+ this.G = default;
+ this.B = default;
+
+ // temporal pixel buffers
+ this.pixelSpan = new TPixel[PixelsPerSample].AsSpan();
+ this.rgbSpan = MemoryMarshal.Cast(new byte[RgbSpanByteSize + RgbToYCbCrConverterVectorized.AvxCompatibilityPadding].AsSpan());
+
+ // frame data
+ this.samplingAreaSize = new Size(frame.Width, frame.Height);
+ this.config = frame.GetConfiguration();
+ }
+
+ ///
+ /// Converts a 8x8 image area inside 'pixels' at position (x, y) to Rgb24.
+ ///
+ public void Convert(int x, int y, ref RowOctet currentRows)
+ {
+ YCbCrForwardConverter.LoadAndStretchEdges(currentRows, this.pixelSpan, new Point(x, y), SampleSize, this.samplingAreaSize);
+
+ PixelOperations.Instance.ToRgb24(this.config, this.pixelSpan, this.rgbSpan);
+
+ ref Block8x8F redBlock = ref this.R;
+ ref Block8x8F greenBlock = ref this.G;
+ ref Block8x8F blueBlock = ref this.B;
+
+ CopyToBlock(this.rgbSpan, ref redBlock, ref greenBlock, ref blueBlock);
+ }
+
+ private static void CopyToBlock(Span rgbSpan, ref Block8x8F redBlock, ref Block8x8F greenBlock, ref Block8x8F blueBlock)
+ {
+ ref Rgb24 rgbStart = ref MemoryMarshal.GetReference(rgbSpan);
+
+ for (int i = 0; i < Block8x8F.Size; i++)
+ {
+ Rgb24 c = Unsafe.Add(ref rgbStart, i);
+
+ redBlock[i] = c.R;
+ greenBlock[i] = c.G;
+ blueBlock[i] = c.B;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs
index a4abd532b3..bfeafcbb3d 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs
@@ -58,22 +58,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
///
/// Temporal 16x8 block to hold TPixel data
///
- private Span pixelSpan;
+ private readonly Span pixelSpan;
///
/// Temporal RGB block
///
- private Span rgbSpan;
+ private readonly Span rgbSpan;
///
/// Sampled pixel buffer size
///
- private Size samplingAreaSize;
+ private readonly Size samplingAreaSize;
///
/// for internal operations
///
- private Configuration config;
+ private readonly Configuration config;
public YCbCrForwardConverter420(ImageFrame frame)
{
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs
index ef589272bd..2dbd1a2dc5 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs
@@ -53,22 +53,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
///
/// Temporal 64-byte span to hold unconverted TPixel data
///
- private Span pixelSpan;
+ private readonly Span pixelSpan;
///
/// Temporal 64-byte span to hold converted Rgb24 data
///
- private Span rgbSpan;
+ private readonly Span rgbSpan;
///
/// Sampled pixel buffer size
///
- private Size samplingAreaSize;
+ private readonly Size samplingAreaSize;
///
/// for internal operations
///
- private Configuration config;
+ private readonly Configuration config;
public YCbCrForwardConverter444(ImageFrame frame)
{
diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
index a9f564b450..70cfd18e94 100644
--- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
+++ b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
@@ -16,13 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public int? Quality { get; set; }
///
- /// Gets the subsample ration, that will be used to encode the image.
- ///
- /// The subsample ratio of the jpg image.
- JpegSubsample? Subsample { get; }
-
- ///
- /// Gets the color type.
+ /// Gets the color type, that will be used to encode the image.
///
JpegColorType? ColorType { get; }
}
diff --git a/src/ImageSharp/Formats/Jpeg/JpegColorType.cs b/src/ImageSharp/Formats/Jpeg/JpegColorType.cs
index 73b3215d62..c15038c23b 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegColorType.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegColorType.cs
@@ -10,12 +10,57 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{
///
/// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
+ /// Medium Quality - The horizontal sampling is halved and the Cb and Cr channels are only
+ /// sampled on each alternate line.
///
- YCbCr = 0,
+ YCbCrRatio420 = 0,
+
+ ///
+ /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
+ /// High Quality - Each of the three Y'CbCr components have the same sample rate,
+ /// thus there is no chroma subsampling.
+ ///
+ YCbCrRatio444 = 1,
+
+ ///
+ /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
+ /// The two chroma components are sampled at half the horizontal sample rate of luma while vertically it has full resolution.
+ ///
+ /// Note: Not supported by the encoder.
+ ///
+ YCbCrRatio422 = 2,
+
+ ///
+ /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
+ /// In 4:1:1 chroma subsampling, the horizontal color resolution is quartered.
+ ///
+ /// Note: Not supported by the encoder.
+ ///
+ YCbCrRatio411 = 3,
+
+ ///
+ /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
+ /// This ratio uses half of the vertical and one-fourth the horizontal color resolutions.
+ ///
+ /// Note: Not supported by the encoder.
+ ///
+ YCbCrRatio410 = 4,
///
/// Single channel, luminance.
///
- Luminance = 1
+ Luminance = 5,
+
+ ///
+ /// The pixel data will be preserved as RGB without any sub sampling.
+ ///
+ Rgb = 6,
+
+ ///
+ /// CMYK colorspace (cyan, magenta, yellow, and key black) intended for printing.
+ ///
+ /// Note: Not supported by the encoder.
+ ///
+ Cmyk = 7,
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index 96520c535c..dfef139ab0 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
@@ -4,6 +4,7 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
+using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
@@ -136,8 +137,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Finds the next file marker within the byte stream.
///
- /// The buffer to read file markers to
- /// The input stream
+ /// The buffer to read file markers to.
+ /// The input stream.
/// The
public static JpegFileMarker FindNextFileMarker(byte[] marker, BufferedReadStream stream)
{
@@ -200,6 +201,67 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata);
}
+ ///
+ /// Load quantization and/or Huffman tables for subsequent use for jpeg's embedded in tiff's,
+ /// so those tables do not need to be duplicated with segmented tiff's (tiff's with multiple strips).
+ ///
+ /// The table bytes.
+ /// The scan decoder.
+ public void LoadTables(byte[] tableBytes, HuffmanScanDecoder huffmanScanDecoder)
+ {
+ this.Metadata = new ImageMetadata();
+ this.QuantizationTables = new Block8x8F[4];
+ this.scanDecoder = huffmanScanDecoder;
+ using var ms = new MemoryStream(tableBytes);
+ using var stream = new BufferedReadStream(this.Configuration, ms);
+
+ // Check for the Start Of Image marker.
+ stream.Read(this.markerBuffer, 0, 2);
+ var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0);
+ if (fileMarker.Marker != JpegConstants.Markers.SOI)
+ {
+ JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker.");
+ }
+
+ // Read next marker.
+ stream.Read(this.markerBuffer, 0, 2);
+ byte marker = this.markerBuffer[1];
+ fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2);
+
+ while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid))
+ {
+ if (!fileMarker.Invalid)
+ {
+ // Get the marker length.
+ int remaining = this.ReadUint16(stream) - 2;
+
+ switch (fileMarker.Marker)
+ {
+ case JpegConstants.Markers.SOI:
+ break;
+ case JpegConstants.Markers.RST0:
+ case JpegConstants.Markers.RST7:
+ break;
+ case JpegConstants.Markers.DHT:
+ this.ProcessDefineHuffmanTablesMarker(stream, remaining);
+ break;
+ case JpegConstants.Markers.DQT:
+ this.ProcessDefineQuantizationTablesMarker(stream, remaining);
+ break;
+ case JpegConstants.Markers.DRI:
+ this.ProcessDefineRestartIntervalMarker(stream, remaining);
+ break;
+ case JpegConstants.Markers.EOI:
+ return;
+ }
+ }
+
+ // Read next marker.
+ stream.Read(this.markerBuffer, 0, 2);
+ fileMarker = new JpegFileMarker(this.markerBuffer[1], 0);
+ }
+ }
+
///
/// Parses the input stream for file markers.
///
@@ -225,7 +287,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
stream.Read(this.markerBuffer, 0, 2);
byte marker = this.markerBuffer[1];
fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2);
- this.QuantizationTables = new Block8x8F[4];
+ this.QuantizationTables ??= new Block8x8F[4];
// Break only when we discover a valid EOI marker.
// https://github.com/SixLabors/ImageSharp/issues/695
@@ -236,7 +298,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (!fileMarker.Invalid)
{
- // Get the marker length
+ // Get the marker length.
int remaining = this.ReadUint16(stream) - 2;
switch (fileMarker.Marker)
@@ -272,7 +334,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.SOS:
if (!metadataOnly)
{
- this.ProcessStartOfScanMarker(stream, remaining, cancellationToken);
+ this.ProcessStartOfScanMarker(stream, remaining);
break;
}
else
@@ -371,10 +433,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
///
- /// Returns the correct colorspace based on the image component count and the jpeg frame components.
+ /// Returns the correct colorspace based on the image component count and the jpeg frame component id's.
///
+ /// The number of components.
/// The
- private JpegColorSpace DeduceJpegColorSpace(byte componentCount, JpegComponent[] components)
+ private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
{
if (componentCount == 1)
{
@@ -389,7 +452,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
// If the component Id's are R, G, B in ASCII the colorspace is RGB and not YCbCr.
- if (components[2].Id == 66 && components[1].Id == 71 && components[0].Id == 82)
+ if (this.Components[2].Id == 66 && this.Components[1].Id == 71 && this.Components[0].Id == 82)
{
return JpegColorSpace.RGB;
}
@@ -410,6 +473,64 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return default;
}
+ ///
+ /// Returns the jpeg color type based on the colorspace and subsampling used.
+ ///
+ /// Jpeg color type.
+ private JpegColorType DeduceJpegColorType()
+ {
+ switch (this.ColorSpace)
+ {
+ case JpegColorSpace.Grayscale:
+ return JpegColorType.Luminance;
+
+ case JpegColorSpace.RGB:
+ return JpegColorType.Rgb;
+
+ case JpegColorSpace.YCbCr:
+ if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 &&
+ this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
+ this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
+ {
+ return JpegColorType.YCbCrRatio444;
+ }
+ else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 2 &&
+ this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
+ this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
+ {
+ return JpegColorType.YCbCrRatio420;
+ }
+ else if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 &&
+ this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 2 &&
+ this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 2)
+ {
+ return JpegColorType.YCbCrRatio422;
+ }
+ else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 1 &&
+ this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
+ this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
+ {
+ return JpegColorType.YCbCrRatio411;
+ }
+ else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 2 &&
+ this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
+ this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
+ {
+ return JpegColorType.YCbCrRatio410;
+ }
+ else
+ {
+ return JpegColorType.YCbCrRatio420;
+ }
+
+ case JpegColorSpace.Cmyk:
+ return JpegColorType.Cmyk;
+
+ default:
+ return JpegColorType.YCbCrRatio420;
+ }
+ }
+
///
/// Initializes the EXIF profile.
///
@@ -608,7 +729,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop.
- /// The content of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks.
+ /// The tableBytes of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks.
///
/// The input stream.
/// The remaining bytes in the segment block.
@@ -823,7 +944,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
///
- /// Processes the Start of Frame marker. Specified in section B.2.2.
+ /// Processes the Start of Frame marker. Specified in section B.2.2.
///
/// The input stream.
/// The remaining bytes in the segment block.
@@ -915,9 +1036,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
index += componentBytes;
}
- this.ColorSpace = this.DeduceJpegColorSpace(componentCount, this.Frame.Components);
-
- this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
+ this.ColorSpace = this.DeduceJpegColorSpace(componentCount);
+ this.Metadata.GetJpegMetadata().ColorType = this.DeduceJpegColorType();
if (!metadataOnly)
{
@@ -1015,7 +1135,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Processes the SOS (Start of scan marker).
///
- private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining, CancellationToken cancellationToken)
+ private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)
{
if (this.Frame is null)
{
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
index 5e199b4204..6f116f4fb3 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
@@ -16,14 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
public int? Quality { get; set; }
- ///
- /// Gets or sets the subsample ration, that will be used to encode the image.
- ///
- public JpegSubsample? Subsample { get; set; }
-
- ///
- /// Gets or sets the color type, that will be used to encode the image.
- ///
+ ///
public JpegColorType? ColorType { get; set; }
///
@@ -36,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel
{
var encoder = new JpegEncoderCore(this);
- this.InitializeColorType(image);
+ this.InitializeColorType(image);
encoder.Encode(image, stream);
}
@@ -52,7 +45,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel
{
var encoder = new JpegEncoderCore(this);
- this.InitializeColorType(image);
+ this.InitializeColorType(image);
return encoder.EncodeAsync(image, stream, cancellationToken);
}
@@ -75,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
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;
+ this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCrRatio420;
}
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index 88d96f554d..14e7cc6778 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
@@ -33,20 +33,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
private readonly byte[] buffer = new byte[20];
- ///
- /// Gets or sets the subsampling method to use.
- ///
- private JpegSubsample? subsample;
-
///
/// The quality, that will be used to encode the image.
///
private readonly int? quality;
///
- /// Gets or sets the subsampling method to use.
+ /// Gets or sets the colorspace to use.
///
- private readonly JpegColorType? colorType;
+ private JpegColorType? colorType;
///
/// The output stream. All attempted writes after the first error become no-ops.
@@ -56,12 +51,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Initializes a new instance of the class.
///
- /// The options
+ /// The options.
public JpegEncoderCore(IJpegEncoderOptions options)
{
this.quality = options.Quality;
- this.subsample = options.Subsample;
- this.colorType = options.ColorType;
+
+ if (IsSupportedColorType(options.ColorType))
+ {
+ this.colorType = options.ColorType;
+ }
}
///
@@ -88,49 +86,72 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
ImageMetadata metadata = image.Metadata;
JpegMetadata jpegMetadata = metadata.GetJpegMetadata();
+ // If the color type was not specified by the user, preserve the color type of the input image, if it's a supported color type.
+ if (!this.colorType.HasValue && IsSupportedColorType(jpegMetadata.ColorType))
+ {
+ this.colorType = jpegMetadata.ColorType;
+ }
+
// Compute number of components based on color type in options.
int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3;
+ ReadOnlySpan componentIds = this.GetComponentIds();
// TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that
// Initialize the quantization tables.
this.InitQuantizationTables(componentCount, jpegMetadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable);
// Write the Start Of Image marker.
- this.WriteApplicationHeader(metadata);
+ this.WriteStartOfImage();
+
+ // Do not write APP0 marker for RGB colorspace.
+ if (this.colorType != JpegColorType.Rgb)
+ {
+ this.WriteJfifApplicationHeader(metadata);
+ }
// Write Exif, ICC and IPTC profiles
this.WriteProfiles(metadata);
+ if (this.colorType == JpegColorType.Rgb)
+ {
+ // Write App14 marker to indicate RGB color space.
+ this.WriteApp14Marker();
+ }
+
// Write the quantization tables.
this.WriteDefineQuantizationTables(ref luminanceQuantTable, ref chrominanceQuantTable);
// Write the image dimensions.
- this.WriteStartOfFrame(image.Width, image.Height, componentCount);
+ this.WriteStartOfFrame(image.Width, image.Height, componentCount, componentIds);
// Write the Huffman tables.
this.WriteDefineHuffmanTables(componentCount);
// Write the scan header.
- this.WriteStartOfScan(image, componentCount, cancellationToken);
+ this.WriteStartOfScan(componentCount, componentIds);
// Write the scan compressed data.
var scanEncoder = new HuffmanScanEncoder(stream);
if (this.colorType == JpegColorType.Luminance)
{
- // luminance quantization table only
+ // luminance quantization table only.
scanEncoder.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken);
}
else
{
- // luminance and chrominance quantization tables
- switch (this.subsample)
+ // luminance and chrominance quantization tables.
+ switch (this.colorType)
{
- case JpegSubsample.Ratio444:
+ case JpegColorType.YCbCrRatio444:
+ case JpegColorType.Luminance:
scanEncoder.Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break;
- case JpegSubsample.Ratio420:
+ case JpegColorType.YCbCrRatio420:
scanEncoder.Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break;
+ case JpegColorType.Rgb:
+ scanEncoder.EncodeRgb(image, ref luminanceQuantTable, cancellationToken);
+ break;
}
}
@@ -141,12 +162,32 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
///
- /// Writes data to "Define Quantization Tables" block for QuantIndex
+ /// Returns true, if the color type is supported by the encoder.
///
- /// The "Define Quantization Tables" block
- /// Offset in "Define Quantization Tables" block
- /// The quantization index
- /// The quantization table to copy data from
+ /// The color type.
+ /// true, if color type is supported.
+ private static bool IsSupportedColorType(JpegColorType? colorType)
+ => colorType == JpegColorType.YCbCrRatio444
+ || colorType == JpegColorType.YCbCrRatio420
+ || colorType == JpegColorType.Luminance
+ || colorType == JpegColorType.Rgb;
+
+ ///
+ /// Gets the component ids.
+ /// For color space RGB this will be RGB as ASCII, otherwise 1, 2, 3.
+ ///
+ /// The component Ids.
+ private ReadOnlySpan GetComponentIds() => this.colorType == JpegColorType.Rgb
+ ? new ReadOnlySpan(new byte[] { 82, 71, 66 })
+ : new ReadOnlySpan(new byte[] { 1, 2, 3 });
+
+ ///
+ /// Writes data to "Define Quantization Tables" block for QuantIndex.
+ ///
+ /// The "Define Quantization Tables" block.
+ /// Offset in "Define Quantization Tables" block.
+ /// The quantization index.
+ /// The quantization table to copy data from.
private static void WriteDataToDqt(byte[] dqt, ref int offset, QuantIndex i, ref Block8x8F quant)
{
dqt[offset++] = (byte)i;
@@ -157,52 +198,60 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
///
- /// Writes the application header containing the JFIF identifier plus extra data.
+ /// Write the start of image marker.
///
- /// The image metadata.
- private void WriteApplicationHeader(ImageMetadata meta)
+ private void WriteStartOfImage()
{
- // Write the start of image marker. Markers are always prefixed with 0xff.
+ // Markers are always prefixed with 0xff.
this.buffer[0] = JpegConstants.Markers.XFF;
this.buffer[1] = JpegConstants.Markers.SOI;
+ this.outputStream.Write(this.buffer, 0, 2);
+ }
+
+ ///
+ /// Writes the application header containing the JFIF identifier plus extra data.
+ ///
+ /// The image metadata.
+ private void WriteJfifApplicationHeader(ImageMetadata meta)
+ {
// Write the JFIF headers
- this.buffer[2] = JpegConstants.Markers.XFF;
- this.buffer[3] = JpegConstants.Markers.APP0; // Application Marker
- this.buffer[4] = 0x00;
- this.buffer[5] = 0x10;
- this.buffer[6] = 0x4a; // J
+ this.buffer[0] = JpegConstants.Markers.XFF;
+ this.buffer[1] = JpegConstants.Markers.APP0; // Application Marker
+ this.buffer[2] = 0x00;
+ this.buffer[3] = 0x10;
+ this.buffer[4] = 0x4a; // J
+ this.buffer[5] = 0x46; // F
+ this.buffer[6] = 0x49; // I
this.buffer[7] = 0x46; // F
- this.buffer[8] = 0x49; // I
- this.buffer[9] = 0x46; // F
- this.buffer[10] = 0x00; // = "JFIF",'\0'
- this.buffer[11] = 0x01; // versionhi
- this.buffer[12] = 0x01; // versionlo
+ this.buffer[8] = 0x00; // = "JFIF",'\0'
+ this.buffer[9] = 0x01; // versionhi
+ this.buffer[10] = 0x01; // versionlo
// Resolution. Big Endian
- Span hResolution = this.buffer.AsSpan(14, 2);
- Span vResolution = this.buffer.AsSpan(16, 2);
+ Span hResolution = this.buffer.AsSpan(12, 2);
+ Span vResolution = this.buffer.AsSpan(14, 2);
if (meta.ResolutionUnits == PixelResolutionUnit.PixelsPerMeter)
{
// Scale down to PPI
- this.buffer[13] = (byte)PixelResolutionUnit.PixelsPerInch; // xyunits
+ this.buffer[11] = (byte)PixelResolutionUnit.PixelsPerInch; // xyunits
BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.HorizontalResolution)));
BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.VerticalResolution)));
}
else
{
// We can simply pass the value.
- this.buffer[13] = (byte)meta.ResolutionUnits; // xyunits
+ this.buffer[11] = (byte)meta.ResolutionUnits; // xyunits
BinaryPrimitives.WriteInt16BigEndian(hResolution, (short)Math.Round(meta.HorizontalResolution));
BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(meta.VerticalResolution));
}
// No thumbnail
- this.buffer[18] = 0x00; // Thumbnail width
- this.buffer[19] = 0x00; // Thumbnail height
+ this.buffer[16] = 0x00; // Thumbnail width
+ this.buffer[17] = 0x00; // Thumbnail height
- this.outputStream.Write(this.buffer, 0, 20);
+ this.outputStream.Write(this.buffer, 0, 18);
}
///
@@ -212,7 +261,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private void WriteDefineHuffmanTables(int componentCount)
{
// Table identifiers.
- Span headers = stackalloc byte[]
+ ReadOnlySpan headers = stackalloc byte[]
{
0x00,
0x10,
@@ -249,7 +298,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
private void WriteDefineQuantizationTables(ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable)
{
- // Marker + quantization table lengths
+ // Marker + quantization table lengths.
int markerlen = 2 + (QuantizationTableCount * (1 + Block8x8F.Size));
this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen);
@@ -265,6 +314,35 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream.Write(dqt, 0, dqtCount);
}
+ ///
+ /// Writes the APP14 marker to indicate the image is in RGB color space.
+ ///
+ private void WriteApp14Marker()
+ {
+ this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + AdobeMarker.Length);
+
+ // Identifier: ASCII "Adobe".
+ this.buffer[0] = 0x41;
+ this.buffer[1] = 0x64;
+ this.buffer[2] = 0x6F;
+ this.buffer[3] = 0x62;
+ this.buffer[4] = 0x65;
+
+ // Version, currently 100.
+ BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(5, 2), 100);
+
+ // Flags0
+ BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(7, 2), 0);
+
+ // Flags1
+ BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(9, 2), 0);
+
+ // Transform byte, 0 in combination with three components means the image is in RGB colorspace.
+ this.buffer[11] = 0;
+
+ this.outputStream.Write(this.buffer.AsSpan(0, 12));
+ }
+
///
/// Writes the EXIF profile.
///
@@ -343,7 +421,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
throw new ImageFormatException($"Iptc profile size exceeds limit of {Max} bytes");
}
- var app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length +
+ int app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length +
ProfileResolver.AdobeImageResourceBlockMarker.Length +
ProfileResolver.AdobeIptcMarker.Length +
2 + 4 + data.Length;
@@ -385,7 +463,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// The ICC profile to write.
///
- /// Thrown if any of the ICC profiles size exceeds the limit
+ /// Thrown if any of the ICC profiles size exceeds the limit.
///
private void WriteIccProfile(IccProfile iccProfile)
{
@@ -405,7 +483,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return;
}
- // Calculate the number of markers we'll need, rounding up of course
+ // Calculate the number of markers we'll need, rounding up of course.
int dataLength = data.Length;
int count = dataLength / MaxData;
@@ -478,22 +556,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
///
- /// Writes the Start Of Frame (Baseline) marker
+ /// Writes the Start Of Frame (Baseline) marker.
///
- /// The width of the image
- /// The height of the image
- /// The number of components in a pixel
- private void WriteStartOfFrame(int width, int height, int componentCount)
+ /// The width of the image.
+ /// The height of the image.
+ /// The number of components in a pixel.
+ /// The component Id's.
+ private void WriteStartOfFrame(int width, int height, int componentCount, ReadOnlySpan componentIds)
{
+ // This uses a C#'s compiler optimization that refers to the static data segment of the assembly,
+ // and doesn't incur any allocation at all.
// "default" to 4:2:0
- Span subsamples = stackalloc byte[]
+ ReadOnlySpan subsamples = stackalloc byte[]
{
0x22,
0x11,
0x11
};
- Span chroma = stackalloc byte[]
+ ReadOnlySpan chroma = stackalloc byte[]
{
0x00,
0x01,
@@ -511,17 +592,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
else
{
- switch (this.subsample)
+ switch (this.colorType)
{
- case JpegSubsample.Ratio444:
+ case JpegColorType.YCbCrRatio444:
+ case JpegColorType.Rgb:
subsamples = stackalloc byte[]
{
0x11,
0x11,
0x11
};
+
+ if (this.colorType == JpegColorType.Rgb)
+ {
+ chroma = stackalloc byte[]
+ {
+ 0x00,
+ 0x00,
+ 0x00
+ };
+ }
+
break;
- case JpegSubsample.Ratio420:
+ case JpegColorType.YCbCrRatio420:
subsamples = stackalloc byte[]
{
0x22,
@@ -545,10 +638,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
for (int i = 0; i < componentCount; i++)
{
int i3 = 3 * i;
- this.buffer[i3 + 6] = (byte)(i + 1);
- this.buffer[i3 + 7] = subsamples[i];
- this.buffer[i3 + 8] = chroma[i];
+ // Component ID.
+ Span bufferSpan = this.buffer.AsSpan(i3 + 6, 3);
+ bufferSpan[2] = chroma[i];
+ bufferSpan[1] = subsamples[i];
+ bufferSpan[0] = componentIds[i];
}
this.outputStream.Write(this.buffer, 0, (3 * (componentCount - 1)) + 9);
@@ -557,26 +652,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// Writes the StartOfScan marker.
///
- /// 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, int componentCount, CancellationToken cancellationToken)
- where TPixel : unmanaged, IPixel
+ /// The componentId's.
+ private void WriteStartOfScan(int componentCount, ReadOnlySpan componentIds)
{
- Span componentId = stackalloc byte[]
- {
- 0x01,
- 0x02,
- 0x03
- };
- Span huffmanId = stackalloc byte[]
+ // This uses a C#'s compiler optimization that refers to the static data segment of the assembly,
+ // and doesn't incur any allocation at all.
+ ReadOnlySpan huffmanId = stackalloc byte[]
{
0x00,
0x11,
0x11
};
+ // Use the same DC/AC tables for all channels for RGB.
+ if (this.colorType == JpegColorType.Rgb)
+ {
+ huffmanId = stackalloc byte[]
+ {
+ 0x00,
+ 0x00,
+ 0x00
+ };
+ }
+
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c",
// - the number of components "\x03",
@@ -597,7 +696,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
for (int i = 0; i < componentCount; i++)
{
int i2 = 2 * i;
- this.buffer[i2 + 5] = componentId[i]; // Component Id
+ this.buffer[i2 + 5] = componentIds[i]; // Component Id
this.buffer[i2 + 6] = huffmanId[i]; // DC/AC Huffman table
}
@@ -633,7 +732,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
///
- /// Initializes quntization tables.
+ /// Initializes quantization tables.
///
///
/// We take quality values in a hierarchical order:
@@ -672,9 +771,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
chromaQuality = Numerics.Clamp(chromaQuality, 1, 100);
chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality);
- if (!this.subsample.HasValue)
+ if (!this.colorType.HasValue)
{
- this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420;
+ this.colorType = chromaQuality >= 91 ? JpegColorType.YCbCrRatio444 : JpegColorType.YCbCrRatio420;
}
}
}
diff --git a/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs b/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs
deleted file mode 100644
index 16488f6d21..0000000000
--- a/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Apache License, Version 2.0.
-
-namespace SixLabors.ImageSharp.Formats.Jpeg
-{
- ///
- /// Enumerates the chroma subsampling method applied to the image.
- ///
- public enum JpegSubsample
- {
- ///
- /// High Quality - Each of the three Y'CbCr components have the same sample rate,
- /// thus there is no chroma subsampling.
- ///
- Ratio444,
-
- ///
- /// Medium Quality - The horizontal sampling is halved and the Cb and Cr channels are only
- /// sampled on each alternate line.
- ///
- Ratio420
- }
-}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs
index 30d21e54ce..f456324e53 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs
@@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
private static bool IsReplicateRun(ReadOnlySpan rowSpan, int startPos)
{
// We consider run which has at least 3 same consecutive bytes a candidate for a run.
- var startByte = rowSpan[startPos];
+ byte startByte = rowSpan[startPos];
int count = 0;
for (int i = startPos + 1; i < rowSpan.Length; i++)
{
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs
new file mode 100644
index 0000000000..0ae8fd37bd
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Tiff.Constants;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
+{
+ internal class TiffJpegCompressor : TiffBaseCompressor
+ {
+ public TiffJpegCompressor(Stream output, MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None)
+ : base(output, memoryAllocator, width, bitsPerPixel, predictor)
+ {
+ }
+
+ ///
+ public override TiffCompression Method => TiffCompression.Jpeg;
+
+ ///
+ public override void Initialize(int rowsPerStrip)
+ {
+ }
+
+ ///
+ public override void CompressStrip(Span rows, int height)
+ {
+ int pixelCount = rows.Length / 3;
+ int width = pixelCount / height;
+
+ using var memoryStream = new MemoryStream();
+ var image = Image.LoadPixelData(rows, width, height);
+ image.Save(memoryStream, new JpegEncoder()
+ {
+ ColorType = JpegColorType.Rgb
+ });
+ memoryStream.Position = 0;
+ memoryStream.WriteTo(this.Output);
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
new file mode 100644
index 0000000000..bd1c496b49
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
+using SixLabors.ImageSharp.Formats.Tiff.Constants;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
+{
+ ///
+ /// Class to handle cases where TIFF image data is compressed as a jpeg stream.
+ ///
+ internal sealed class JpegTiffCompression : TiffBaseDecompressor
+ {
+ private readonly Configuration configuration;
+
+ private readonly byte[] jpegTables;
+
+ private readonly TiffPhotometricInterpretation photometricInterpretation;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration.
+ /// The memoryAllocator to use for buffer allocations.
+ /// The image width.
+ /// The bits per pixel.
+ /// The JPEG tables containing the quantization and/or Huffman tables.
+ /// The photometric interpretation.
+ public JpegTiffCompression(
+ Configuration configuration,
+ MemoryAllocator memoryAllocator,
+ int width,
+ int bitsPerPixel,
+ byte[] jpegTables,
+ TiffPhotometricInterpretation photometricInterpretation)
+ : base(memoryAllocator, width, bitsPerPixel)
+ {
+ this.configuration = configuration;
+ this.jpegTables = jpegTables;
+ this.photometricInterpretation = photometricInterpretation;
+ }
+
+ ///
+ protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer)
+ {
+ if (this.jpegTables != null)
+ {
+ using var jpegDecoder = new JpegDecoderCore(this.configuration, new JpegDecoder());
+
+ // TODO: Should we pass through the CancellationToken from the tiff decoder?
+ // If the PhotometricInterpretation is YCbCr we explicitly assume the JPEG data is in RGB color space.
+ // There seems no other way to determine that the JPEG data is RGB colorspace (no APP14 marker, componentId's are not RGB).
+ using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ?
+ new RgbJpegSpectralConverter(this.configuration, CancellationToken.None) : new SpectralConverter(this.configuration, CancellationToken.None);
+ var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None);
+ jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
+ scanDecoder.ResetInterval = 0;
+ jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None);
+
+ using var image = new Image(this.configuration, spectralConverter.PixelBuffer, new ImageMetadata());
+ CopyImageBytesToBuffer(buffer, image);
+ }
+ else
+ {
+ using var image = Image.Load(stream);
+ CopyImageBytesToBuffer(buffer, image);
+ }
+ }
+
+ private static void CopyImageBytesToBuffer(Span buffer, Image image)
+ {
+ int offset = 0;
+ for (int y = 0; y < image.Height; y++)
+ {
+ Span pixelRowSpan = image.GetPixelRowSpan(y);
+ Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan);
+ rgbBytes.CopyTo(buffer.Slice(offset));
+ offset += rgbBytes.Length;
+ }
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
new file mode 100644
index 0000000000..45be3dd038
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.Threading;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
+{
+ ///
+ /// Spectral converter for YCbCr TIFF's which use the JPEG compression.
+ /// The jpeg data should be always treated as RGB color space.
+ ///
+ /// The type of the pixel.
+ internal sealed class RgbJpegSpectralConverter : SpectralConverter
+ where TPixel : unmanaged, IPixel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// This Spectral converter will always convert the pixel data to RGB color.
+ ///
+ /// The configuration.
+ /// The cancellation token.
+ public RgbJpegSpectralConverter(Configuration configuration, CancellationToken cancellationToken)
+ : base(configuration, cancellationToken)
+ {
+ }
+
+ ///
+ public override JpegColorConverter GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverter.GetConverter(JpegColorSpace.RGB, frame.Precision);
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs
index 14a0c6e9d7..db2b935b74 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs
@@ -25,7 +25,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
// The following compression types are not implemented in the encoder and will default to no compression instead.
case TiffCompression.ItuTRecT43:
case TiffCompression.ItuTRecT82:
- case TiffCompression.Jpeg:
case TiffCompression.OldJpeg:
case TiffCompression.OldDeflate:
case TiffCompression.None:
@@ -34,6 +33,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
return new NoCompressor(output, allocator, width, bitsPerPixel);
+ case TiffCompression.Jpeg:
+ DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set");
+ DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
+ return new TiffJpegCompressor(output, allocator, width, bitsPerPixel);
+
case TiffCompression.PackBits:
DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set");
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
index 61b691eb84..d8843c1078 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
@@ -36,11 +36,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
///
/// Image data is compressed using CCITT T.6 fax compression.
///
- T6 = 6,
+ T6 = 5,
///
/// Image data is compressed using modified huffman compression.
///
- HuffmanRle = 5,
+ HuffmanRle = 6,
+
+ ///
+ /// The image data is compressed as a JPEG stream.
+ ///
+ Jpeg = 7,
}
}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
index 735ea1aa2a..a7e2e276bd 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
@@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
internal static class TiffDecompressorsFactory
{
public static TiffBaseDecompressor Create(
+ Configuration configuration,
TiffDecoderCompressionType method,
MemoryAllocator allocator,
TiffPhotometricInterpretation photometricInterpretation,
@@ -19,6 +20,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
TiffColorType colorType,
TiffPredictor predictor,
FaxCompressionOptions faxOptions,
+ byte[] jpegTables,
TiffFillOrder fillOrder,
ByteOrder byteOrder)
{
@@ -54,6 +56,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation);
+ case TiffDecoderCompressionType.Jpeg:
+ DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
+ return new JpegTiffCompression(configuration, allocator, width, bitsPerPixel, jpegTables, photometricInterpretation);
+
default:
throw TiffThrowHelper.NotSupportedDecompressor(nameof(method));
}
diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md
index beac42db7c..ff4aa52b0c 100644
--- a/src/ImageSharp/Formats/Tiff/README.md
+++ b/src/ImageSharp/Formats/Tiff/README.md
@@ -48,7 +48,7 @@
|CcittGroup4Fax | | Y | |
|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case |
|Old Jpeg | | | We should not even try to support this |
-|Jpeg (Technote 2) | | | |
+|Jpeg (Technote 2) | Y | Y | |
|Deflate (Technote 2) | Y | Y | Based on PNG Deflate. |
|Old Deflate (Technote 2) | | Y | |
diff --git a/src/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf b/src/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf
new file mode 100644
index 0000000000..e4822d4093
Binary files /dev/null and b/src/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf differ
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index ff5f8923e3..55af87005a 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -105,6 +105,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
public TiffFillOrder FillOrder { get; set; }
+ ///
+ /// Gets or sets the JPEG tables when jpeg compression is used.
+ ///
+ public byte[] JpegTables { get; set; }
+
///
/// Gets or sets the planar configuration type to use when decoding the image.
///
@@ -144,7 +149,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
var frames = new List>();
foreach (ExifProfile ifd in directories)
{
- ImageFrame frame = this.DecodeFrame(ifd);
+ cancellationToken.ThrowIfCancellationRequested();
+ ImageFrame frame = this.DecodeFrame(ifd, cancellationToken);
frames.Add(frame);
}
@@ -186,10 +192,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
/// The pixel format.
/// The IFD tags.
- ///
- /// The tiff frame.
- ///
- private ImageFrame DecodeFrame(ExifProfile tags)
+ /// The token to monitor cancellation.
+ /// The tiff frame.
+ private ImageFrame DecodeFrame(ExifProfile tags, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ?
@@ -211,11 +216,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar)
{
- this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts);
+ this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken);
}
else
{
- this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts);
+ this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken);
}
return frame;
@@ -263,14 +268,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
///
- /// Decodes the image data for strip encoded data.
+ /// Decodes the image data for planar encoded pixel data.
///
/// The pixel format.
/// The image frame to decode data into.
/// The number of rows per strip of data.
/// An array of byte offsets to each strip in the image.
/// An array of the size of each strip (in bytes).
- private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts)
+ /// The token to monitor cancellation.
+ private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int stripsPerPixel = this.BitsPerSample.Channels;
@@ -290,6 +296,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(
+ this.Configuration,
this.CompressionType,
this.memoryAllocator,
this.PhotometricInterpretation,
@@ -298,6 +305,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.ColorType,
this.Predictor,
this.FaxCompressionOptions,
+ this.JpegTables,
this.FillOrder,
this.byteOrder);
@@ -312,6 +320,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
for (int i = 0; i < stripsPerPlane; i++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip;
int stripIndex = i;
@@ -338,7 +348,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
}
- private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts)
+ ///
+ /// Decodes the image data for chunky encoded pixel data.
+ ///
+ /// The pixel format.
+ /// The image frame to decode data into.
+ /// The rows per strip.
+ /// The strip offsets.
+ /// The strip byte counts.
+ /// The token to monitor cancellation.
+ private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip.
@@ -355,6 +374,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
Buffer2D pixels = frame.PixelBuffer;
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(
+ this.Configuration,
this.CompressionType,
this.memoryAllocator,
this.PhotometricInterpretation,
@@ -363,6 +383,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.ColorType,
this.Predictor,
this.FaxCompressionOptions,
+ this.JpegTables,
this.FillOrder,
this.byteOrder);
@@ -379,6 +400,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0
? rowsPerStrip
: frame.Height % rowsPerStrip;
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
index d8357d945f..a8420fabba 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
@@ -87,6 +87,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value;
options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value;
options.FillOrder = fillOrder;
+ options.JpegTables = exifProfile.GetValue(ExifTag.JPEGTables)?.Value;
options.ParseColorType(exifProfile);
options.ParseCompression(frameMetadata.Compression, exifProfile);
@@ -432,6 +433,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
break;
}
+ case TiffCompression.Jpeg:
+ {
+ options.CompressionType = TiffDecoderCompressionType.Jpeg;
+ break;
+ }
+
default:
{
TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported");
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index d7c9848a44..1e4254a4ef 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
@@ -155,6 +155,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
Image metadataImage = image;
foreach (ImageFrame frame in image.Frames)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile?.GetValue(ExifTag.SubfileType)?.Value ?? (int)TiffNewSubfileType.FullImage);
ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker);
@@ -223,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
entriesCollector,
(int)this.BitsPerPixel);
- int rowsPerStrip = this.CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow);
+ int rowsPerStrip = this.CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType);
colorWriter.Write(compressor, rowsPerStrip);
@@ -245,13 +247,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
/// The height of the image.
/// The number of bytes per row.
+ /// The compression used.
/// Number of rows per strip.
- private int CalcRowsPerStrip(int height, int bytesPerRow)
+ private int CalcRowsPerStrip(int height, int bytesPerRow, TiffCompression? compression)
{
DebugGuard.MustBeGreaterThan(height, 0, nameof(height));
DebugGuard.MustBeGreaterThan(bytesPerRow, 0, nameof(bytesPerRow));
- int rowsPerStrip = TiffConstants.DefaultStripSize / bytesPerRow;
+ // Jpeg compressed images should be written in one strip.
+ if (compression is TiffCompression.Jpeg)
+ {
+ return height;
+ }
+
+ // If compression is used, change stripSizeInBytes heuristically to a larger value to not write to many strips.
+ int stripSizeInBytes = compression is TiffCompression.Deflate || compression is TiffCompression.Lzw ? TiffConstants.DefaultStripSize * 2 : TiffConstants.DefaultStripSize;
+ int rowsPerStrip = stripSizeInBytes / bytesPerRow;
if (rowsPerStrip > 0)
{
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
index 15694978fc..55dd7d3973 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
@@ -396,6 +396,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
case TiffCompression.Ccitt1D:
return (ushort)TiffCompression.Ccitt1D;
+
+ case TiffCompression.Jpeg:
+ return (ushort)TiffCompression.Jpeg;
}
return (ushort)TiffCompression.None;
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs
index 87170e8d24..508b4b3b09 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs
@@ -40,8 +40,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
this.bmpCore = Image.Load(this.bmpStream);
this.bmpCore.Metadata.ExifProfile = null;
- this.encoder420 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio420 };
- this.encoder444 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio444 };
+ this.encoder420 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio420 };
+ this.encoder444 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio444 };
this.bmpStream.Position = 0;
this.bmpDrawing = SDImage.FromStream(this.bmpStream);
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
index 403eeaf908..e5f8989c5c 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
@@ -67,16 +67,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
string imagePath,
int expectedPixelSize,
bool exifProfilePresent,
- bool iccProfilePresent)
- {
- TestMetadataImpl(
+ bool iccProfilePresent) => TestMetadataImpl(
useIdentify,
JpegDecoder,
imagePath,
expectedPixelSize,
exifProfilePresent,
iccProfilePresent);
- }
[Theory]
[MemberData(nameof(RatioFiles))]
@@ -133,8 +130,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
- var decoder = new JpegDecoder();
- using (Image image = decoder.Decode(Configuration.Default, stream))
+ using (Image image = JpegDecoder.Decode(Configuration.Default, stream))
{
JpegMetadata meta = image.Metadata.GetJpegMetadata();
Assert.Equal(quality, meta.Quality);
@@ -142,6 +138,42 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
}
+ [Theory]
+ [InlineData(TestImages.Jpeg.Baseline.Floorplan, JpegColorType.Luminance)]
+ [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, JpegColorType.YCbCrRatio420)]
+ [InlineData(TestImages.Jpeg.Baseline.Jpeg444, JpegColorType.YCbCrRatio444)]
+ [InlineData(TestImages.Jpeg.Baseline.JpegRgb, JpegColorType.Rgb)]
+ [InlineData(TestImages.Jpeg.Baseline.Cmyk, JpegColorType.Cmyk)]
+ [InlineData(TestImages.Jpeg.Baseline.Jpeg410, JpegColorType.YCbCrRatio410)]
+ [InlineData(TestImages.Jpeg.Baseline.Jpeg422, JpegColorType.YCbCrRatio422)]
+ [InlineData(TestImages.Jpeg.Baseline.Jpeg411, JpegColorType.YCbCrRatio411)]
+ public void Identify_DetectsCorrectColorType(string imagePath, JpegColorType expectedColorType)
+ {
+ var testFile = TestFile.Create(imagePath);
+ using (var stream = new MemoryStream(testFile.Bytes, false))
+ {
+ IImageInfo image = JpegDecoder.Identify(Configuration.Default, stream);
+ JpegMetadata meta = image.Metadata.GetJpegMetadata();
+ Assert.Equal(expectedColorType, meta.ColorType);
+ }
+ }
+
+ [Theory]
+ [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32, JpegColorType.Luminance)]
+ [WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgba32, JpegColorType.YCbCrRatio420)]
+ [WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)]
+ [WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgba32, JpegColorType.Rgb)]
+ [WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgba32, JpegColorType.Cmyk)]
+ public void Decode_DetectsCorrectColorType(TestImageProvider provider, JpegColorType expectedColorType)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image image = provider.GetImage(JpegDecoder))
+ {
+ JpegMetadata meta = image.Metadata.GetJpegMetadata();
+ Assert.Equal(expectedColorType, meta.ColorType);
+ }
+ }
+
private static void TestImageInfo(string imagePath, IImageDecoder decoder, bool useIdentify, Action test)
{
var testFile = TestFile.Create(imagePath);
@@ -161,9 +193,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
string imagePath,
int expectedPixelSize,
bool exifProfilePresent,
- bool iccProfilePresent)
- {
- TestImageInfo(
+ bool iccProfilePresent) => TestImageInfo(
imagePath,
decoder,
useIdentify,
@@ -207,7 +237,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Null(iccProfile);
}
});
- }
[Theory]
[InlineData(false)]
@@ -237,9 +266,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory]
[InlineData(false)]
[InlineData(true)]
- public void Decoder_Reads_Correct_Resolution_From_Jfif(bool useIdentify)
- {
- TestImageInfo(
+ public void Decoder_Reads_Correct_Resolution_From_Jfif(bool useIdentify) => TestImageInfo(
TestImages.Jpeg.Baseline.Floorplan,
JpegDecoder,
useIdentify,
@@ -248,14 +275,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(300, imageInfo.Metadata.HorizontalResolution);
Assert.Equal(300, imageInfo.Metadata.VerticalResolution);
});
- }
[Theory]
[InlineData(false)]
[InlineData(true)]
- public void Decoder_Reads_Correct_Resolution_From_Exif(bool useIdentify)
- {
- TestImageInfo(
+ public void Decoder_Reads_Correct_Resolution_From_Exif(bool useIdentify) => TestImageInfo(
TestImages.Jpeg.Baseline.Jpeg420Exif,
JpegDecoder,
useIdentify,
@@ -264,6 +288,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(72, imageInfo.Metadata.HorizontalResolution);
Assert.Equal(72, imageInfo.Metadata.VerticalResolution);
});
- }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index 6b1ce19e49..2a18a2c103 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
@@ -193,7 +193,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public void Issue1732_DecodesWithRgbColorSpace(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
- using (Image image = provider.GetImage(new JpegDecoder()))
+ using (Image image = provider.GetImage(JpegDecoder))
{
image.DebugSave(provider);
image.CompareToOriginal(provider);
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
index 8e12b04be9..2bd2961de3 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
@@ -24,6 +24,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Trait("Format", "Jpg")]
public class JpegEncoderTests
{
+ private static JpegEncoder JpegEncoder => new JpegEncoder();
+
+ private static JpegDecoder JpegDecoder => new JpegDecoder();
+
public static readonly TheoryData QualityFiles =
new TheoryData
{
@@ -31,15 +35,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ TestImages.Jpeg.Progressive.Fb, 75 }
};
- public static readonly TheoryData BitsPerPixel_Quality =
- new TheoryData
+ public static readonly TheoryData BitsPerPixel_Quality =
+ new TheoryData
{
- { JpegSubsample.Ratio420, 40 },
- { JpegSubsample.Ratio420, 60 },
- { JpegSubsample.Ratio420, 100 },
- { JpegSubsample.Ratio444, 40 },
- { JpegSubsample.Ratio444, 60 },
- { JpegSubsample.Ratio444, 100 },
+ { JpegColorType.YCbCrRatio420, 40 },
+ { JpegColorType.YCbCrRatio420, 60 },
+ { JpegColorType.YCbCrRatio420, 100 },
+ { JpegColorType.YCbCrRatio444, 40 },
+ { JpegColorType.YCbCrRatio444, 60 },
+ { JpegColorType.YCbCrRatio444, 100 },
+ { JpegColorType.Rgb, 40 },
+ { JpegColorType.Rgb, 60 },
+ { JpegColorType.Rgb, 100 }
};
public static readonly TheoryData Grayscale_Quality =
@@ -59,17 +66,84 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
};
[Theory]
- [MemberData(nameof(QualityFiles))]
- public void Encode_PreserveQuality(string imagePath, int quality)
+ [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32, JpegColorType.Luminance)]
+ [WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)]
+ [WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgba32, JpegColorType.YCbCrRatio420)]
+ [WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgba32, JpegColorType.Rgb)]
+ public void Encode_PreservesColorType(TestImageProvider provider, JpegColorType expectedColorType)
+ where TPixel : unmanaged, IPixel
+ {
+ // arrange
+ using Image input = provider.GetImage(JpegDecoder);
+ using var memoryStream = new MemoryStream();
+
+ // act
+ input.Save(memoryStream, JpegEncoder);
+
+ // assert
+ memoryStream.Position = 0;
+ using var output = Image.Load(memoryStream);
+ JpegMetadata meta = output.Metadata.GetJpegMetadata();
+ Assert.Equal(expectedColorType, meta.ColorType);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Jpeg.Baseline.Jpeg410, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Jpeg.Baseline.Jpeg411, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Jpeg.Baseline.Jpeg422, PixelTypes.Rgba32)]
+ public void Encode_WithUnsupportedColorType_FromInputImage_DefaultsToYCbCr420(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ // arrange
+ using Image input = provider.GetImage(JpegDecoder);
+ using var memoryStream = new MemoryStream();
+
+ // act
+ input.Save(memoryStream, new JpegEncoder()
+ {
+ Quality = 75
+ });
+
+ // assert
+ memoryStream.Position = 0;
+ using var output = Image.Load(memoryStream);
+ JpegMetadata meta = output.Metadata.GetJpegMetadata();
+ Assert.Equal(JpegColorType.YCbCrRatio420, meta.ColorType);
+ }
+
+ [Theory]
+ [InlineData(JpegColorType.Cmyk)]
+ [InlineData(JpegColorType.YCbCrRatio410)]
+ [InlineData(JpegColorType.YCbCrRatio411)]
+ [InlineData(JpegColorType.YCbCrRatio422)]
+ public void Encode_WithUnsupportedColorType_DefaultsToYCbCr420(JpegColorType colorType)
{
- var options = new JpegEncoder();
+ // arrange
+ var jpegEncoder = new JpegEncoder() { ColorType = colorType };
+ using var input = new Image(10, 10);
+ using var memoryStream = new MemoryStream();
+
+ // act
+ input.Save(memoryStream, jpegEncoder);
+ // assert
+ memoryStream.Position = 0;
+ using var output = Image.Load(memoryStream);
+ JpegMetadata meta = output.Metadata.GetJpegMetadata();
+ Assert.Equal(JpegColorType.YCbCrRatio420, meta.ColorType);
+ }
+
+ [Theory]
+ [MemberData(nameof(QualityFiles))]
+ public void Encode_PreservesQuality(string imagePath, int quality)
+ {
var testFile = TestFile.Create(imagePath);
using (Image input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
- input.Save(memStream, options);
+ input.Save(memStream, JpegEncoder);
memStream.Position = 0;
using (var output = Image.Load(memStream))
@@ -83,16 +157,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)]
+ public void EncodeBaseline_CalliphoraPartial(TestImageProvider provider, JpegColorType colorType, int quality)
+ where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality);
+
+ [Theory]
+ [WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)]
+ [WithTestPatternImages(nameof(BitsPerPixel_Quality), 158, 24, PixelTypes.Rgba32)]
+ [WithTestPatternImages(nameof(BitsPerPixel_Quality), 153, 21, PixelTypes.Rgba32)]
+ [WithTestPatternImages(nameof(BitsPerPixel_Quality), 600, 400, PixelTypes.Rgba32)]
+ [WithTestPatternImages(nameof(BitsPerPixel_Quality), 138, 24, PixelTypes.Rgba32)]
+ public void EncodeBaseline_WorksWithDifferentSizes(TestImageProvider provider, JpegColorType colorType, int quality)
+ where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality);
+
+ [Theory]
+ [WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 100, 100, 100, 255, PixelTypes.L8)]
+ [WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)]
+ [WithTestPatternImages(nameof(BitsPerPixel_Quality), 143, 81, PixelTypes.Rgba32)]
+ [WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)]
+ [WithTestPatternImages(nameof(BitsPerPixel_Quality), 96, 48, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 73, 71, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 24, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 46, 8, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 51, 7, PixelTypes.Rgba32)]
- [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);
+ public void EncodeBaseline_WithSmallImages_WorksWithDifferentSizes(TestImageProvider provider, JpegColorType colorType, int quality)
+ where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.12f));
[Theory]
[WithFile(TestImages.Png.BikeGrayscale, nameof(Grayscale_Quality), PixelTypes.L8)]
@@ -102,46 +190,51 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[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);
+ where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, JpegColorType.Luminance, quality);
+
+ [Theory]
+ [WithTestPatternImages(nameof(BitsPerPixel_Quality), 96, 96, PixelTypes.Rgba32 | PixelTypes.Bgra32)]
+ public void EncodeBaseline_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegColorType colorType, int quality)
+ where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality);
[Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)]
- public void EncodeBaseline_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegSubsample subsample, int quality)
- where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, subsample, quality);
+ public void EncodeBaseline_WithSmallImages_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegColorType colorType, int quality)
+ where TPixel : unmanaged, IPixel => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.06f));
[Theory]
- [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, JpegSubsample.Ratio444)]
- [WithTestPatternImages(587, 821, PixelTypes.Rgba32, JpegSubsample.Ratio444)]
- [WithTestPatternImages(677, 683, PixelTypes.Bgra32, JpegSubsample.Ratio420)]
- [WithSolidFilledImages(400, 400, "Red", PixelTypes.Bgr24, JpegSubsample.Ratio420)]
- public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvider provider, JpegSubsample subsample)
+ [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)]
+ [WithTestPatternImages(587, 821, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)]
+ [WithTestPatternImages(677, 683, PixelTypes.Bgra32, JpegColorType.YCbCrRatio420)]
+ [WithSolidFilledImages(400, 400, "Red", PixelTypes.Bgr24, JpegColorType.YCbCrRatio420)]
+ public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvider provider, JpegColorType colorType)
where TPixel : unmanaged, IPixel
{
- ImageComparer comparer = subsample == JpegSubsample.Ratio444
+ ImageComparer comparer = colorType == JpegColorType.YCbCrRatio444
? ImageComparer.TolerantPercentage(0.1f)
: ImageComparer.TolerantPercentage(5f);
provider.LimitAllocatorBufferCapacity().InBytesSqrt(200);
- TestJpegEncoderCore(provider, subsample, 100, JpegColorType.YCbCr, comparer);
+ TestJpegEncoderCore(provider, colorType, 100, comparer);
}
///
/// Anton's SUPER-SCIENTIFIC tolerance threshold calculation
///
- private static ImageComparer GetComparer(int quality, JpegSubsample? subsample)
+ private static ImageComparer GetComparer(int quality, JpegColorType? colorType)
{
float tolerance = 0.015f; // ~1.5%
if (quality < 50)
{
- tolerance *= 10f;
+ tolerance *= 4.5f;
}
- else if (quality < 75 || subsample == JpegSubsample.Ratio420)
+ else if (quality < 75 || colorType == JpegColorType.YCbCrRatio420)
{
- tolerance *= 5f;
- if (subsample == JpegSubsample.Ratio420)
+ tolerance *= 2.0f;
+ if (colorType == JpegColorType.YCbCrRatio420)
{
- tolerance *= 2f;
+ tolerance *= 2.0f;
}
}
@@ -150,9 +243,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private static void TestJpegEncoderCore(
TestImageProvider provider,
- JpegSubsample? subsample,
+ JpegColorType colorType = JpegColorType.YCbCrRatio420,
int quality = 100,
- JpegColorType colorType = JpegColorType.YCbCr,
ImageComparer comparer = null)
where TPixel : unmanaged, IPixel
{
@@ -163,13 +255,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var encoder = new JpegEncoder
{
- Subsample = subsample,
Quality = quality,
ColorType = colorType
};
- string info = $"{subsample}-Q{quality}";
+ string info = $"{colorType}-Q{quality}";
- comparer ??= GetComparer(quality, subsample);
+ comparer ??= GetComparer(quality, colorType);
// Does DebugSave & load reference CompareToReferenceInput():
image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "png");
@@ -225,14 +316,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
- var options = new JpegEncoder();
-
var testFile = TestFile.Create(imagePath);
using (Image input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
- input.Save(memStream, options);
+ input.Save(memStream, JpegEncoder);
memStream.Position = 0;
using (var output = Image.Load(memStream))
@@ -253,11 +342,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var input = new Image(1, 1);
input.Metadata.IptcProfile = new IptcProfile();
input.Metadata.IptcProfile.SetValue(IptcTag.Byline, "unit_test");
- var encoder = new JpegEncoder();
// act
using var memStream = new MemoryStream();
- input.Save(memStream, encoder);
+ input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
@@ -275,11 +363,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var input = new Image(1, 1);
input.Metadata.ExifProfile = new ExifProfile();
input.Metadata.ExifProfile.SetValue(ExifTag.Software, "unit_test");
- var encoder = new JpegEncoder();
// act
using var memStream = new MemoryStream();
- input.Save(memStream, encoder);
+ input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
@@ -296,11 +383,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// arrange
using var input = new Image(1, 1);
input.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.Profile_Random_Array);
- var encoder = new JpegEncoder();
// act
using var memStream = new MemoryStream();
- input.Save(memStream, encoder);
+ input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
@@ -312,9 +398,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
[Theory]
- [InlineData(JpegSubsample.Ratio420)]
- [InlineData(JpegSubsample.Ratio444)]
- public async Task Encode_IsCancellable(JpegSubsample subsample)
+ [InlineData(JpegColorType.YCbCrRatio420)]
+ [InlineData(JpegColorType.YCbCrRatio444)]
+ public async Task Encode_IsCancellable(JpegColorType colorType)
{
var cts = new CancellationTokenSource();
using var pausedStream = new PausedStream(new MemoryStream());
@@ -336,7 +422,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var image = new Image(5000, 5000);
await Assert.ThrowsAsync(async () =>
{
- var encoder = new JpegEncoder() { Subsample = subsample };
+ var encoder = new JpegEncoder() { ColorType = colorType };
await image.SaveAsync(pausedStream, encoder, cts.Token);
});
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs
index 56bf207b97..3f045dd1a0 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs
@@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var clone = (JpegMetadata)meta.DeepClone();
clone.Quality = 99;
- clone.ColorType = JpegColorType.YCbCr;
+ clone.ColorType = JpegColorType.YCbCrRatio420;
Assert.False(meta.Quality.Equals(clone.Quality));
Assert.False(meta.ColorType.Equals(clone.ColorType));
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
index 40b9e68677..1785f3dec4 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
@@ -21,10 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Trait("Format", "Jpg")]
public class SpectralJpegTests
{
- public SpectralJpegTests(ITestOutputHelper output)
- {
- this.Output = output;
- }
+ public SpectralJpegTests(ITestOutputHelper output) => this.Output = output;
private ITestOutputHelper Output { get; }
@@ -46,7 +43,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public static readonly string[] AllTestJpegs = BaselineTestJpegs.Concat(ProgressiveTestJpegs).ToArray();
[Theory(Skip = "Debug only, enable manually!")]
- //[Theory]
[WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)]
public void Decoder_ParseStream_SaveSpectralResult(TestImageProvider provider)
where TPixel : unmanaged, IPixel
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index 290b992c25..b64d22991f 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -28,8 +28,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
private static MagickReferenceDecoder ReferenceDecoder => new MagickReferenceDecoder();
[Theory]
- [WithFile(Calliphora_RgbJpeg, PixelTypes.Rgba32)]
- [WithFile(RgbJpeg, PixelTypes.Rgba32)]
[WithFile(RgbUncompressedTiled, PixelTypes.Rgba32)]
[WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)]
[WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)]
@@ -39,7 +37,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Theory]
[InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)]
[InlineData(SmallRgbDeflate, 24, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)]
- [InlineData(Calliphora_GrayscaleUncompressed, 8, 804, 1198, 96, 96, PixelResolutionUnit.PixelsPerInch)]
+ [InlineData(Calliphora_GrayscaleUncompressed, 8, 200, 298, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(Flower4BitPalette, 4, 73, 43, 72, 72, PixelResolutionUnit.PixelsPerInch)]
public void Identify(string imagePath, int expectedPixelSize, int expectedWidth, int expectedHeight, double expectedHResolution, double expectedVResolution, PixelResolutionUnit expectedResolutionUnit)
{
@@ -377,6 +375,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffDecoder_CanDecode_PackBitsCompressed(TestImageProvider provider)
where TPixel : unmanaged, IPixel => TestTiffDecoder(provider);
+ [Theory]
+ [WithFile(RgbJpegCompressed, PixelTypes.Rgba32)]
+ [WithFile(RgbWithStripsJpegCompressed, PixelTypes.Rgba32)]
+ [WithFile(YCbCrJpegCompressed, PixelTypes.Rgba32)]
+ [WithFile(RgbJpegCompressedNoJpegTable, PixelTypes.Rgba32)]
+ public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false);
+
[Theory]
[WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)]
public void DecodeMultiframe(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
index 712c8502ab..d7333c0b5a 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
@@ -114,7 +114,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Ccitt1D, TiffBitsPerPixel.Bit1, TiffCompression.Ccitt1D)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT43, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT82, TiffBitsPerPixel.Bit24, TiffCompression.None)]
- [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)]
+ [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.Jpeg)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldDeflate, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldJpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)]
public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(
@@ -288,6 +288,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works(TestImageProvider provider)
where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.PackBits);
+ [Theory]
+ [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)]
+ public void TiffEncoder_EncodeRgb_WithJpegCompression_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, useExactComparer: false, compareTolerance: 0.012f);
+
[Theory]
[WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeGray_Works(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
index 9de3fc8dfe..4a47ac2365 100644
--- a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
+++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
@@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
img.Dispose();
},
#pragma warning disable SA1515 // Single-line comment should be preceded by blank line
- // ReSharper disable once ExplicitCallerInfoArgument
+ // ReSharper disable once ExplicitCallerInfoArgument
$"Decode {fileName}");
#pragma warning restore SA1515 // Single-line comment should be preceded by blank line
}
@@ -92,11 +92,11 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
// Benchmark, enable manually!
[Theory(Skip = ProfilingSetup.SkipProfilingTests)]
- [InlineData(1, 75, JpegSubsample.Ratio420)]
- [InlineData(30, 75, JpegSubsample.Ratio420)]
- [InlineData(30, 75, JpegSubsample.Ratio444)]
- [InlineData(30, 100, JpegSubsample.Ratio444)]
- public void EncodeJpeg(int executionCount, int quality, JpegSubsample subsample)
+ [InlineData(1, 75, JpegColorType.YCbCrRatio420)]
+ [InlineData(30, 75, JpegColorType.YCbCrRatio420)]
+ [InlineData(30, 75, JpegColorType.YCbCrRatio444)]
+ [InlineData(30, 100, JpegColorType.YCbCrRatio444)]
+ public void EncodeJpeg(int executionCount, int quality, JpegColorType colorType)
{
// do not run this on CI even by accident
if (TestEnvironment.RunsOnCI)
@@ -118,7 +118,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{
foreach (Image img in testImages)
{
- var options = new JpegEncoder { Quality = quality, Subsample = subsample };
+ var options = new JpegEncoder { Quality = quality, ColorType = colorType };
img.Save(ms, options);
ms.Seek(0, SeekOrigin.Begin);
}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index de2f2b64ee..d1a6624af2 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -190,6 +190,10 @@ namespace SixLabors.ImageSharp.Tests
public const string Jpeg420Exif = "Jpg/baseline/jpeg420exif.jpg";
public const string Jpeg444 = "Jpg/baseline/jpeg444.jpg";
public const string Jpeg420Small = "Jpg/baseline/jpeg420small.jpg";
+ public const string JpegRgb = "Jpg/baseline/jpeg-rgb.jpg";
+ public const string Jpeg410 = "Jpg/baseline/jpeg410.jpg";
+ public const string Jpeg411 = "Jpg/baseline/jpeg411.jpg";
+ public const string Jpeg422 = "Jpg/baseline/jpeg422.jpg";
public const string Testorig420 = "Jpg/baseline/testorig.jpg";
public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg";
public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.jpg";
@@ -558,7 +562,9 @@ namespace SixLabors.ImageSharp.Tests
public const string RgbDeflate = "Tiff/rgb_deflate.tiff";
public const string RgbDeflatePredictor = "Tiff/rgb_deflate_predictor.tiff";
public const string RgbDeflateMultistrip = "Tiff/rgb_deflate_multistrip.tiff";
- public const string RgbJpeg = "Tiff/rgb_jpeg.tiff";
+ public const string RgbJpegCompressed = "Tiff/rgb_jpegcompression.tiff";
+ public const string RgbWithStripsJpegCompressed = "Tiff/rgb_jpegcompressed_stripped.tiff";
+ public const string RgbJpegCompressedNoJpegTable = "Tiff/rgb_jpegcompressed_nojpegtable.tiff";
public const string RgbLzwPredictor = "Tiff/rgb_lzw_predictor.tiff";
public const string RgbLzwNoPredictor = "Tiff/rgb_lzw_no_predictor.tiff";
public const string RgbLzwNoPredictorMultistrip = "Tiff/rgb_lzw_noPredictor_multistrip.tiff";
@@ -606,6 +612,7 @@ namespace SixLabors.ImageSharp.Tests
public const string RgbYCbCr888Contiguoush2v1 = "Tiff/rgb-ycbcr-contig-08_h2v1.tiff";
public const string RgbYCbCr888Contiguoush2v2 = "Tiff/rgb-ycbcr-contig-08_h2v2.tiff";
public const string RgbYCbCr888Contiguoush4v4 = "Tiff/rgb-ycbcr-contig-08_h4v4.tiff";
+ public const string YCbCrJpegCompressed = "Tiff/ycbcr_jpegcompressed.tiff";
public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff";
public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff";
public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff";
diff --git a/tests/Images/Input/Jpg/baseline/jpeg-rgb.jpg b/tests/Images/Input/Jpg/baseline/jpeg-rgb.jpg
new file mode 100644
index 0000000000..2f2be0fa1a
--- /dev/null
+++ b/tests/Images/Input/Jpg/baseline/jpeg-rgb.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f4630c33d722a89de5cb1834bffa43c729f204c0f8c95d4ec2127ddfcd433f60
+size 10100
diff --git a/tests/Images/Input/Jpg/baseline/jpeg410.jpg b/tests/Images/Input/Jpg/baseline/jpeg410.jpg
new file mode 100644
index 0000000000..3bc41af8d8
--- /dev/null
+++ b/tests/Images/Input/Jpg/baseline/jpeg410.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:318338c1a541227632a99cc47b04fc9f6d19c3e8300a76e71136edec2d17a5f5
+size 9073
diff --git a/tests/Images/Input/Jpg/baseline/jpeg411.jpg b/tests/Images/Input/Jpg/baseline/jpeg411.jpg
new file mode 100644
index 0000000000..43a2ba49d8
--- /dev/null
+++ b/tests/Images/Input/Jpg/baseline/jpeg411.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:70315936e36bb1edf38fc950b7b321f20be5a2592db59bfd28d79dbc391a5aaf
+size 4465
diff --git a/tests/Images/Input/Jpg/baseline/jpeg422.jpg b/tests/Images/Input/Jpg/baseline/jpeg422.jpg
new file mode 100644
index 0000000000..4782b53b33
--- /dev/null
+++ b/tests/Images/Input/Jpg/baseline/jpeg422.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:be21207ecb96bcb0925706649678c522c68bf08ee26e6e8878456f4f7772dd31
+size 3951
diff --git a/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff
index b0dbdde549..5b668ac513 100644
--- a/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ed3b08730e5c34eb8d268f58d1e09efe2605398899bfd726cc3b35de21baa6ff
-size 121196
+oid sha256:c35902ca485fba441230efa88f794ee5aafa9f75ad5e4a14cb3d592a0a98c538
+size 7760
diff --git a/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff
index d2761d2919..3592206bc5 100644
--- a/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bba35f1e43c8425f3bcfab682efae4d2c00c62f0d8a4b411e646d32047469526
-size 125802
+oid sha256:da7d98823c284d92982a88c4a51434bbc140dceac245a8a054c6e41a489d0cc7
+size 5986
diff --git a/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff b/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff
index 8bdbdcddc6..e0b4fa35e3 100644
--- a/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b8c7f712f9e7d1feeeb55e7743f6ce7d66bc5292f4786ea8526d95057d73145e
-size 4534
+oid sha256:118e55fe0f0dbb5d2712337ec9456a27407f11c0eb9f7e7e81700d4d84b7db09
+size 4378
diff --git a/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff b/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff
index 621ef158aa..3909521203 100644
--- a/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2314b31ca9938fa8b11cbabda0b118a90025a45d2931fca9afa131c0d6919aca
-size 557717
+oid sha256:d4d3541db6b7751d3225599aa822c3376fb27bb9dc2dd28674ef4f78bf426191
+size 83356
diff --git a/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff
index f44a6e9343..e7fdef14bb 100644
--- a/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b9576b3a49b84e26938a7e9ded5f43a1a3c3390bf4824803f5aaab8e00c1afb4
-size 630947
+oid sha256:d007429701cc20154e84909af6988414001e6e60fba5c995f67b2a04cad6e57b
+size 41135
diff --git a/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff
index b14eeba8d5..de85622968 100644
--- a/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3f24fd8f36a4847fcb84a317de4fd2eacd5eb0c58ef4436d33919f0a6658d0d9
-size 698309
+oid sha256:649f0b8ad50a9465fdb447c411e33f20a8b367600e7c3af83c8f686f3ab3e6dc
+size 47143
diff --git a/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff
index 5db7ef564e..e6ff007d4a 100644
--- a/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0283f2be39a151ca3ed19be97ebe4a6b17978ed251dd4d0d568895865fec24c7
-size 964588
+oid sha256:5925388b374b75161ef49273e8c85c4f99713e5e3be380ea13a03895f47809ba
+size 60001
diff --git a/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff b/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff
index e0a39d2483..1998b371cb 100644
--- a/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1a4f687de9925863b1c9f32f53b6c05fb121f9d7b02ff5869113c4745433f10d
-size 124644
+oid sha256:f27e758bb72d5e03fdcf0b1c58c417e4a7928cbdf8d432dc5b9a7d8d7ee4d06b
+size 5668
diff --git a/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff
index 1592645c8e..1d3c9a7899 100644
--- a/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b70500348b1af7828c15e7782eaca105ff749136d7c45eb4cab8c5cd5269c3f6
-size 966134
+oid sha256:29d4b30265158a7cc651d75130843a7f5a7ebf8b2f0f4bb0cf86c82cbec7f6ec
+size 61549
diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff
index c2ebed3649..0ded461400 100644
--- a/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:da6e6a35a0bb0f5f2d49e3c5f0eb2deb7118718dd08844f66a6cb72f48b5c489
-size 1476294
+oid sha256:392e1269220e7e3feb9e2b256e82cce6a2394a5cabef042fdb10def6b24ff165
+size 111819
diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff
index c9f5fadee8..f45aacce44 100644
--- a/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ba79ffac35e16208406e073492d770d3a77e51a47112aa02ab8fd98b5a8487b2
-size 198564
+oid sha256:4baf0f4c462e5bef71ab36f505dfff87a31bd1d25deabccd822a359c1075e08e
+size 65748
diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif b/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif
deleted file mode 100644
index 7450522679..0000000000
--- a/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:53006876fcdc655a794462de57eb6b56f4d0cdd3cb8b752c63328db0eb4aa3c1
-size 725085
diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tiff
new file mode 100644
index 0000000000..5e4cf8be9b
--- /dev/null
+++ b/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d83d8a81ebb7337f00b319a8c37cfdef07423d6a61006411130e386238dd00dd
+size 121907
diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff
index 99642af524..2fa884f36b 100644
--- a/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ecb529e5e3e0eca6f5e407b034fa8ba67bb4b9068af9e9b30425b08d30a249c0
-size 1756355
+oid sha256:993207c34358165af5fac0d0b955b56cd79e9707c1c46344863e01cbc9c7707d
+size 126695
diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff
index 862db0b39f..6fc2c9b213 100644
--- a/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:59dbb48f10c40cbbd4f5617a9f57536790ce0b9a4cc241dc8d6257095598cb76
-size 2891292
+oid sha256:81e7456578510c85e5bebe8bc7c5796da6e2cd61f5bbe0a1f6bb46b8aee3d695
+size 179949
diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff
index 7ebd74d9d4..7fc5923153 100644
--- a/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff
+++ b/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:acb46c990af78fcb0e63849f0f26acffe26833d5cfd2fc77a728baf92166eea3
-size 2893218
+oid sha256:bb25349a7f803aeafc47d8a05deca1a8afdc4bdc5a53e2916f68d5c3e7d8cad3
+size 179207
diff --git a/tests/Images/Input/Tiff/rgb_jpegcompressed_nojpegtable.tiff b/tests/Images/Input/Tiff/rgb_jpegcompressed_nojpegtable.tiff
new file mode 100644
index 0000000000..b97ab8830d
--- /dev/null
+++ b/tests/Images/Input/Tiff/rgb_jpegcompressed_nojpegtable.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6b81013d7b0a29ed1ac9c33e175e0c0e69494b93b2b65b692f16d9ea042b9d5d
+size 7759
diff --git a/tests/Images/Input/Tiff/rgb_jpegcompressed_stripped.tiff b/tests/Images/Input/Tiff/rgb_jpegcompressed_stripped.tiff
new file mode 100644
index 0000000000..2d43f97789
--- /dev/null
+++ b/tests/Images/Input/Tiff/rgb_jpegcompressed_stripped.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1f027d8c2ab0b244f04e51b9bf724eac0123e104a2446324496a08bdf5881922
+size 10550
diff --git a/tests/Images/Input/Tiff/rgb_jpeg.tiff b/tests/Images/Input/Tiff/rgb_jpegcompression.tiff
similarity index 100%
rename from tests/Images/Input/Tiff/rgb_jpeg.tiff
rename to tests/Images/Input/Tiff/rgb_jpegcompression.tiff
diff --git a/tests/Images/Input/Tiff/ycbcr_jpegcompressed.tiff b/tests/Images/Input/Tiff/ycbcr_jpegcompressed.tiff
new file mode 100644
index 0000000000..18334be2af
--- /dev/null
+++ b/tests/Images/Input/Tiff/ycbcr_jpegcompressed.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27fa1d37cd62a9cf105a5e3e015e6a16a13ca7fa82f927a73d32847046c66073
+size 6136