Browse Source

Merge pull request #1734 from SixLabors/bp/tiffjpegcompression

Adds support for Tiff's with jpeg compression
pull/1756/head
James Jackson-South 5 years ago
committed by GitHub
parent
commit
659daffad9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  2. 10
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
  3. 8
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  4. 79
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  5. 10
      src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs
  6. 114
      src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs
  7. 8
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs
  8. 8
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs
  9. 8
      src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
  10. 49
      src/ImageSharp/Formats/Jpeg/JpegColorType.cs
  11. 148
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  12. 15
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  13. 255
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  14. 23
      src/ImageSharp/Formats/Jpeg/JpegSubsample.cs
  15. 2
      src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs
  16. 49
      src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs
  17. 95
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
  18. 33
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
  19. 6
      src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs
  20. 9
      src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
  21. 6
      src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
  22. 2
      src/ImageSharp/Formats/Tiff/README.md
  23. BIN
      src/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf
  24. 43
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  25. 7
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  26. 17
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  27. 3
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  28. 4
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs
  29. 59
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
  30. 2
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  31. 190
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  32. 2
      tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs
  33. 6
      tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
  34. 12
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  35. 7
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  36. 14
      tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
  37. 9
      tests/ImageSharp.Tests/TestImages.cs
  38. 3
      tests/Images/Input/Jpg/baseline/jpeg-rgb.jpg
  39. 3
      tests/Images/Input/Jpg/baseline/jpeg410.jpg
  40. 3
      tests/Images/Input/Jpg/baseline/jpeg411.jpg
  41. 3
      tests/Images/Input/Jpg/baseline/jpeg422.jpg
  42. 4
      tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff
  43. 4
      tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff
  44. 4
      tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff
  45. 4
      tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff
  46. 4
      tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff
  47. 4
      tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff
  48. 4
      tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff
  49. 4
      tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff
  50. 4
      tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff
  51. 4
      tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff
  52. 4
      tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff
  53. 3
      tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif
  54. 3
      tests/Images/Input/Tiff/Calliphora_rgb_lzw.tiff
  55. 4
      tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff
  56. 4
      tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff
  57. 4
      tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff
  58. 3
      tests/Images/Input/Tiff/rgb_jpegcompressed_nojpegtable.tiff
  59. 3
      tests/Images/Input/Tiff/rgb_jpegcompressed_stripped.tiff
  60. 0
      tests/Images/Input/Tiff/rgb_jpegcompression.tiff
  61. 3
      tests/Images/Input/Tiff/ycbcr_jpegcompressed.tiff

14
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs

@ -38,10 +38,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary> /// </summary>
private int restartInterval; private int restartInterval;
// How many mcu's are left to do. /// <summary>
/// How many mcu's are left to do.
/// </summary>
private int todo; private int todo;
// The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero. /// <summary>
/// The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero.
/// </summary>
private int eobrun; private int eobrun;
/// <summary> /// <summary>
@ -54,14 +58,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary> /// </summary>
private readonly HuffmanTable[] acHuffmanTables; private readonly HuffmanTable[] acHuffmanTables;
// The unzig data. /// <summary>
/// The unzig data.
/// </summary>
private ZigZag dctZigZag; private ZigZag dctZigZag;
private HuffmanScanBuffer scanBuffer; private HuffmanScanBuffer scanBuffer;
private readonly SpectralConverter spectralConverter; private readonly SpectralConverter spectralConverter;
private CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HuffmanScanDecoder"/> class. /// Initializes a new instance of the <see cref="HuffmanScanDecoder"/> class.

10
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{ {
/// <summary> /// <summary>
@ -30,5 +32,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// Actual stride height depends on the subsampling factor of the given component. /// Actual stride height depends on the subsampling factor of the given component.
/// </remarks> /// </remarks>
public abstract void ConvertStrideBaseline(); public abstract void ConvertStrideBaseline();
/// <summary>
/// Gets the color converter.
/// </summary>
/// <param name="frame">The jpeg frame with the color space to convert to.</param>
/// <param name="jpegData">The raw JPEG data.</param>
/// <returns>The color converter.</returns>
public virtual JpegColorConverter GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverter.GetConverter(jpegData.ColorSpace, frame.Precision);
} }
} }

8
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs

@ -11,12 +11,12 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{ {
internal sealed class SpectralConverter<TPixel> : SpectralConverter, IDisposable internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly Configuration configuration; private readonly Configuration configuration;
private CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
private JpegComponentPostProcessor[] componentProcessors; private JpegComponentPostProcessor[] componentProcessors;
@ -59,6 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
} }
} }
/// <inheritdoc/>
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{ {
MemoryAllocator allocator = this.configuration.MemoryAllocator; MemoryAllocator allocator = this.configuration.MemoryAllocator;
@ -85,9 +86,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.rgbaBuffer = allocator.Allocate<Vector4>(frame.PixelWidth); this.rgbaBuffer = allocator.Allocate<Vector4>(frame.PixelWidth);
// color converter from Rgba32 to TPixel // color converter from Rgba32 to TPixel
this.colorConverter = JpegColorConverter.GetConverter(jpegData.ColorSpace, frame.Precision); this.colorConverter = this.GetColorConverter(frame, jpegData);
} }
/// <inheritdoc/>
public override void ConvertStrideBaseline() public override void ConvertStrideBaseline()
{ {
// Convert next pixel stride using single spectral `stride' // Convert next pixel stride using single spectral `stride'

79
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs

@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
private int emitLen = 0; private int emitLen = 0;
/// <summary> /// <summary>
/// Emmited bits 'micro buffer' before being transfered to the <see cref="emitBuffer"/>. /// Emitted bits 'micro buffer' before being transferred to the <see cref="emitBuffer"/>.
/// </summary> /// </summary>
private int accumulatedBits; private int accumulatedBits;
@ -58,18 +58,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </summary> /// </summary>
private readonly Stream target; private readonly Stream target;
public HuffmanScanEncoder(Stream outputStream) public HuffmanScanEncoder(Stream outputStream) => this.target = outputStream;
{
this.target = outputStream;
}
/// <summary> /// <summary>
/// Encodes the image with no subsampling. /// Encodes the image with no subsampling.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param> /// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee</param> /// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param>
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee</param> /// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param> /// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void Encode444<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) public void Encode444<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -128,8 +125,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param> /// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee</param> /// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param>
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee</param> /// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param> /// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void Encode420<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) public void Encode420<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -196,7 +193,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param> /// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee</param> /// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param> /// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void EncodeGrayscale<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken) public void EncodeGrayscale<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -234,6 +231,64 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
this.FlushInternalBuffer(); this.FlushInternalBuffer();
} }
/// <summary>
/// Encodes the image with no subsampling and keeps the pixel data as Rgb24.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void EncodeRgb<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
this.huffmanTables = HuffmanLut.TheHuffmanLut;
var unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming
int prevDCR = 0, prevDCG = 0, prevDCB = 0;
ImageFrame<TPixel> frame = pixels.Frames.RootFrame;
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
RowOctet<TPixel> currentRows = default;
var pixelConverter = new RgbForwardConverter<TPixel>(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();
}
/// <summary> /// <summary>
/// Writes a block of pixel data using the given quantization table, /// Writes a block of pixel data using the given quantization table,
/// returning the post-quantized DC value of the DCT-transformed block. /// 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"); DebugGuard.IsTrue(value <= (1 << 16), "Huffman encoder is supposed to encode a value of 16bit size max");
#if SUPPORTS_BITOPERATIONS #if SUPPORTS_BITOPERATIONS
// This should have been implemented as (BitOperations.Log2(value) + 1) as in non-intrinsic implementation // 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 // 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 // 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 // if 0 - return 0 in this case
// else - return log2(value) + 1 // 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 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) // We can safely shift input value for one bit -> log2(value << 1)
// Because of the 16 bit value constraint it won't overflow // Because of the 16 bit value constraint it won't overflow

10
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. // Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{ {
/// <summary> /// <summary>
/// Enumerates the quantization tables /// Enumerates the quantization tables.
/// </summary> /// </summary>
internal enum QuantIndex internal enum QuantIndex
{ {
/// <summary> /// <summary>
/// The luminance quantization table index /// The luminance quantization table index.
/// </summary> /// </summary>
Luminance = 0, Luminance = 0,
/// <summary> /// <summary>
/// The chrominance quantization table index /// The chrominance quantization table index.
/// </summary> /// </summary>
Chrominance = 1, Chrominance = 1,
} }
} }

