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>
private int restartInterval;
// How many mcu's are left to do.
/// <summary>
/// How many mcu's are left to do.
/// </summary>
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;
/// <summary>
@ -54,14 +58,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </summary>
private readonly HuffmanTable[] acHuffmanTables;
// The unzig data.
/// <summary>
/// The unzig data.
/// </summary>
private ZigZag dctZigZag;
private HuffmanScanBuffer scanBuffer;
private readonly SpectralConverter spectralConverter;
private CancellationToken cancellationToken;
private readonly CancellationToken cancellationToken;
/// <summary>
/// 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.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
@ -30,5 +32,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// Actual stride height depends on the subsampling factor of the given component.
/// </remarks>
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
{
internal sealed class SpectralConverter<TPixel> : SpectralConverter, IDisposable
internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Configuration configuration;
private CancellationToken cancellationToken;
private readonly CancellationToken cancellationToken;
private JpegComponentPostProcessor[] componentProcessors;
@ -59,6 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
}
/// <inheritdoc/>
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
MemoryAllocator allocator = this.configuration.MemoryAllocator;
@ -85,9 +86,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.rgbaBuffer = allocator.Allocate<Vector4>(frame.PixelWidth);
// 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()
{
// 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;
/// <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>
private int accumulatedBits;
@ -58,18 +58,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </summary>
private readonly Stream target;
public HuffmanScanEncoder(Stream outputStream)
{
this.target = outputStream;
}
public HuffmanScanEncoder(Stream outputStream) => this.target = outputStream;
/// <summary>
/// Encodes the image with no subsampling.
/// </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="chrominanceQuantTable">Chrominance 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="cancellationToken">The token to monitor for cancellation.</param>
public void Encode444<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
@ -128,8 +125,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </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="chrominanceQuantTable">Chrominance 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="cancellationToken">The token to monitor for cancellation.</param>
public void Encode420<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
@ -196,7 +193,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </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="luminanceQuantTable">Luminance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void EncodeGrayscale<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
@ -234,6 +231,64 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
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>
/// Writes a block of pixel data using the given quantization table,
/// returning the post-quantized DC value of the DCT-transformed block.
@ -437,7 +492,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
DebugGuard.IsTrue(value <= (1 << 16), "Huffman encoder is supposed to encode a value of 16bit size max");
#if SUPPORTS_BITOPERATIONS
// This should have been implemented as (BitOperations.Log2(value) + 1) as in non-intrinsic implementation
// But internal log2 is implementated like this: (31 - (int)Lzcnt.LeadingZeroCount(value))
// But internal log2 is implemented like this: (31 - (int)Lzcnt.LeadingZeroCount(value))
// BitOperations.Log2 implementation also checks if input value is zero for the convention 0->0
// Lzcnt would return 32 for input value of 0 - no need to check that with branching
@ -449,7 +504,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
// if 0 - return 0 in this case
// else - return log2(value) + 1
//
// Hack based on input value constaint:
// Hack based on input value constraint:
// We know that input values are guaranteed to be maximum 16 bit large for huffman encoding
// We can safely shift input value for one bit -> log2(value << 1)
// Because of the 16 bit value constraint it won't overflow

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.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// Enumerates the quantization tables
/// Enumerates the quantization tables.
/// </summary>
internal enum QuantIndex
{
/// <summary>
/// The luminance quantization table index
/// The luminance quantization table index.
/// </summary>
Luminance = 0,
/// <summary>
/// The chrominance quantization table index
/// The chrominance quantization table index.
/// </summary>
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>
/// Temporal 16x8 block to hold TPixel data
/// </summary>
private Span<TPixel> pixelSpan;
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal RGB block
/// </summary>
private Span<Rgb24> rgbSpan;
private readonly Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size
/// </summary>
private Size samplingAreaSize;
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations
/// </summary>
private Configuration config;
private readonly Configuration config;
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>
/// Temporal 64-byte span to hold unconverted TPixel data
/// </summary>
private Span<TPixel> pixelSpan;
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal 64-byte span to hold converted Rgb24 data
/// </summary>
private Span<Rgb24> rgbSpan;
private readonly Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size
/// </summary>
private Size samplingAreaSize;
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations
/// </summary>
private Configuration config;
private readonly Configuration config;
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; }
/// <summary>
/// Gets the subsample ration, 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.
/// Gets the color type, that will be used to encode the image.
/// </summary>
JpegColorType? ColorType { get; }
}

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

@ -10,12 +10,57 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{
/// <summary>
/// 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>
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>
/// Single channel, luminance.
/// </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.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
@ -136,8 +137,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// Finds the next file marker within the byte stream.
/// </summary>
/// <param name="marker">The buffer to read file markers to</param>
/// <param name="stream">The input stream</param>
/// <param name="marker">The buffer to read file markers to.</param>
/// <param name="stream">The input stream.</param>
/// <returns>The <see cref="JpegFileMarker"/></returns>
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);
}
/// <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>
/// Parses the input stream for file markers.
/// </summary>
@ -225,7 +287,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
stream.Read(this.markerBuffer, 0, 2);
byte marker = this.markerBuffer[1];
fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2);
this.QuantizationTables = new Block8x8F[4];
this.QuantizationTables ??= new Block8x8F[4];
// Break only when we discover a valid EOI marker.
// https://github.com/SixLabors/ImageSharp/issues/695
@ -236,7 +298,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (!fileMarker.Invalid)
{
// Get the marker length
// Get the marker length.
int remaining = this.ReadUint16(stream) - 2;
switch (fileMarker.Marker)
@ -272,7 +334,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.SOS:
if (!metadataOnly)
{
this.ProcessStartOfScanMarker(stream, remaining, cancellationToken);
this.ProcessStartOfScanMarker(stream, remaining);
break;
}
else
@ -371,10 +433,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
/// <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>
/// <param name="componentCount">The number of components.</param>
/// <returns>The <see cref="JpegColorSpace"/></returns>
private JpegColorSpace DeduceJpegColorSpace(byte componentCount, JpegComponent[] components)
private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
{
if (componentCount == 1)
{
@ -389,7 +452,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
// If the component Id's are R, G, B in ASCII the colorspace is RGB and not YCbCr.
if (components[2].Id == 66 && components[1].Id == 71 && components[0].Id == 82)
if (this.Components[2].Id == 66 && this.Components[1].Id == 71 && this.Components[0].Id == 82)
{
return JpegColorSpace.RGB;
}
@ -410,6 +473,64 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return default;
}
/// <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>
/// Initializes the EXIF profile.
/// </summary>
@ -608,7 +729,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// 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>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
@ -823,7 +944,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
/// <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>
/// <param name="stream">The input stream.</param>
/// <param name="remaining">The remaining bytes in the segment block.</param>
@ -915,9 +1036,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
index += componentBytes;
}
this.ColorSpace = this.DeduceJpegColorSpace(componentCount, this.Frame.Components);
this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
this.ColorSpace = this.DeduceJpegColorSpace(componentCount);
this.Metadata.GetJpegMetadata().ColorType = this.DeduceJpegColorType();
if (!metadataOnly)
{
@ -1015,7 +1135,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// Processes the SOS (Start of scan marker).
/// </summary>
private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining, CancellationToken cancellationToken)
private void ProcessStartOfScanMarker(BufferedReadStream stream, int remaining)
{
if (this.Frame is null)
{

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

@ -16,14 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <inheritdoc/>
public int? Quality { get; set; }
/// <summary>
/// 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>
/// <inheritdoc/>
public JpegColorType? ColorType { get; set; }
/// <summary>
@ -36,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel>
{
var encoder = new JpegEncoderCore(this);
this.InitializeColorType<TPixel>(image);
this.InitializeColorType(image);
encoder.Encode(image, stream);
}
@ -52,7 +45,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel>
{
var encoder = new JpegEncoderCore(this);
this.InitializeColorType<TPixel>(image);
this.InitializeColorType(image);
return encoder.EncodeAsync(image, stream, cancellationToken);
}
@ -75,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
bool isGrayscale =
typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) ||
typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32);
this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCrRatio420;
}
}
}

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

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

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)
{
// We consider run which has at least 3 same consecutive bytes a candidate for a run.
var startByte = rowSpan[startPos];
byte startByte = rowSpan[startPos];
int count = 0;
for (int i = startPos + 1; i < rowSpan.Length; i++)
{

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

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

@ -36,11 +36,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
/// <summary>
/// Image data is compressed using CCITT T.6 fax compression.
/// </summary>
T6 = 6,
T6 = 5,
/// <summary>
/// Image data is compressed using modified huffman compression.
/// </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
{
public static TiffBaseDecompressor Create(
Configuration configuration,
TiffDecoderCompressionType method,
MemoryAllocator allocator,
TiffPhotometricInterpretation photometricInterpretation,
@ -19,6 +20,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
TiffColorType colorType,
TiffPredictor predictor,
FaxCompressionOptions faxOptions,
byte[] jpegTables,
TiffFillOrder fillOrder,
ByteOrder byteOrder)
{
@ -54,6 +56,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation);
case TiffDecoderCompressionType.Jpeg:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new JpegTiffCompression(configuration, allocator, width, bitsPerPixel, jpegTables, photometricInterpretation);
default:
throw TiffThrowHelper.NotSupportedDecompressor(nameof(method));
}

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

@ -48,7 +48,7 @@
|CcittGroup4Fax | | Y | |
|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case |
|Old Jpeg | | | We should not even try to support this |
|Jpeg (Technote 2) | | | |
|Jpeg (Technote 2) | Y | Y | |
|Deflate (Technote 2) | Y | Y | Based on PNG Deflate. |
|Old Deflate (Technote 2) | | Y | |

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

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

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

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

@ -155,6 +155,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
Image<TPixel> metadataImage = image;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
cancellationToken.ThrowIfCancellationRequested();
var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile?.GetValue(ExifTag.SubfileType)?.Value ?? (int)TiffNewSubfileType.FullImage);
ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker);
@ -223,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
entriesCollector,
(int)this.BitsPerPixel);
int rowsPerStrip = this.CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow);
int rowsPerStrip = this.CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType);
colorWriter.Write(compressor, rowsPerStrip);
@ -245,13 +247,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
/// <param name="height">The height of the image.</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>
private int CalcRowsPerStrip(int height, int bytesPerRow)
private int CalcRowsPerStrip(int height, int bytesPerRow, TiffCompression? compression)
{
DebugGuard.MustBeGreaterThan(height, 0, nameof(height));
DebugGuard.MustBeGreaterThan(bytesPerRow, 0, nameof(bytesPerRow));
int rowsPerStrip = TiffConstants.DefaultStripSize / bytesPerRow;
// Jpeg compressed images should be written in one strip.
if (compression is TiffCompression.Jpeg)
{
return height;
}
// If compression is used, change stripSizeInBytes heuristically to a larger value to not write to many strips.
int stripSizeInBytes = compression is TiffCompression.Deflate || compression is TiffCompression.Lzw ? TiffConstants.DefaultStripSize * 2 : TiffConstants.DefaultStripSize;
int rowsPerStrip = stripSizeInBytes / bytesPerRow;
if (rowsPerStrip > 0)
{

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

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

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.Metadata.ExifProfile = null;
this.encoder420 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio420 };
this.encoder444 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio444 };
this.encoder420 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio420 };
this.encoder444 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio444 };
this.bmpStream.Position = 0;
this.bmpDrawing = SDImage.FromStream(this.bmpStream);

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