114
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
{
/// <summary>
/// On-stack worker struct to convert TPixel -> Rgb24 of 8x8 pixel blocks.
/// </summary>
/// <typeparam name="TPixel">The pixel type to work on.</typeparam>
internal ref struct RgbForwardConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Number of pixels processed per single <see cref="Convert(int, int, ref RowOctet{TPixel})"/> call
/// </summary>
private const int PixelsPerSample = 8 * 8;
/// <summary>
/// Total byte size of processed pixels converted from TPixel to <see cref="Rgb24"/>
/// </summary>
private const int RgbSpanByteSize = PixelsPerSample * 3;
/// <summary>
/// <see cref="Size"/> of sampling area from given frame pixel buffer.
/// </summary>
private static readonly Size SampleSize = new Size(8, 8);
/// <summary>
/// The Red component.
/// </summary>
public Block8x8F R;
/// <summary>
/// The Green component.
/// </summary>
public Block8x8F G;
/// <summary>
/// The Blue component.
/// </summary>
public Block8x8F B;
/// <summary>
/// Temporal 64-byte span to hold unconverted TPixel data.
/// </summary>
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal 64-byte span to hold converted Rgb24 data.
/// </summary>
private readonly Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size.
/// </summary>
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations.
/// </summary>
private readonly Configuration config;
public RgbForwardConverter(ImageFrame<TPixel> frame)
{
this.R = default;
this.G = default;
this.B = default;
// temporal pixel buffers
this.pixelSpan = new TPixel[PixelsPerSample].AsSpan();
this.rgbSpan = MemoryMarshal.Cast<byte, Rgb24>(new byte[RgbSpanByteSize + RgbToYCbCrConverterVectorized.AvxCompatibilityPadding].AsSpan());
// frame data
this.samplingAreaSize = new Size(frame.Width, frame.Height);
this.config = frame.GetConfiguration();
}
/// <summary>
/// Converts a 8x8 image area inside 'pixels' at position (x, y) to Rgb24.
/// </summary>
public void Convert(int x, int y, ref RowOctet<TPixel> currentRows)
{
YCbCrForwardConverter<TPixel>.LoadAndStretchEdges(currentRows, this.pixelSpan, new Point(x, y), SampleSize, this.samplingAreaSize);
PixelOperations<TPixel>.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<Rgb24> 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;
}
}
}
}

8
src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs

@ -58,22 +58,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// <summary> /// <summary>
/// Temporal 16x8 block to hold TPixel data /// Temporal 16x8 block to hold TPixel data
/// </summary> /// </summary>
private Span<TPixel> pixelSpan; private readonly Span<TPixel> pixelSpan;
/// <summary> /// <summary>
/// Temporal RGB block /// Temporal RGB block
/// </summary> /// </summary>
private Span<Rgb24> rgbSpan; private readonly Span<Rgb24> rgbSpan;
/// <summary> /// <summary>
/// Sampled pixel buffer size /// Sampled pixel buffer size
/// </summary> /// </summary>
private Size samplingAreaSize; private readonly Size samplingAreaSize;
/// <summary> /// <summary>
/// <see cref="Configuration"/> for internal operations /// <see cref="Configuration"/> for internal operations
/// </summary> /// </summary>
private Configuration config; private readonly Configuration config;
public YCbCrForwardConverter420(ImageFrame<TPixel> frame) public YCbCrForwardConverter420(ImageFrame<TPixel> frame)
{ {

8
src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs

@ -53,22 +53,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// <summary> /// <summary>
/// Temporal 64-byte span to hold unconverted TPixel data /// Temporal 64-byte span to hold unconverted TPixel data
/// </summary> /// </summary>
private Span<TPixel> pixelSpan; private readonly Span<TPixel> pixelSpan;
/// <summary> /// <summary>
/// Temporal 64-byte span to hold converted Rgb24 data /// Temporal 64-byte span to hold converted Rgb24 data
/// </summary> /// </summary>
private Span<Rgb24> rgbSpan; private readonly Span<Rgb24> rgbSpan;
/// <summary> /// <summary>
/// Sampled pixel buffer size /// Sampled pixel buffer size
/// </summary> /// </summary>
private Size samplingAreaSize; private readonly Size samplingAreaSize;
/// <summary> /// <summary>
/// <see cref="Configuration"/> for internal operations /// <see cref="Configuration"/> for internal operations
/// </summary> /// </summary>
private Configuration config; private readonly Configuration config;
public YCbCrForwardConverter444(ImageFrame<TPixel> frame) public YCbCrForwardConverter444(ImageFrame<TPixel> frame)
{ {

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

@ -16,13 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public int? Quality { get; set; } public int? Quality { get; set; }
/// <summary> /// <summary>
/// Gets the subsample ration, that will be used to encode the image. /// Gets the color type, that will be used to encode the image.
/// </summary>
/// <value>The subsample ratio of the jpg image.</value>
JpegSubsample? Subsample { get; }
/// <summary>
/// Gets the color type.
/// </summary> /// </summary>
JpegColorType? ColorType { get; } JpegColorType? ColorType { get; }
} }

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

@ -10,12 +10,57 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{ {
/// <summary> /// <summary>
/// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. /// 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.
/// </summary> /// </summary>
YCbCr = 0, YCbCrRatio420 = 0,
/// <summary>
/// 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.
/// </summary>
YCbCrRatio444 = 1,
/// <summary>
/// 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.
/// </summary>
YCbCrRatio422 = 2,
/// <summary>
/// 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.
/// </summary>
YCbCrRatio411 = 3,
/// <summary>
/// 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.
/// </summary>
YCbCrRatio410 = 4,
/// <summary> /// <summary>
/// Single channel, luminance. /// Single channel, luminance.
/// </summary> /// </summary>
Luminance = 1 Luminance = 5,
/// <summary>
/// The pixel data will be preserved as RGB without any sub sampling.
/// </summary>
Rgb = 6,
/// <summary>
/// CMYK colorspace (cyan, magenta, yellow, and key black) intended for printing.
///
/// Note: Not supported by the encoder.
/// </summary>
Cmyk = 7,
} }
} }

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

@ -4,6 +4,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
@ -136,8 +137,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Finds the next file marker within the byte stream. /// Finds the next file marker within the byte stream.
/// </summary> /// </summary>
/// <param name="marker">The buffer to read file markers to</param> /// <param name="marker">The buffer to read file markers to.</param>
/// <param name="stream">The input stream</param> /// <param name="stream">The input stream.</param>
/// <returns>The <see cref="JpegFileMarker"/></returns> /// <returns>The <see cref="JpegFileMarker"/></returns>
public static JpegFileMarker FindNextFileMarker(byte[] marker, BufferedReadStream stream) 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); return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata);
} }
/// <summary>
/// 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).
/// </summary>
/// <param name="tableBytes">The table bytes.</param>
/// <param name="huffmanScanDecoder">The scan decoder.</param>
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);
}
}
/// <summary> /// <summary>
/// Parses the input stream for file markers. /// Parses the input stream for file markers.
/// </summary> /// </summary>
@ -225,7 +287,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
stream.Read(this.markerBuffer, 0, 2); stream.Read(this.markerBuffer, 0, 2);
byte marker = this.markerBuffer[1]; byte marker = this.markerBuffer[1];
fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); 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. // Break only when we discover a valid EOI marker.
// https://github.com/SixLabors/ImageSharp/issues/695 // https://github.com/SixLabors/ImageSharp/issues/695
@ -236,7 +298,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (!fileMarker.Invalid) if (!fileMarker.Invalid)
{ {
// Get the marker length // Get the marker length.
int remaining = this.ReadUint16(stream) - 2; int remaining = this.ReadUint16(stream) - 2;
switch (fileMarker.Marker) switch (fileMarker.Marker)
@ -272,7 +334,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.SOS: case JpegConstants.Markers.SOS:
if (!metadataOnly) if (!metadataOnly)
{ {
this.ProcessStartOfScanMarker(stream, remaining, cancellationToken); this.ProcessStartOfScanMarker(stream, remaining);
break; break;
} }
else else
@ -371,10 +433,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="componentCount">The number of components.</param>
/// <returns>The <see cref="JpegColorSpace"/></returns> /// <returns>The <see cref="JpegColorSpace"/></returns>
private JpegColorSpace DeduceJpegColorSpace(byte componentCount, JpegComponent[] components) private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
{ {
if (componentCount == 1) 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 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; return JpegColorSpace.RGB;
} }
@ -410,6 +473,64 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return default; return default;
} }
/// <summary>
/// Returns the jpeg color type based on the colorspace and subsampling used.
/// </summary>
/// <returns>Jpeg color type.</returns>
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;
}
}
/// <summary> /// <summary>
/// Initializes the EXIF profile. /// Initializes the EXIF profile.
/// </summary> /// </summary>
@ -608,7 +729,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop. /// 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.
/// </summary> /// </summary>
/// <param name="stream">The input stream.</param> /// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param> /// <param name="remaining">The remaining bytes in the segment block.</param>
@ -823,7 +944,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
/// <summary> /// <summary>
/// Processes the Start of Frame marker. Specified in section B.2.2. /// Processes the Start of Frame marker. Specified in section B.2.2.
/// </summary> /// </summary>
/// <param name="stream">The input stream.</param> /// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param> /// <param name="remaining">The remaining bytes in the segment block.</param>
@ -915,9 +1036,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
index += componentBytes; index += componentBytes;
} }
this.ColorSpace = this.DeduceJpegColorSpace(componentCount, this.Frame.Components); this.ColorSpace = this.DeduceJpegColorSpace(componentCount);
this.Metadata.GetJpegMetadata().ColorType = this.DeduceJpegColorType();
this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
if (!metadataOnly) if (!metadataOnly)
{ {
@ -1015,7 +1135,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Processes the SOS (Start of scan marker). /// Processes the SOS (Start of scan marker).
/// </summary> /// </summary>
private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining, CancellationToken cancellationToken) private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)
{ {
if (this.Frame is null) if (this.Frame is null)
{ {

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

@ -16,14 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <inheritdoc/> /// <inheritdoc/>
public int? Quality { get; set; } public int? Quality { get; set; }
/// <summary> /// <inheritdoc/>
/// Gets or sets the subsample ration, that will be used to encode the image.
/// </summary>
public JpegSubsample? Subsample { get; set; }
/// <summary>
/// Gets or sets the color type, that will be used to encode the image.
/// </summary>
public JpegColorType? ColorType { get; set; } public JpegColorType? ColorType { get; set; }
/// <summary> /// <summary>
@ -36,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new JpegEncoderCore(this); var encoder = new JpegEncoderCore(this);
this.InitializeColorType<TPixel>(image); this.InitializeColorType(image);
encoder.Encode(image, stream); encoder.Encode(image, stream);
} }
@ -52,7 +45,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new JpegEncoderCore(this); var encoder = new JpegEncoderCore(this);
this.InitializeColorType<TPixel>(image); this.InitializeColorType(image);
return encoder.EncodeAsync(image, stream, cancellationToken); return encoder.EncodeAsync(image, stream, cancellationToken);
} }
@ -75,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
bool isGrayscale = bool isGrayscale =
typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) || typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) ||
typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32); typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32);
this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCr; this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCrRatio420;
} }
} }
} }

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

@ -33,20 +33,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
private readonly byte[] buffer = new byte[20]; private readonly byte[] buffer = new byte[20];
/// <summary>
/// Gets or sets the subsampling method to use.
/// </summary>
private JpegSubsample? subsample;
/// <summary> /// <summary>
/// The quality, that will be used to encode the image. /// The quality, that will be used to encode the image.
/// </summary> /// </summary>
private readonly int? quality; private readonly int? quality;
/// <summary> /// <summary>
/// Gets or sets the subsampling method to use. /// Gets or sets the colorspace to use.
/// </summary> /// </summary>
private readonly JpegColorType? colorType; private JpegColorType? colorType;
/// <summary> /// <summary>
/// The output stream. All attempted writes after the first error become no-ops. /// The output stream. All attempted writes after the first error become no-ops.
@ -56,12 +51,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JpegEncoderCore"/> class. /// Initializes a new instance of the <see cref="JpegEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="options">The options</param> /// <param name="options">The options.</param>
public JpegEncoderCore(IJpegEncoderOptions options) public JpegEncoderCore(IJpegEncoderOptions options)
{ {
this.quality = options.Quality; this.quality = options.Quality;
this.subsample = options.Subsample;
this.colorType = options.ColorType; if (IsSupportedColorType(options.ColorType))
{
this.colorType = options.ColorType;
}
} }
/// <summary> /// <summary>
@ -88,49 +86,72 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); 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. // Compute number of components based on color type in options.
int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3; int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3;
ReadOnlySpan<byte> componentIds = this.GetComponentIds();
// TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that // TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that
// Initialize the quantization tables. // Initialize the quantization tables.
this.InitQuantizationTables(componentCount, jpegMetadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable); this.InitQuantizationTables(componentCount, jpegMetadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable);
// Write the Start Of Image marker. // 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 // Write Exif, ICC and IPTC profiles
this.WriteProfiles(metadata); this.WriteProfiles(metadata);
if (this.colorType == JpegColorType.Rgb)
{
// Write App14 marker to indicate RGB color space.
this.WriteApp14Marker();
}
// Write the quantization tables. // Write the quantization tables.
this.WriteDefineQuantizationTables(ref luminanceQuantTable, ref chrominanceQuantTable); this.WriteDefineQuantizationTables(ref luminanceQuantTable, ref chrominanceQuantTable);
// Write the image dimensions. // Write the image dimensions.
this.WriteStartOfFrame(image.Width, image.Height, componentCount); this.WriteStartOfFrame(image.Width, image.Height, componentCount, componentIds);
// Write the Huffman tables. // Write the Huffman tables.
this.WriteDefineHuffmanTables(componentCount); this.WriteDefineHuffmanTables(componentCount);
// Write the scan header. // Write the scan header.
this.WriteStartOfScan(image, componentCount, cancellationToken); this.WriteStartOfScan(componentCount, componentIds);
// Write the scan compressed data. // Write the scan compressed data.
var scanEncoder = new HuffmanScanEncoder(stream); var scanEncoder = new HuffmanScanEncoder(stream);
if (this.colorType == JpegColorType.Luminance) if (this.colorType == JpegColorType.Luminance)
{ {
// luminance quantization table only // luminance quantization table only.
scanEncoder.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken); scanEncoder.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken);
} }
else else
{ {
// luminance and chrominance quantization tables // luminance and chrominance quantization tables.
switch (this.subsample) switch (this.colorType)
{ {
case JpegSubsample.Ratio444: case JpegColorType.YCbCrRatio444:
case JpegColorType.Luminance:
scanEncoder.Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); scanEncoder.Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break; break;
case JpegSubsample.Ratio420: case JpegColorType.YCbCrRatio420:
scanEncoder.Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); scanEncoder.Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break; break;
case JpegColorType.Rgb:
scanEncoder.EncodeRgb(image, ref luminanceQuantTable, cancellationToken);
break;
} }
} }
@ -141,12 +162,32 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
/// <summary> /// <summary>
/// Writes data to "Define Quantization Tables" block for QuantIndex /// Returns true, if the color type is supported by the encoder.
/// </summary> /// </summary>
/// <param name="dqt">The "Define Quantization Tables" block</param> /// <param name="colorType">The color type.</param>
/// <param name="offset">Offset in "Define Quantization Tables" block</param> /// <returns>true, if color type is supported.</returns>
/// <param name="i">The quantization index</param> private static bool IsSupportedColorType(JpegColorType? colorType)
/// <param name="quant">The quantization table to copy data from</param> => colorType == JpegColorType.YCbCrRatio444
|| colorType == JpegColorType.YCbCrRatio420
|| colorType == JpegColorType.Luminance
|| colorType == JpegColorType.Rgb;
/// <summary>
/// Gets the component ids.
/// For color space RGB this will be RGB as ASCII, otherwise 1, 2, 3.
/// </summary>
/// <returns>The component Ids.</returns>
private ReadOnlySpan<byte> GetComponentIds() => this.colorType == JpegColorType.Rgb
? new ReadOnlySpan<byte>(new byte[] { 82, 71, 66 })
: new ReadOnlySpan<byte>(new byte[] { 1, 2, 3 });
/// <summary>
/// Writes data to "Define Quantization Tables" block for QuantIndex.
/// </summary>
/// <param name="dqt">The "Define Quantization Tables" block.</param>
/// <param name="offset">Offset in "Define Quantization Tables" block.</param>
/// <param name="i">The quantization index.</param>
/// <param name="quant">The quantization table to copy data from.</param>
private static void WriteDataToDqt(byte[] dqt, ref int offset, QuantIndex i, ref Block8x8F quant) private static void WriteDataToDqt(byte[] dqt, ref int offset, QuantIndex i, ref Block8x8F quant)
{ {
dqt[offset++] = (byte)i; dqt[offset++] = (byte)i;
@ -157,52 +198,60 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
/// <summary> /// <summary>
/// Writes the application header containing the JFIF identifier plus extra data. /// Write the start of image marker.
/// </summary> /// </summary>
/// <param name="meta">The image metadata.</param> private void WriteStartOfImage()
private void WriteApplicationHeader(ImageMetadata meta)
{ {
// 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[0] = JpegConstants.Markers.XFF;
this.buffer[1] = JpegConstants.Markers.SOI; this.buffer[1] = JpegConstants.Markers.SOI;
this.outputStream.Write(this.buffer, 0, 2);
}
/// <summary>
/// Writes the application header containing the JFIF identifier plus extra data.
/// </summary>
/// <param name="meta">The image metadata.</param>
private void WriteJfifApplicationHeader(ImageMetadata meta)
{
// Write the JFIF headers // Write the JFIF headers
this.buffer[2] = JpegConstants.Markers.XFF; this.buffer[0] = JpegConstants.Markers.XFF;
this.buffer[3] = JpegConstants.Markers.APP0; // Application Marker this.buffer[1] = JpegConstants.Markers.APP0; // Application Marker
this.buffer[4] = 0x00; this.buffer[2] = 0x00;
this.buffer[5] = 0x10; this.buffer[3] = 0x10;
this.buffer[6] = 0x4a; // J this.buffer[4] = 0x4a; // J
this.buffer[5] = 0x46; // F
this.buffer[6] = 0x49; // I
this.buffer[7] = 0x46; // F this.buffer[7] = 0x46; // F
this.buffer[8] = 0x49; // I this.buffer[8] = 0x00; // = "JFIF",'\0'
this.buffer[9] = 0x46; // F this.buffer[9] = 0x01; // versionhi
this.buffer[10] = 0x00; // = "JFIF",'\0' this.buffer[10] = 0x01; // versionlo
this.buffer[11] = 0x01; // versionhi
this.buffer[12] = 0x01; // versionlo
// Resolution. Big Endian // Resolution. Big Endian
Span<byte> hResolution = this.buffer.AsSpan(14, 2); Span<byte> hResolution = this.buffer.AsSpan(12, 2);
Span<byte> vResolution = this.buffer.AsSpan(16, 2); Span<byte> vResolution = this.buffer.AsSpan(14, 2);
if (meta.ResolutionUnits == PixelResolutionUnit.PixelsPerMeter) if (meta.ResolutionUnits == PixelResolutionUnit.PixelsPerMeter)
{ {
// Scale down to PPI // 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(hResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.HorizontalResolution)));
BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.VerticalResolution))); BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(UnitConverter.MeterToInch(meta.VerticalResolution)));
} }
else else
{ {
// We can simply pass the value. // 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(hResolution, (short)Math.Round(meta.HorizontalResolution));
BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(meta.VerticalResolution)); BinaryPrimitives.WriteInt16BigEndian(vResolution, (short)Math.Round(meta.VerticalResolution));
} }
// No thumbnail // No thumbnail
this.buffer[18] = 0x00; // Thumbnail width this.buffer[16] = 0x00; // Thumbnail width
this.buffer[19] = 0x00; // Thumbnail height this.buffer[17] = 0x00; // Thumbnail height
this.outputStream.Write(this.buffer, 0, 20); this.outputStream.Write(this.buffer, 0, 18);
} }
/// <summary> /// <summary>
@ -212,7 +261,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private void WriteDefineHuffmanTables(int componentCount) private void WriteDefineHuffmanTables(int componentCount)
{ {
// Table identifiers. // Table identifiers.
Span<byte> headers = stackalloc byte[] ReadOnlySpan<byte> headers = stackalloc byte[]
{ {
0x00, 0x00,
0x10, 0x10,
@ -249,7 +298,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
private void WriteDefineQuantizationTables(ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable) private void WriteDefineQuantizationTables(ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable)
{ {
// Marker + quantization table lengths // Marker + quantization table lengths.
int markerlen = 2 + (QuantizationTableCount * (1 + Block8x8F.Size)); int markerlen = 2 + (QuantizationTableCount * (1 + Block8x8F.Size));
this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen); this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen);
@ -265,6 +314,35 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream.Write(dqt, 0, dqtCount); this.outputStream.Write(dqt, 0, dqtCount);
} }
/// <summary>
/// Writes the APP14 marker to indicate the image is in RGB color space.
/// </summary>
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));
}
/// <summary> /// <summary>
/// Writes the EXIF profile. /// Writes the EXIF profile.
/// </summary> /// </summary>
@ -343,7 +421,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
throw new ImageFormatException($"Iptc profile size exceeds limit of {Max} bytes"); 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.AdobeImageResourceBlockMarker.Length +
ProfileResolver.AdobeIptcMarker.Length + ProfileResolver.AdobeIptcMarker.Length +
2 + 4 + data.Length; 2 + 4 + data.Length;
@ -385,7 +463,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
/// <param name="iccProfile">The ICC profile to write.</param> /// <param name="iccProfile">The ICC profile to write.</param>
/// <exception cref="ImageFormatException"> /// <exception cref="ImageFormatException">
/// Thrown if any of the ICC profiles size exceeds the limit /// Thrown if any of the ICC profiles size exceeds the limit.
/// </exception> /// </exception>
private void WriteIccProfile(IccProfile iccProfile) private void WriteIccProfile(IccProfile iccProfile)
{ {
@ -405,7 +483,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return; 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 dataLength = data.Length;
int count = dataLength / MaxData; int count = dataLength / MaxData;
@ -478,22 +556,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
/// <summary> /// <summary>
/// Writes the Start Of Frame (Baseline) marker /// Writes the Start Of Frame (Baseline) marker.
/// </summary> /// </summary>
/// <param name="width">The width of the image</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image</param> /// <param name="height">The height of the image.</param>
/// <param name="componentCount">The number of components in a pixel</param> /// <param name="componentCount">The number of components in a pixel.</param>
private void WriteStartOfFrame(int width, int height, int componentCount) /// <param name="componentIds">The component Id's.</param>
private void WriteStartOfFrame(int width, int height, int componentCount, ReadOnlySpan<byte> 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 // "default" to 4:2:0
Span<byte> subsamples = stackalloc byte[] ReadOnlySpan<byte> subsamples = stackalloc byte[]
{ {
0x22, 0x22,
0x11, 0x11,
0x11 0x11
}; };
Span<byte> chroma = stackalloc byte[] ReadOnlySpan<byte> chroma = stackalloc byte[]
{ {
0x00, 0x00,
0x01, 0x01,
@ -511,17 +592,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
else else
{ {
switch (this.subsample) switch (this.colorType)
{ {
case JpegSubsample.Ratio444: case JpegColorType.YCbCrRatio444:
case JpegColorType.Rgb:
subsamples = stackalloc byte[] subsamples = stackalloc byte[]
{ {
0x11, 0x11,
0x11, 0x11,
0x11 0x11
}; };
if (this.colorType == JpegColorType.Rgb)
{
chroma = stackalloc byte[]
{
0x00,
0x00,
0x00
};
}
break; break;
case JpegSubsample.Ratio420: case JpegColorType.YCbCrRatio420:
subsamples = stackalloc byte[] subsamples = stackalloc byte[]
{ {
0x22, 0x22,
@ -545,10 +638,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
for (int i = 0; i < componentCount; i++) for (int i = 0; i < componentCount; i++)
{ {
int i3 = 3 * i; int i3 = 3 * i;
this.buffer[i3 + 6] = (byte)(i + 1);
this.buffer[i3 + 7] = subsamples[i]; // Component ID.
this.buffer[i3 + 8] = chroma[i]; Span<byte> 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); this.outputStream.Write(this.buffer, 0, (3 * (componentCount - 1)) + 9);
@ -557,26 +652,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Writes the StartOfScan marker. /// Writes the StartOfScan marker.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The pixel accessor providing access to the image pixels.</param>
/// <param name="componentCount">The number of components in a pixel.</param> /// <param name="componentCount">The number of components in a pixel.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param> /// <param name="componentIds">The componentId's.</param>
private void WriteStartOfScan<TPixel>(Image<TPixel> image, int componentCount, CancellationToken cancellationToken) private void WriteStartOfScan(int componentCount, ReadOnlySpan<byte> componentIds)
where TPixel : unmanaged, IPixel<TPixel>
{ {
Span<byte> componentId = 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.
0x01, ReadOnlySpan<byte> huffmanId = stackalloc byte[]
0x02,
0x03
};
Span<byte> huffmanId = stackalloc byte[]
{ {
0x00, 0x00,
0x11, 0x11,
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: // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c", // - the marker length "\x00\x0c",
// - the number of components "\x03", // - the number of components "\x03",
@ -597,7 +696,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
for (int i = 0; i < componentCount; i++) for (int i = 0; i < componentCount; i++)
{ {
int i2 = 2 * 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 this.buffer[i2 + 6] = huffmanId[i]; // DC/AC Huffman table
} }
@ -633,7 +732,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
/// <summary> /// <summary>
/// Initializes quntization tables. /// Initializes quantization tables.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// We take quality values in a hierarchical order: /// We take quality values in a hierarchical order:
@ -672,9 +771,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
chromaQuality = Numerics.Clamp(chromaQuality, 1, 100); chromaQuality = Numerics.Clamp(chromaQuality, 1, 100);
chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); 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;
} }
} }
} }

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

@ -1,23 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg
{
/// <summary>
/// Enumerates the chroma subsampling method applied to the image.
/// </summary>
public enum JpegSubsample
{
/// <summary>
/// High Quality - Each of the three Y'CbCr components have the same sample rate,
/// thus there is no chroma subsampling.
/// </summary>
Ratio444,
/// <summary>
/// Medium Quality - The horizontal sampling is halved and the Cb and Cr channels are only
/// sampled on each alternate line.
/// </summary>
Ratio420
}
}

2
src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs

@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
private static bool IsReplicateRun(ReadOnlySpan<byte> rowSpan, int startPos) private static bool IsReplicateRun(ReadOnlySpan<byte> rowSpan, int startPos)
{ {
// We consider run which has at least 3 same consecutive bytes a candidate for a run. // 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; int count = 0;
for (int i = startPos + 1; i < rowSpan.Length; i++) for (int i = startPos + 1; i < rowSpan.Length; i++)
{ {

49
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)
{
}
/// <inheritdoc/>
public override TiffCompression Method => TiffCompression.Jpeg;
/// <inheritdoc/>
public override void Initialize(int rowsPerStrip)
{
}
/// <inheritdoc/>
public override void CompressStrip(Span<byte> rows, int height)
{
int pixelCount = rows.Length / 3;
int width = pixelCount / height;
using var memoryStream = new MemoryStream();
var image = Image.LoadPixelData<Rgb24>(rows, width, height);
image.Save(memoryStream, new JpegEncoder()
{
ColorType = JpegColorType.Rgb
});
memoryStream.Position = 0;
memoryStream.WriteTo(this.Output);
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

95
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
{
/// <summary>
/// Class to handle cases where TIFF image data is compressed as a jpeg stream.
/// </summary>
internal sealed class JpegTiffCompression : TiffBaseDecompressor
{
private readonly Configuration configuration;
private readonly byte[] jpegTables;
private readonly TiffPhotometricInterpretation photometricInterpretation;
/// <summary>
/// Initializes a new instance of the <see cref="JpegTiffCompression"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="memoryAllocator">The memoryAllocator to use for buffer allocations.</param>
/// <param name="width">The image width.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param>
/// <param name="jpegTables">The JPEG tables containing the quantization and/or Huffman tables.</param>
/// <param name="photometricInterpretation">The photometric interpretation.</param>
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;
}
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> 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<Rgb24> spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ?
new RgbJpegSpectralConverter<Rgb24>(this.configuration, CancellationToken.None) : new SpectralConverter<Rgb24>(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<Rgb24>(this.configuration, spectralConverter.PixelBuffer, new ImageMetadata());
CopyImageBytesToBuffer(buffer, image);
}
else
{
using var image = Image.Load<Rgb24>(stream);
CopyImageBytesToBuffer(buffer, image);
}
}
private static void CopyImageBytesToBuffer(Span<byte> buffer, Image<Rgb24> image)
{
int offset = 0;
for (int y = 0; y < image.Height; y++)
{
Span<Rgb24> pixelRowSpan = image.GetPixelRowSpan(y);
Span<byte> rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan);
rgbBytes.CopyTo(buffer.Slice(offset));
offset += rgbBytes.Length;
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

33
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
{
/// <summary>
/// Spectral converter for YCbCr TIFF's which use the JPEG compression.
/// The jpeg data should be always treated as RGB color space.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
internal sealed class RgbJpegSpectralConverter<TPixel> : SpectralConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="RgbJpegSpectralConverter{TPixel}"/> class.
/// This Spectral converter will always convert the pixel data to RGB color.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public RgbJpegSpectralConverter(Configuration configuration, CancellationToken cancellationToken)
: base(configuration, cancellationToken)
{
}
/// <inheritdoc/>
public override JpegColorConverter GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverter.GetConverter(JpegColorSpace.RGB, frame.Precision);
}
}

6
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. // The following compression types are not implemented in the encoder and will default to no compression instead.
case TiffCompression.ItuTRecT43: case TiffCompression.ItuTRecT43:
case TiffCompression.ItuTRecT82: case TiffCompression.ItuTRecT82:
case TiffCompression.Jpeg:
case TiffCompression.OldJpeg: case TiffCompression.OldJpeg:
case TiffCompression.OldDeflate: case TiffCompression.OldDeflate:
case TiffCompression.None: case TiffCompression.None:
@ -34,6 +33,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
return new NoCompressor(output, allocator, width, bitsPerPixel); 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: case TiffCompression.PackBits:
DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); 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"); DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");

9
src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs

@ -36,11 +36,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
/// <summary> /// <summary>
/// Image data is compressed using CCITT T.6 fax compression. /// Image data is compressed using CCITT T.6 fax compression.
/// </summary> /// </summary>
T6 = 6, T6 = 5,
/// <summary> /// <summary>
/// Image data is compressed using modified huffman compression. /// Image data is compressed using modified huffman compression.
/// </summary> /// </summary>
HuffmanRle = 5, HuffmanRle = 6,
/// <summary>
/// The image data is compressed as a JPEG stream.
/// </summary>
Jpeg = 7,
} }
} }

6
src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs

@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
internal static class TiffDecompressorsFactory internal static class TiffDecompressorsFactory
{ {
public static TiffBaseDecompressor Create( public static TiffBaseDecompressor Create(
Configuration configuration,
TiffDecoderCompressionType method, TiffDecoderCompressionType method,
MemoryAllocator allocator, MemoryAllocator allocator,
TiffPhotometricInterpretation photometricInterpretation, TiffPhotometricInterpretation photometricInterpretation,
@ -19,6 +20,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
TiffColorType colorType, TiffColorType colorType,
TiffPredictor predictor, TiffPredictor predictor,
FaxCompressionOptions faxOptions, FaxCompressionOptions faxOptions,
byte[] jpegTables,
TiffFillOrder fillOrder, TiffFillOrder fillOrder,
ByteOrder byteOrder) 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"); DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); 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: default:
throw TiffThrowHelper.NotSupportedDecompressor(nameof(method)); throw TiffThrowHelper.NotSupportedDecompressor(nameof(method));
} }

2
src/ImageSharp/Formats/Tiff/README.md

@ -48,7 +48,7 @@
|CcittGroup4Fax | | Y | | |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 | |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 | |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. | |Deflate (Technote 2) | Y | Y | Based on PNG Deflate. |
|Old Deflate (Technote 2) | | Y | | |Old Deflate (Technote 2) | | Y | |

BIN
src/ImageSharp/Formats/Tiff/TIFF-AdobeTechNote-22032002.pdf

Binary file not shown.

43
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -105,6 +105,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary> /// </summary>
public TiffFillOrder FillOrder { get; set; } public TiffFillOrder FillOrder { get; set; }
/// <summary>
/// Gets or sets the JPEG tables when jpeg compression is used.
/// </summary>
public byte[] JpegTables { get; set; }
/// <summary> /// <summary>
/// Gets or sets the planar configuration type to use when decoding the image. /// Gets or sets the planar configuration type to use when decoding the image.
/// </summary> /// </summary>
@ -144,7 +149,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
var frames = new List<ImageFrame<TPixel>>(); var frames = new List<ImageFrame<TPixel>>();
foreach (ExifProfile ifd in directories) foreach (ExifProfile ifd in directories)
{ {
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd); cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, cancellationToken);
frames.Add(frame); frames.Add(frame);
} }
@ -186,10 +192,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param> /// <param name="tags">The IFD tags.</param>
/// <returns> /// <param name="cancellationToken">The token to monitor cancellation.</param>
/// The tiff frame. /// <returns> The tiff frame. </returns>
/// </returns> private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken)
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ? ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ?
@ -211,11 +216,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar)
{ {
this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts); this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken);
} }
else else
{ {
this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts); this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken);
} }
return frame; return frame;
@ -263,14 +268,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff
} }
/// <summary> /// <summary>
/// Decodes the image data for strip encoded data. /// Decodes the image data for planar encoded pixel data.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode data into.</param> /// <param name="frame">The image frame to decode data into.</param>
/// <param name="rowsPerStrip">The number of rows per strip of data.</param> /// <param name="rowsPerStrip">The number of rows per strip of data.</param>
/// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param> /// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param>
/// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param> /// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts) /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int stripsPerPixel = this.BitsPerSample.Channels; int stripsPerPixel = this.BitsPerSample.Channels;
@ -290,6 +296,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
} }
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(
this.Configuration,
this.CompressionType, this.CompressionType,
this.memoryAllocator, this.memoryAllocator,
this.PhotometricInterpretation, this.PhotometricInterpretation,
@ -298,6 +305,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.ColorType, this.ColorType,
this.Predictor, this.Predictor,
this.FaxCompressionOptions, this.FaxCompressionOptions,
this.JpegTables,
this.FillOrder, this.FillOrder,
this.byteOrder); this.byteOrder);
@ -312,6 +320,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
for (int i = 0; i < stripsPerPlane; i++) for (int i = 0; i < stripsPerPlane; i++)
{ {
cancellationToken.ThrowIfCancellationRequested();
int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip;
int stripIndex = i; int stripIndex = i;
@ -338,7 +348,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff
} }
} }
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts) /// <summary>
/// Decodes the image data for chunky encoded pixel data.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode data into.</param>
/// <param name="rowsPerStrip">The rows per strip.</param>
/// <param name="stripOffsets">The strip offsets.</param>
/// <param name="stripByteCounts">The strip byte counts.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. // 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<TPixel> pixels = frame.PixelBuffer; Buffer2D<TPixel> pixels = frame.PixelBuffer;
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(
this.Configuration,
this.CompressionType, this.CompressionType,
this.memoryAllocator, this.memoryAllocator,
this.PhotometricInterpretation, this.PhotometricInterpretation,
@ -363,6 +383,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.ColorType, this.ColorType,
this.Predictor, this.Predictor,
this.FaxCompressionOptions, this.FaxCompressionOptions,
this.JpegTables,
this.FillOrder, this.FillOrder,
this.byteOrder); this.byteOrder);
@ -379,6 +400,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{ {
cancellationToken.ThrowIfCancellationRequested();
int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0
? rowsPerStrip ? rowsPerStrip
: frame.Height % rowsPerStrip; : frame.Height % rowsPerStrip;

7
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -87,6 +87,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value; options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value;
options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value;
options.FillOrder = fillOrder; options.FillOrder = fillOrder;
options.JpegTables = exifProfile.GetValue(ExifTag.JPEGTables)?.Value;
options.ParseColorType(exifProfile); options.ParseColorType(exifProfile);
options.ParseCompression(frameMetadata.Compression, exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile);
@ -432,6 +433,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
break; break;
} }
case TiffCompression.Jpeg:
{
options.CompressionType = TiffDecoderCompressionType.Jpeg;
break;
}
default: default:
{ {
TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported"); TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported");

17
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -155,6 +155,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
Image<TPixel> metadataImage = image; Image<TPixel> metadataImage = image;
foreach (ImageFrame<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
cancellationToken.ThrowIfCancellationRequested();
var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile?.GetValue(ExifTag.SubfileType)?.Value ?? (int)TiffNewSubfileType.FullImage); var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile?.GetValue(ExifTag.SubfileType)?.Value ?? (int)TiffNewSubfileType.FullImage);
ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker); ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker);
@ -223,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
entriesCollector, entriesCollector,
(int)this.BitsPerPixel); (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); colorWriter.Write(compressor, rowsPerStrip);
@ -245,13 +247,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary> /// </summary>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="bytesPerRow">The number of bytes per row.</param> /// <param name="bytesPerRow">The number of bytes per row.</param>
/// <param name="compression">The compression used.</param>
/// <returns>Number of rows per strip.</returns> /// <returns>Number of rows per strip.</returns>
private int CalcRowsPerStrip(int height, int bytesPerRow) private int CalcRowsPerStrip(int height, int bytesPerRow, TiffCompression? compression)
{ {
DebugGuard.MustBeGreaterThan(height, 0, nameof(height)); DebugGuard.MustBeGreaterThan(height, 0, nameof(height));
DebugGuard.MustBeGreaterThan(bytesPerRow, 0, nameof(bytesPerRow)); 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) if (rowsPerStrip > 0)
{ {

3
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -396,6 +396,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
case TiffCompression.Ccitt1D: case TiffCompression.Ccitt1D:
return (ushort)TiffCompression.Ccitt1D; return (ushort)TiffCompression.Ccitt1D;
case TiffCompression.Jpeg:
return (ushort)TiffCompression.Jpeg;
} }
return (ushort)TiffCompression.None; return (ushort)TiffCompression.None;

4
tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs

@ -40,8 +40,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
this.bmpCore = Image.Load<Rgba32>(this.bmpStream); this.bmpCore = Image.Load<Rgba32>(this.bmpStream);
this.bmpCore.Metadata.ExifProfile = null; this.bmpCore.Metadata.ExifProfile = null;
this.encoder420 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio420 }; this.encoder420 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio420 };
this.encoder444 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio444 }; this.encoder444 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio444 };
this.bmpStream.Position = 0; this.bmpStream.Position = 0;
this.bmpDrawing = SDImage.FromStream(this.bmpStream); this.bmpDrawing = SDImage.FromStream(this.bmpStream);