@ -67,16 +67,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
string imagePath,
int expectedPixelSize,
bool exifProfilePresent,
bool iccProfilePresent)
{
TestMetadataImpl(
bool iccProfilePresent) => TestMetadataImpl(
useIdentify,
JpegDecoder,
imagePath,
expectedPixelSize,
exifProfilePresent,
iccProfilePresent);
}
[Theory]
[MemberData(nameof(RatioFiles))]
@ -133,8 +130,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new JpegDecoder();
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, stream))
using (Image<Rgba32> image = JpegDecoder.Decode<Rgba32>(Configuration.Default, stream))
{
JpegMetadata meta = image.Metadata.GetJpegMetadata();
Assert.Equal(quality, meta.Quality);
@ -142,6 +138,42 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
}
[Theory]
[InlineData(TestImages.Jpeg.Baseline.Floorplan, JpegColorType.Luminance)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, JpegColorType.YCbCrRatio420)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg444, JpegColorType.YCbCrRatio444)]
[InlineData(TestImages.Jpeg.Baseline.JpegRgb, JpegColorType.Rgb)]
[InlineData(TestImages.Jpeg.Baseline.Cmyk, JpegColorType.Cmyk)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg410, JpegColorType.YCbCrRatio410)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg422, JpegColorType.YCbCrRatio422)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg411, JpegColorType.YCbCrRatio411)]
public void Identify_DetectsCorrectColorType(string imagePath, JpegColorType expectedColorType)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
IImageInfo image = JpegDecoder.Identify(Configuration.Default, stream);
JpegMetadata meta = image.Metadata.GetJpegMetadata();
Assert.Equal(expectedColorType, meta.ColorType);
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32, JpegColorType.Luminance)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgba32, JpegColorType.YCbCrRatio420)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)]
[WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgba32, JpegColorType.Rgb)]
[WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgba32, JpegColorType.Cmyk)]
public void Decode_DetectsCorrectColorType<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)
{
var testFile = TestFile.Create(imagePath);
@ -161,9 +193,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
string imagePath,
int expectedPixelSize,
bool exifProfilePresent,
bool iccProfilePresent)
{
TestImageInfo(
bool iccProfilePresent) => TestImageInfo(
imagePath,
decoder,
useIdentify,
@ -207,7 +237,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Null(iccProfile);
}
});
}
[Theory]
[InlineData(false)]
@ -237,9 +266,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Decoder_Reads_Correct_Resolution_From_Jfif(bool useIdentify)
{
TestImageInfo(
public void Decoder_Reads_Correct_Resolution_From_Jfif(bool useIdentify) => TestImageInfo(
TestImages.Jpeg.Baseline.Floorplan,
JpegDecoder,
useIdentify,
@ -248,14 +275,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(300, imageInfo.Metadata.HorizontalResolution);
Assert.Equal(300, imageInfo.Metadata.VerticalResolution);
});
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Decoder_Reads_Correct_Resolution_From_Exif(bool useIdentify)
{
TestImageInfo(
public void Decoder_Reads_Correct_Resolution_From_Exif(bool useIdentify) => TestImageInfo(
TestImages.Jpeg.Baseline.Jpeg420Exif,
JpegDecoder,
useIdentify,
@ -264,6 +288,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(72, imageInfo.Metadata.HorizontalResolution);
Assert.Equal(72, imageInfo.Metadata.VerticalResolution);
});
}
}
}

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

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