59
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs

@ -67,16 +67,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
string imagePath, string imagePath,
int expectedPixelSize, int expectedPixelSize,
bool exifProfilePresent, bool exifProfilePresent,
bool iccProfilePresent) bool iccProfilePresent) => TestMetadataImpl(
{
TestMetadataImpl(
useIdentify, useIdentify,
JpegDecoder, JpegDecoder,
imagePath, imagePath,
expectedPixelSize, expectedPixelSize,
exifProfilePresent, exifProfilePresent,
iccProfilePresent); iccProfilePresent);
}
[Theory] [Theory]
[MemberData(nameof(RatioFiles))] [MemberData(nameof(RatioFiles))]
@ -133,8 +130,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var testFile = TestFile.Create(imagePath); var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false)) using (var stream = new MemoryStream(testFile.Bytes, false))
{ {
var decoder = new JpegDecoder(); using (Image<Rgba32> image = JpegDecoder.Decode<Rgba32>(Configuration.Default, stream))
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, stream))
{ {
JpegMetadata meta = image.Metadata.GetJpegMetadata(); JpegMetadata meta = image.Metadata.GetJpegMetadata();
Assert.Equal(quality, meta.Quality); 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<TPixel>(TestImageProvider<TPixel> provider, JpegColorType expectedColorType)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> 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<IImageInfo> test) private static void TestImageInfo(string imagePath, IImageDecoder decoder, bool useIdentify, Action<IImageInfo> test)
{ {
var testFile = TestFile.Create(imagePath); var testFile = TestFile.Create(imagePath);
@ -161,9 +193,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
string imagePath, string imagePath,
int expectedPixelSize, int expectedPixelSize,
bool exifProfilePresent, bool exifProfilePresent,
bool iccProfilePresent) bool iccProfilePresent) => TestImageInfo(
{
TestImageInfo(
imagePath, imagePath,
decoder, decoder,
useIdentify, useIdentify,
@ -207,7 +237,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Null(iccProfile); Assert.Null(iccProfile);
} }
}); });
}
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
@ -237,9 +266,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
public void Decoder_Reads_Correct_Resolution_From_Jfif(bool useIdentify) public void Decoder_Reads_Correct_Resolution_From_Jfif(bool useIdentify) => TestImageInfo(
{
TestImageInfo(
TestImages.Jpeg.Baseline.Floorplan, TestImages.Jpeg.Baseline.Floorplan,
JpegDecoder, JpegDecoder,
useIdentify, useIdentify,
@ -248,14 +275,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(300, imageInfo.Metadata.HorizontalResolution); Assert.Equal(300, imageInfo.Metadata.HorizontalResolution);
Assert.Equal(300, imageInfo.Metadata.VerticalResolution); Assert.Equal(300, imageInfo.Metadata.VerticalResolution);
}); });
}
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
public void Decoder_Reads_Correct_Resolution_From_Exif(bool useIdentify) public void Decoder_Reads_Correct_Resolution_From_Exif(bool useIdentify) => TestImageInfo(
{
TestImageInfo(
TestImages.Jpeg.Baseline.Jpeg420Exif, TestImages.Jpeg.Baseline.Jpeg420Exif,
JpegDecoder, JpegDecoder,
useIdentify, useIdentify,
@ -264,6 +288,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(72, imageInfo.Metadata.HorizontalResolution); Assert.Equal(72, imageInfo.Metadata.HorizontalResolution);
Assert.Equal(72, imageInfo.Metadata.VerticalResolution); Assert.Equal(72, imageInfo.Metadata.VerticalResolution);
}); });
}
} }
} }

2
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -193,7 +193,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public void Issue1732_DecodesWithRgbColorSpace<TPixel>(TestImageProvider<TPixel> provider) public void Issue1732_DecodesWithRgbColorSpace<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage(new JpegDecoder())) using (Image<TPixel> image = provider.GetImage(JpegDecoder))
{ {
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToOriginal(provider); image.CompareToOriginal(provider);

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

@ -24,6 +24,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Trait("Format", "Jpg")] [Trait("Format", "Jpg")]
public class JpegEncoderTests public class JpegEncoderTests
{ {
private static JpegEncoder JpegEncoder => new JpegEncoder();
private static JpegDecoder JpegDecoder => new JpegDecoder();
public static readonly TheoryData<string, int> QualityFiles = public static readonly TheoryData<string, int> QualityFiles =
new TheoryData<string, int> new TheoryData<string, int>
{ {
@ -31,15 +35,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ TestImages.Jpeg.Progressive.Fb, 75 } { TestImages.Jpeg.Progressive.Fb, 75 }
}; };
public static readonly TheoryData<JpegSubsample, int> BitsPerPixel_Quality = public static readonly TheoryData<JpegColorType, int> BitsPerPixel_Quality =
new TheoryData<JpegSubsample, int> new TheoryData<JpegColorType, int>
{ {
{ JpegSubsample.Ratio420, 40 }, { JpegColorType.YCbCrRatio420, 40 },
{ JpegSubsample.Ratio420, 60 }, { JpegColorType.YCbCrRatio420, 60 },
{ JpegSubsample.Ratio420, 100 }, { JpegColorType.YCbCrRatio420, 100 },
{ JpegSubsample.Ratio444, 40 }, { JpegColorType.YCbCrRatio444, 40 },
{ JpegSubsample.Ratio444, 60 }, { JpegColorType.YCbCrRatio444, 60 },
{ JpegSubsample.Ratio444, 100 }, { JpegColorType.YCbCrRatio444, 100 },
{ JpegColorType.Rgb, 40 },
{ JpegColorType.Rgb, 60 },
{ JpegColorType.Rgb, 100 }
}; };
public static readonly TheoryData<int> Grayscale_Quality = public static readonly TheoryData<int> Grayscale_Quality =
@ -59,17 +66,84 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}; };
[Theory] [Theory]
[MemberData(nameof(QualityFiles))] [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32, JpegColorType.Luminance)]
public void Encode_PreserveQuality(string imagePath, int quality) [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<TPixel>(TestImageProvider<TPixel> provider, JpegColorType expectedColorType)
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
using Image<TPixel> input = provider.GetImage(JpegDecoder);
using var memoryStream = new MemoryStream();
// act
input.Save(memoryStream, JpegEncoder);
// assert
memoryStream.Position = 0;
using var output = Image.Load<Rgba32>(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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
using Image<TPixel> 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<Rgba32>(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<Rgb24>(10, 10);
using var memoryStream = new MemoryStream();
// act
input.Save(memoryStream, jpegEncoder);
// assert
memoryStream.Position = 0;
using var output = Image.Load<Rgba32>(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); var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateRgba32Image()) using (Image<Rgba32> input = testFile.CreateRgba32Image())
{ {
using (var memStream = new MemoryStream()) using (var memStream = new MemoryStream())
{ {
input.Save(memStream, options); input.Save(memStream, JpegEncoder);
memStream.Position = 0; memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream)) using (var output = Image.Load<Rgba32>(memStream))
@ -83,16 +157,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory] [Theory]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)] [WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)]
public void EncodeBaseline_CalliphoraPartial<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality)
where TPixel : unmanaged, IPixel<TPixel> => 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<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality)
where TPixel : unmanaged, IPixel<TPixel> => 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), 73, 71, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 24, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 24, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 46, 8, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 46, 8, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 51, 7, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 51, 7, PixelTypes.Rgba32)]
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] public void EncodeBaseline_WithSmallImages_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality)
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)] where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.12f));
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 600, 400, PixelTypes.Rgba32)]
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 100, 100, 100, 255, PixelTypes.L8)]
public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality);
[Theory] [Theory]
[WithFile(TestImages.Png.BikeGrayscale, nameof(Grayscale_Quality), PixelTypes.L8)] [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.La16, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La32, 100)] [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La32, 100)]
public void EncodeBaseline_Grayscale<TPixel>(TestImageProvider<TPixel> provider, int quality) public void EncodeBaseline_Grayscale<TPixel>(TestImageProvider<TPixel> provider, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, null, quality, JpegColorType.Luminance); where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, JpegColorType.Luminance, quality);
[Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 96, 96, PixelTypes.Rgba32 | PixelTypes.Bgra32)]
public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality);
[Theory] [Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)]
public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality) public void EncodeBaseline_WithSmallImages_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality); where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.06f));
[Theory] [Theory]
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, JpegSubsample.Ratio444)] [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)]
[WithTestPatternImages(587, 821, PixelTypes.Rgba32, JpegSubsample.Ratio444)] [WithTestPatternImages(587, 821, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)]
[WithTestPatternImages(677, 683, PixelTypes.Bgra32, JpegSubsample.Ratio420)] [WithTestPatternImages(677, 683, PixelTypes.Bgra32, JpegColorType.YCbCrRatio420)]
[WithSolidFilledImages(400, 400, "Red", PixelTypes.Bgr24, JpegSubsample.Ratio420)] [WithSolidFilledImages(400, 400, "Red", PixelTypes.Bgr24, JpegColorType.YCbCrRatio420)]
public void EncodeBaseline_WorksWithDiscontiguousBuffers<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample) public void EncodeBaseline_WorksWithDiscontiguousBuffers<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageComparer comparer = subsample == JpegSubsample.Ratio444 ImageComparer comparer = colorType == JpegColorType.YCbCrRatio444
? ImageComparer.TolerantPercentage(0.1f) ? ImageComparer.TolerantPercentage(0.1f)
: ImageComparer.TolerantPercentage(5f); : ImageComparer.TolerantPercentage(5f);
provider.LimitAllocatorBufferCapacity().InBytesSqrt(200); provider.LimitAllocatorBufferCapacity().InBytesSqrt(200);
TestJpegEncoderCore(provider, subsample, 100, JpegColorType.YCbCr, comparer); TestJpegEncoderCore(provider, colorType, 100, comparer);
} }
/// <summary> /// <summary>
/// Anton's SUPER-SCIENTIFIC tolerance threshold calculation /// Anton's SUPER-SCIENTIFIC tolerance threshold calculation
/// </summary> /// </summary>
private static ImageComparer GetComparer(int quality, JpegSubsample? subsample) private static ImageComparer GetComparer(int quality, JpegColorType? colorType)
{ {
float tolerance = 0.015f; // ~1.5% float tolerance = 0.015f; // ~1.5%
if (quality < 50) if (quality < 50)
{ {
tolerance *= 10f; tolerance *= 4.5f;
} }
else if (quality < 75 || subsample == JpegSubsample.Ratio420) else if (quality < 75 || colorType == JpegColorType.YCbCrRatio420)
{ {
tolerance *= 5f; tolerance *= 2.0f;
if (subsample == JpegSubsample.Ratio420) if (colorType == JpegColorType.YCbCrRatio420)
{ {
tolerance *= 2f; tolerance *= 2.0f;
} }
} }
@ -150,9 +243,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private static void TestJpegEncoderCore<TPixel>( private static void TestJpegEncoderCore<TPixel>(
TestImageProvider<TPixel> provider, TestImageProvider<TPixel> provider,
JpegSubsample? subsample, JpegColorType colorType = JpegColorType.YCbCrRatio420,
int quality = 100, int quality = 100,
JpegColorType colorType = JpegColorType.YCbCr,
ImageComparer comparer = null) ImageComparer comparer = null)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -163,13 +255,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var encoder = new JpegEncoder var encoder = new JpegEncoder
{ {
Subsample = subsample,
Quality = quality, Quality = quality,
ColorType = colorType 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(): // Does DebugSave & load reference CompareToReferenceInput():
image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "png"); image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "png");
@ -225,14 +316,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[MemberData(nameof(RatioFiles))] [MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{ {
var options = new JpegEncoder();
var testFile = TestFile.Create(imagePath); var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateRgba32Image()) using (Image<Rgba32> input = testFile.CreateRgba32Image())
{ {
using (var memStream = new MemoryStream()) using (var memStream = new MemoryStream())
{ {
input.Save(memStream, options); input.Save(memStream, JpegEncoder);
memStream.Position = 0; memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream)) using (var output = Image.Load<Rgba32>(memStream))
@ -253,11 +342,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var input = new Image<Rgba32>(1, 1); using var input = new Image<Rgba32>(1, 1);
input.Metadata.IptcProfile = new IptcProfile(); input.Metadata.IptcProfile = new IptcProfile();
input.Metadata.IptcProfile.SetValue(IptcTag.Byline, "unit_test"); input.Metadata.IptcProfile.SetValue(IptcTag.Byline, "unit_test");
var encoder = new JpegEncoder();
// act // act
using var memStream = new MemoryStream(); using var memStream = new MemoryStream();
input.Save(memStream, encoder); input.Save(memStream, JpegEncoder);
// assert // assert
memStream.Position = 0; memStream.Position = 0;
@ -275,11 +363,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var input = new Image<Rgba32>(1, 1); using var input = new Image<Rgba32>(1, 1);
input.Metadata.ExifProfile = new ExifProfile(); input.Metadata.ExifProfile = new ExifProfile();
input.Metadata.ExifProfile.SetValue(ExifTag.Software, "unit_test"); input.Metadata.ExifProfile.SetValue(ExifTag.Software, "unit_test");
var encoder = new JpegEncoder();
// act // act
using var memStream = new MemoryStream(); using var memStream = new MemoryStream();
input.Save(memStream, encoder); input.Save(memStream, JpegEncoder);
// assert // assert
memStream.Position = 0; memStream.Position = 0;
@ -296,11 +383,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// arrange // arrange
using var input = new Image<Rgba32>(1, 1); using var input = new Image<Rgba32>(1, 1);
input.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.Profile_Random_Array); input.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.Profile_Random_Array);
var encoder = new JpegEncoder();
// act // act
using var memStream = new MemoryStream(); using var memStream = new MemoryStream();
input.Save(memStream, encoder); input.Save(memStream, JpegEncoder);
// assert // assert
memStream.Position = 0; memStream.Position = 0;
@ -312,9 +398,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
} }
[Theory] [Theory]
[InlineData(JpegSubsample.Ratio420)] [InlineData(JpegColorType.YCbCrRatio420)]
[InlineData(JpegSubsample.Ratio444)] [InlineData(JpegColorType.YCbCrRatio444)]
public async Task Encode_IsCancellable(JpegSubsample subsample) public async Task Encode_IsCancellable(JpegColorType colorType)
{ {
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
using var pausedStream = new PausedStream(new MemoryStream()); using var pausedStream = new PausedStream(new MemoryStream());
@ -336,7 +422,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var image = new Image<Rgba32>(5000, 5000); using var image = new Image<Rgba32>(5000, 5000);
await Assert.ThrowsAsync<TaskCanceledException>(async () => await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{ {
var encoder = new JpegEncoder() { Subsample = subsample }; var encoder = new JpegEncoder() { ColorType = colorType };
await image.SaveAsync(pausedStream, encoder, cts.Token); await image.SaveAsync(pausedStream, encoder, cts.Token);
}); });
} }

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

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var clone = (JpegMetadata)meta.DeepClone(); var clone = (JpegMetadata)meta.DeepClone();
clone.Quality = 99; clone.Quality = 99;
clone.ColorType = JpegColorType.YCbCr; clone.ColorType = JpegColorType.YCbCrRatio420;
Assert.False(meta.Quality.Equals(clone.Quality)); Assert.False(meta.Quality.Equals(clone.Quality));
Assert.False(meta.ColorType.Equals(clone.ColorType)); Assert.False(meta.ColorType.Equals(clone.ColorType));

6
tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs

@ -21,10 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Trait("Format", "Jpg")] [Trait("Format", "Jpg")]
public class SpectralJpegTests public class SpectralJpegTests
{ {
public SpectralJpegTests(ITestOutputHelper output) public SpectralJpegTests(ITestOutputHelper output) => this.Output = output;
{
this.Output = output;
}
private ITestOutputHelper Output { get; } private ITestOutputHelper Output { get; }
@ -46,7 +43,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public static readonly string[] AllTestJpegs = BaselineTestJpegs.Concat(ProgressiveTestJpegs).ToArray(); public static readonly string[] AllTestJpegs = BaselineTestJpegs.Concat(ProgressiveTestJpegs).ToArray();
[Theory(Skip = "Debug only, enable manually!")] [Theory(Skip = "Debug only, enable manually!")]
//[Theory]
[WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)] [WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)]
public void Decoder_ParseStream_SaveSpectralResult<TPixel>(TestImageProvider<TPixel> provider) public void Decoder_ParseStream_SaveSpectralResult<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>