@ -21,10 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Trait("Format", "Jpg")]
public class SpectralJpegTests
{
public SpectralJpegTests(ITestOutputHelper output)
{
this.Output = output;
}
public SpectralJpegTests(ITestOutputHelper output) => this.Output = output;
private ITestOutputHelper Output { get; }
@ -46,7 +43,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public static readonly string[] AllTestJpegs = BaselineTestJpegs.Concat(ProgressiveTestJpegs).ToArray();
[Theory(Skip = "Debug only, enable manually!")]
//[Theory]
[WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)]
public void Decoder_ParseStream_SaveSpectralResult<TPixel>(TestImageProvider<TPixel> provider)
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();
[Theory]
[WithFile(Calliphora_RgbJpeg, PixelTypes.Rgba32)]
[WithFile(RgbJpeg, PixelTypes.Rgba32)]
[WithFile(RgbUncompressedTiled, PixelTypes.Rgba32)]
[WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)]
[WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)]
@ -39,7 +37,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Theory]
[InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)]
[InlineData(SmallRgbDeflate, 24, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(Calliphora_GrayscaleUncompressed, 8, 804, 1198, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(Calliphora_GrayscaleUncompressed, 8, 200, 298, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(Flower4BitPalette, 4, 73, 43, 72, 72, PixelResolutionUnit.PixelsPerInch)]
public void Identify(string imagePath, int expectedPixelSize, int expectedWidth, int expectedHeight, double expectedHResolution, double expectedVResolution, PixelResolutionUnit expectedResolutionUnit)
{
@ -377,6 +375,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffDecoder_CanDecode_PackBitsCompressed<TPixel>(TestImageProvider<TPixel> 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]
[WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)]
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.Rgb, TiffCompression.ItuTRecT43, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT82, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.Jpeg)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldDeflate, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldJpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)]
public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(
@ -288,6 +288,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
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]
[WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
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();
},
#pragma warning disable SA1515 // Single-line comment should be preceded by blank line
// ReSharper disable once ExplicitCallerInfoArgument
// ReSharper disable once ExplicitCallerInfoArgument
$"Decode {fileName}");
#pragma warning restore SA1515 // Single-line comment should be preceded by blank line
}
@ -92,11 +92,11 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
// Benchmark, enable manually!
[Theory(Skip = ProfilingSetup.SkipProfilingTests)]
[InlineData(1, 75, JpegSubsample.Ratio420)]
[InlineData(30, 75, JpegSubsample.Ratio420)]
[InlineData(30, 75, JpegSubsample.Ratio444)]
[InlineData(30, 100, JpegSubsample.Ratio444)]
public void EncodeJpeg(int executionCount, int quality, JpegSubsample subsample)
[InlineData(1, 75, JpegColorType.YCbCrRatio420)]
[InlineData(30, 75, JpegColorType.YCbCrRatio420)]
[InlineData(30, 75, JpegColorType.YCbCrRatio444)]
[InlineData(30, 100, JpegColorType.YCbCrRatio444)]
public void EncodeJpeg(int executionCount, int quality, JpegColorType colorType)
{
// do not run this on CI even by accident
if (TestEnvironment.RunsOnCI)
@ -118,7 +118,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{
foreach (Image<Rgba32> img in testImages)
{
var options = new JpegEncoder { Quality = quality, Subsample = subsample };
var options = new JpegEncoder { Quality = quality, ColorType = colorType };
img.Save(ms, options);
ms.Seek(0, SeekOrigin.Begin);
}

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 Jpeg444 = "Jpg/baseline/jpeg444.jpg";
public const string Jpeg420Small = "Jpg/baseline/jpeg420small.jpg";
public const string JpegRgb = "Jpg/baseline/jpeg-rgb.jpg";
public const string Jpeg410 = "Jpg/baseline/jpeg410.jpg";
public const string Jpeg411 = "Jpg/baseline/jpeg411.jpg";
public const string Jpeg422 = "Jpg/baseline/jpeg422.jpg";
public const string Testorig420 = "Jpg/baseline/testorig.jpg";
public const string MultiScanBaselineCMYK = "Jpg/baseline/MultiScanBaselineCMYK.jpg";
public const string Ratio1x1 = "Jpg/baseline/ratio-1x1.jpg";
@ -558,7 +562,9 @@ namespace SixLabors.ImageSharp.Tests
public const string RgbDeflate = "Tiff/rgb_deflate.tiff";
public const string RgbDeflatePredictor = "Tiff/rgb_deflate_predictor.tiff";
public const string RgbDeflateMultistrip = "Tiff/rgb_deflate_multistrip.tiff";
public const string RgbJpeg = "Tiff/rgb_jpeg.tiff";
public const string RgbJpegCompressed = "Tiff/rgb_jpegcompression.tiff";
public const string RgbWithStripsJpegCompressed = "Tiff/rgb_jpegcompressed_stripped.tiff";
public const string RgbJpegCompressedNoJpegTable = "Tiff/rgb_jpegcompressed_nojpegtable.tiff";
public const string RgbLzwPredictor = "Tiff/rgb_lzw_predictor.tiff";
public const string RgbLzwNoPredictor = "Tiff/rgb_lzw_no_predictor.tiff";
public const string RgbLzwNoPredictorMultistrip = "Tiff/rgb_lzw_noPredictor_multistrip.tiff";
@ -606,6 +612,7 @@ namespace SixLabors.ImageSharp.Tests
public const string RgbYCbCr888Contiguoush2v1 = "Tiff/rgb-ycbcr-contig-08_h2v1.tiff";
public const string RgbYCbCr888Contiguoush2v2 = "Tiff/rgb-ycbcr-contig-08_h2v2.tiff";
public const string RgbYCbCr888Contiguoush4v4 = "Tiff/rgb-ycbcr-contig-08_h4v4.tiff";
public const string YCbCrJpegCompressed = "Tiff/ycbcr_jpegcompressed.tiff";
public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff";
public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff";
public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff";

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
oid sha256:ed3b08730e5c34eb8d268f58d1e09efe2605398899bfd726cc3b35de21baa6ff
size 121196
oid sha256:c35902ca485fba441230efa88f794ee5aafa9f75ad5e4a14cb3d592a0a98c538
size 7760

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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
oid sha256:ecb529e5e3e0eca6f5e407b034fa8ba67bb4b9068af9e9b30425b08d30a249c0
size 1756355
oid sha256:993207c34358165af5fac0d0b955b56cd79e9707c1c46344863e01cbc9c7707d
size 126695

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

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

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

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

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