12
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -28,8 +28,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
private static MagickReferenceDecoder ReferenceDecoder => new MagickReferenceDecoder(); private static MagickReferenceDecoder ReferenceDecoder => new MagickReferenceDecoder();
[Theory] [Theory]
[WithFile(Calliphora_RgbJpeg, PixelTypes.Rgba32)]
[WithFile(RgbJpeg, PixelTypes.Rgba32)]
[WithFile(RgbUncompressedTiled, PixelTypes.Rgba32)] [WithFile(RgbUncompressedTiled, PixelTypes.Rgba32)]
[WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)]
[WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)]
@ -39,7 +37,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Theory] [Theory]
[InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)] [InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)]
[InlineData(SmallRgbDeflate, 24, 32, 32, 96, 96, 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)] [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) 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<TPixel>(TestImageProvider<TPixel> provider) public void TiffDecoder_CanDecode_PackBitsCompressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider); where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(RgbJpegCompressed, PixelTypes.Rgba32)]
[WithFile(RgbWithStripsJpegCompressed, PixelTypes.Rgba32)]
[WithFile(YCbCrJpegCompressed, PixelTypes.Rgba32)]
[WithFile(RgbJpegCompressedNoJpegTable, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_JpegCompressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false);
[Theory] [Theory]
[WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)] [WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)]
public void DecodeMultiframe<TPixel>(TestImageProvider<TPixel> provider) public void DecodeMultiframe<TPixel>(TestImageProvider<TPixel> provider)

7
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.BlackIsZero, TiffCompression.Ccitt1D, TiffBitsPerPixel.Bit1, TiffCompression.Ccitt1D)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT43, TiffBitsPerPixel.Bit24, TiffCompression.None)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT43, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT82, 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.OldDeflate, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldJpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldJpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)]
public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works( public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(
@ -288,6 +288,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.PackBits); where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.PackBits);
[Theory]
[WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeRgb_WithJpegCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, useExactComparer: false, compareTolerance: 0.012f);
[Theory] [Theory]
[WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeGray_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeGray_Works<TPixel>(TestImageProvider<TPixel> provider)

14
tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs

@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
img.Dispose(); img.Dispose();
}, },
#pragma warning disable SA1515 // Single-line comment should be preceded by blank line #pragma warning disable SA1515 // Single-line comment should be preceded by blank line
// ReSharper disable once ExplicitCallerInfoArgument // ReSharper disable once ExplicitCallerInfoArgument
$"Decode {fileName}"); $"Decode {fileName}");
#pragma warning restore SA1515 // Single-line comment should be preceded by blank line #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! // Benchmark, enable manually!
[Theory(Skip = ProfilingSetup.SkipProfilingTests)] [Theory(Skip = ProfilingSetup.SkipProfilingTests)]
[InlineData(1, 75, JpegSubsample.Ratio420)] [InlineData(1, 75, JpegColorType.YCbCrRatio420)]
[InlineData(30, 75, JpegSubsample.Ratio420)] [InlineData(30, 75, JpegColorType.YCbCrRatio420)]
[InlineData(30, 75, JpegSubsample.Ratio444)] [InlineData(30, 75, JpegColorType.YCbCrRatio444)]
[InlineData(30, 100, JpegSubsample.Ratio444)] [InlineData(30, 100, JpegColorType.YCbCrRatio444)]
public void EncodeJpeg(int executionCount, int quality, JpegSubsample subsample) public void EncodeJpeg(int executionCount, int quality, JpegColorType colorType)
{ {
// do not run this on CI even by accident // do not run this on CI even by accident
if (TestEnvironment.RunsOnCI) if (TestEnvironment.RunsOnCI)
@ -118,7 +118,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{ {
foreach (Image<Rgba32> img in testImages) foreach (Image<Rgba32> img in testImages)
{ {
var options = new JpegEncoder { Quality = quality, Subsample = subsample }; var options = new JpegEncoder { Quality = quality, ColorType = colorType };
img.Save(ms, options); img.Save(ms, options);
ms.Seek(0, SeekOrigin.Begin); ms.Seek(0, SeekOrigin.Begin);
} }

9
tests/ImageSharp.Tests/TestImages.cs

@ -190,6 +190,10 @@ namespace SixLabors.ImageSharp.Tests
public const string Jpeg420Exif = "Jpg/baseline/jpeg420exif.jpg"; public const string Jpeg420Exif = "Jpg/baseline/jpeg420exif.jpg";
public const string Jpeg444 = "Jpg/baseline/jpeg444.jpg"; public const string Jpeg444 = "Jpg/baseline/jpeg444.jpg";
public const string Jpeg420Small = "Jpg/baseline/jpeg420small.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 Testorig420 = "Jpg/baseline/testorig.jpg";
public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg"; public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg";
public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.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 RgbDeflate = "Tiff/rgb_deflate.tiff";
public const string RgbDeflatePredictor = "Tiff/rgb_deflate_predictor.tiff"; public const string RgbDeflatePredictor = "Tiff/rgb_deflate_predictor.tiff";
public const string RgbDeflateMultistrip = "Tiff/rgb_deflate_multistrip.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 RgbLzwPredictor = "Tiff/rgb_lzw_predictor.tiff";
public const string RgbLzwNoPredictor = "Tiff/rgb_lzw_no_predictor.tiff"; public const string RgbLzwNoPredictor = "Tiff/rgb_lzw_no_predictor.tiff";
public const string RgbLzwNoPredictorMultistrip = "Tiff/rgb_lzw_noPredictor_multistrip.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 RgbYCbCr888Contiguoush2v1 = "Tiff/rgb-ycbcr-contig-08_h2v1.tiff";
public const string RgbYCbCr888Contiguoush2v2 = "Tiff/rgb-ycbcr-contig-08_h2v2.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 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 FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff";
public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff"; public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff";
public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff"; public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff";

3
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

3
tests/Images/Input/Jpg/baseline/jpeg410.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:318338c1a541227632a99cc47b04fc9f6d19c3e8300a76e71136edec2d17a5f5
size 9073

3
tests/Images/Input/Jpg/baseline/jpeg411.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:70315936e36bb1edf38fc950b7b321f20be5a2592db59bfd28d79dbc391a5aaf
size 4465

3
tests/Images/Input/Jpg/baseline/jpeg422.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be21207ecb96bcb0925706649678c522c68bf08ee26e6e8878456f4f7772dd31
size 3951

4
tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ed3b08730e5c34eb8d268f58d1e09efe2605398899bfd726cc3b35de21baa6ff oid sha256:c35902ca485fba441230efa88f794ee5aafa9f75ad5e4a14cb3d592a0a98c538
size 121196 size 7760

4
tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bba35f1e43c8425f3bcfab682efae4d2c00c62f0d8a4b411e646d32047469526 oid sha256:da7d98823c284d92982a88c4a51434bbc140dceac245a8a054c6e41a489d0cc7
size 125802 size 5986

4
tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b8c7f712f9e7d1feeeb55e7743f6ce7d66bc5292f4786ea8526d95057d73145e oid sha256:118e55fe0f0dbb5d2712337ec9456a27407f11c0eb9f7e7e81700d4d84b7db09
size 4534 size 4378

4
tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2314b31ca9938fa8b11cbabda0b118a90025a45d2931fca9afa131c0d6919aca oid sha256:d4d3541db6b7751d3225599aa822c3376fb27bb9dc2dd28674ef4f78bf426191
size 557717 size 83356

4
tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b9576b3a49b84e26938a7e9ded5f43a1a3c3390bf4824803f5aaab8e00c1afb4 oid sha256:d007429701cc20154e84909af6988414001e6e60fba5c995f67b2a04cad6e57b
size 630947 size 41135

4
tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3f24fd8f36a4847fcb84a317de4fd2eacd5eb0c58ef4436d33919f0a6658d0d9 oid sha256:649f0b8ad50a9465fdb447c411e33f20a8b367600e7c3af83c8f686f3ab3e6dc
size 698309 size 47143

4
tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0283f2be39a151ca3ed19be97ebe4a6b17978ed251dd4d0d568895865fec24c7 oid sha256:5925388b374b75161ef49273e8c85c4f99713e5e3be380ea13a03895f47809ba
size 964588 size 60001

4
tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1a4f687de9925863b1c9f32f53b6c05fb121f9d7b02ff5869113c4745433f10d oid sha256:f27e758bb72d5e03fdcf0b1c58c417e4a7928cbdf8d432dc5b9a7d8d7ee4d06b
size 124644 size 5668

4
tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b70500348b1af7828c15e7782eaca105ff749136d7c45eb4cab8c5cd5269c3f6 oid sha256:29d4b30265158a7cc651d75130843a7f5a7ebf8b2f0f4bb0cf86c82cbec7f6ec
size 966134 size 61549

4
tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:da6e6a35a0bb0f5f2d49e3c5f0eb2deb7118718dd08844f66a6cb72f48b5c489 oid sha256:392e1269220e7e3feb9e2b256e82cce6a2394a5cabef042fdb10def6b24ff165
size 1476294 size 111819

4
tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ba79ffac35e16208406e073492d770d3a77e51a47112aa02ab8fd98b5a8487b2 oid sha256:4baf0f4c462e5bef71ab36f505dfff87a31bd1d25deabccd822a359c1075e08e
size 198564 size 65748

3
tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:53006876fcdc655a794462de57eb6b56f4d0cdd3cb8b752c63328db0eb4aa3c1
size 725085

3
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

4
tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ecb529e5e3e0eca6f5e407b034fa8ba67bb4b9068af9e9b30425b08d30a249c0 oid sha256:993207c34358165af5fac0d0b955b56cd79e9707c1c46344863e01cbc9c7707d
size 1756355 size 126695

4
tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:59dbb48f10c40cbbd4f5617a9f57536790ce0b9a4cc241dc8d6257095598cb76 oid sha256:81e7456578510c85e5bebe8bc7c5796da6e2cd61f5bbe0a1f6bb46b8aee3d695
size 2891292 size 179949

4
tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:acb46c990af78fcb0e63849f0f26acffe26833d5cfd2fc77a728baf92166eea3 oid sha256:bb25349a7f803aeafc47d8a05deca1a8afdc4bdc5a53e2916f68d5c3e7d8cad3
size 2893218 size 179207

3
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

3
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

0
tests/Images/Input/Tiff/rgb_jpeg.tiff → tests/Images/Input/Tiff/rgb_jpegcompression.tiff

3
tests/Images/Input/Tiff/ycbcr_jpegcompressed.tiff

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:27fa1d37cd62a9cf105a5e3e015e6a16a13ca7fa82f927a73d32847046c66073
size 6136
Loading…
Cancel
Save