Browse Source

Merge branch 'main' into js/buffered-read-fix

pull/2267/head
James Jackson-South 4 years ago
committed by GitHub
parent
commit
68f9510f9b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  2. 2
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  3. 33
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegCompressionUtils.cs
  4. 93
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
  5. 87
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs
  6. 45
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffOldJpegSpectralConverter{TPixel}.cs
  7. 5
      src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
  8. 5
      src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
  9. 48
      src/ImageSharp/Formats/Tiff/README.md
  10. 17
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  11. 27
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  12. 22
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  13. 5
      tests/ImageSharp.Tests/TestImages.cs
  14. 1
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
  15. 13
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  16. 3
      tests/Images/Input/Tiff/OldJpegCompression.tiff
  17. 3
      tests/Images/Input/Tiff/OldJpegCompression2.tiff
  18. 3
      tests/Images/Input/Tiff/OldJpegCompression3.tiff
  19. 3
      tests/Images/Input/Tiff/OldJpegCompressionGray.tiff
  20. 3
      tests/Images/Input/Tiff/YCbCrOldJpegCompressed.tiff

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

@ -21,12 +21,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
/// <summary>
/// <see cref="Configuration"/> instance associated with current
/// decoding routine.
/// </summary>
private readonly Configuration configuration;
private JpegFrame frame; private JpegFrame frame;
private IRawJpegData jpegData; private IRawJpegData jpegData;
@ -81,11 +75,15 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
/// <param name="targetSize">Optional target size for decoded image.</param> /// <param name="targetSize">Optional target size for decoded image.</param>
public SpectralConverter(Configuration configuration, Size? targetSize = null) public SpectralConverter(Configuration configuration, Size? targetSize = null)
{ {
this.configuration = configuration; this.Configuration = configuration;
this.targetSize = targetSize; this.targetSize = targetSize;
} }
/// <summary>
/// Gets the configuration instance associated with current decoding routine.
/// </summary>
public Configuration Configuration { get; }
/// <summary> /// <summary>
/// Gets converted pixel buffer. /// Gets converted pixel buffer.
/// </summary> /// </summary>
@ -134,7 +132,7 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
{ {
int y = yy - this.pixelRowCounter; int y = yy - this.pixelRowCounter;
var values = new JpegColorConverterBase.ComponentValues(this.componentProcessors, y); JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y);
this.colorConverter.ConvertToRgbInplace(values); this.colorConverter.ConvertToRgbInplace(values);
values = values.Slice(0, width); // slice away Jpeg padding values = values.Slice(0, width); // slice away Jpeg padding
@ -177,7 +175,7 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
{ {
DebugGuard.IsTrue(this.colorConverter == null, "SpectralConverter.PrepareForDecoding() must be called once."); DebugGuard.IsTrue(this.colorConverter == null, "SpectralConverter.PrepareForDecoding() must be called once.");
MemoryAllocator allocator = this.configuration.MemoryAllocator; MemoryAllocator allocator = this.Configuration.MemoryAllocator;
// color converter from RGB to TPixel // color converter from RGB to TPixel
JpegColorConverterBase converter = this.GetColorConverter(this.frame, this.jpegData); JpegColorConverterBase converter = this.GetColorConverter(this.frame, this.jpegData);
@ -196,14 +194,14 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
this.pixelBuffer = allocator.Allocate2D<TPixel>( this.pixelBuffer = allocator.Allocate2D<TPixel>(
pixelSize.Width, pixelSize.Width,
pixelSize.Height, pixelSize.Height,
this.configuration.PreferContiguousImageBuffers); this.Configuration.PreferContiguousImageBuffers);
this.paddedProxyPixelRow = allocator.Allocate<TPixel>(pixelSize.Width + 3); this.paddedProxyPixelRow = allocator.Allocate<TPixel>(pixelSize.Width + 3);
// component processors from spectral to RGB // component processors from spectral to RGB
int bufferWidth = majorBlockWidth * blockPixelSize; int bufferWidth = majorBlockWidth * blockPixelSize;
int batchSize = converter.ElementsPerBatch; int batchSize = converter.ElementsPerBatch;
int batchRemainder = bufferWidth & (batchSize - 1); int batchRemainder = bufferWidth & (batchSize - 1);
var postProcessorBufferSize = new Size(bufferWidth + (batchSize - batchRemainder), this.pixelRowsPerStep); Size postProcessorBufferSize = new(bufferWidth + (batchSize - batchRemainder), this.pixelRowsPerStep);
this.componentProcessors = this.CreateComponentProcessors(this.frame, this.jpegData, blockPixelSize, postProcessorBufferSize); this.componentProcessors = this.CreateComponentProcessors(this.frame, this.jpegData, blockPixelSize, postProcessorBufferSize);
// single 'stride' rgba32 buffer for conversion between spectral and TPixel // single 'stride' rgba32 buffer for conversion between spectral and TPixel
@ -225,8 +223,8 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
protected ComponentProcessor[] CreateComponentProcessors(JpegFrame frame, IRawJpegData jpegData, int blockPixelSize, Size processorBufferSize) protected ComponentProcessor[] CreateComponentProcessors(JpegFrame frame, IRawJpegData jpegData, int blockPixelSize, Size processorBufferSize)
{ {
MemoryAllocator allocator = this.configuration.MemoryAllocator; MemoryAllocator allocator = this.Configuration.MemoryAllocator;
var componentProcessors = new ComponentProcessor[frame.Components.Length]; ComponentProcessor[] componentProcessors = new ComponentProcessor[frame.Components.Length];
for (int i = 0; i < componentProcessors.Length; i++) for (int i = 0; i < componentProcessors.Length; i++)
{ {
componentProcessors[i] = blockPixelSize switch componentProcessors[i] = blockPixelSize switch

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

@ -275,7 +275,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
// Get the marker length. // Get the marker length.
int markerContentByteSize = this.ReadUint16(stream) - 2; int markerContentByteSize = this.ReadUint16(stream) - 2;
// Check whether stream actually has enought bytes to read // Check whether the stream actually has enough bytes to read
// markerContentByteSize is always positive so we cast // markerContentByteSize is always positive so we cast
// to uint to avoid sign extension // to uint to avoid sign extension
if (stream.RemainingBytes < (uint)markerContentByteSize) if (stream.RemainingBytes < (uint)markerContentByteSize)

33
src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegCompressionUtils.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
internal static class JpegCompressionUtils
{
public static void CopyImageBytesToBuffer(Configuration configuration, Span<byte> buffer, Buffer2D<Rgb24> pixelBuffer)
{
int offset = 0;
for (int y = 0; y < pixelBuffer.Height; y++)
{
Span<Rgb24> pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<Rgb24>.Instance.ToRgb24Bytes(configuration, pixelRowSpan, buffer[offset..], pixelRowSpan.Length);
offset += Unsafe.SizeOf<Rgb24>() * pixelRowSpan.Length;
}
}
public static void CopyImageBytesToBuffer(Configuration configuration, Span<byte> buffer, Buffer2D<L8> pixelBuffer)
{
int offset = 0;
for (int y = 0; y < pixelBuffer.Height; y++)
{
Span<L8> pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<L8>.Instance.ToL8Bytes(configuration, pixelRowSpan, buffer[offset..], pixelRowSpan.Length);
offset += Unsafe.SizeOf<L8>() * pixelRowSpan.Length;
}
}
}

93
src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
@ -50,70 +49,52 @@ internal sealed class JpegTiffCompression : TiffBaseDecompressor
{ {
if (this.jpegTables != null) if (this.jpegTables != null)
{ {
using var jpegDecoder = new JpegDecoderCore(this.options); this.DecodeJpegData(stream, buffer, cancellationToken);
Configuration configuration = this.options.GeneralOptions.Configuration;
switch (this.photometricInterpretation)
{
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
{
using SpectralConverter<L8> spectralConverterGray = new GrayJpegSpectralConverter<L8>(configuration);
var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, cancellationToken);
jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray);
jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken);
using Buffer2D<L8> decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
break;
}
case TiffPhotometricInterpretation.YCbCr:
case TiffPhotometricInterpretation.Rgb:
{
using SpectralConverter<Rgb24> spectralConverter =
new TiffJpegSpectralConverter<Rgb24>(configuration, this.photometricInterpretation);
var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, cancellationToken);
jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken);
using Buffer2D<Rgb24> decompressedBuffer = spectralConverter.GetPixelBuffer(cancellationToken);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
break;
}
default:
TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported");
break;
}
} }
else else
{ {
using var image = Image.Load<Rgb24>(stream); using Image<Rgb24> image = Image.Load<Rgb24>(this.options.GeneralOptions, stream);
CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); JpegCompressionUtils.CopyImageBytesToBuffer(this.options.GeneralOptions.Configuration, buffer, image.Frames.RootFrame.PixelBuffer);
} }
} }
private static void CopyImageBytesToBuffer(Span<byte> buffer, Buffer2D<Rgb24> pixelBuffer) private void DecodeJpegData(BufferedReadStream stream, Span<byte> buffer, CancellationToken cancellationToken)
{ {
int offset = 0; using JpegDecoderCore jpegDecoder = new(this.options);
for (int y = 0; y < pixelBuffer.Height; y++) Configuration configuration = this.options.GeneralOptions.Configuration;
switch (this.photometricInterpretation)
{ {
Span<Rgb24> pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); case TiffPhotometricInterpretation.BlackIsZero:
Span<byte> rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); case TiffPhotometricInterpretation.WhiteIsZero:
rgbBytes.CopyTo(buffer[offset..]); {
offset += rgbBytes.Length; using SpectralConverter<L8> spectralConverterGray = new GrayJpegSpectralConverter<L8>(configuration);
} HuffmanScanDecoder scanDecoderGray = new(stream, spectralConverterGray, cancellationToken);
}
private static void CopyImageBytesToBuffer(Span<byte> buffer, Buffer2D<L8> pixelBuffer) jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray);
{ jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken);
int offset = 0;
for (int y = 0; y < pixelBuffer.Height; y++) using Buffer2D<L8> decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken);
{ JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverterGray.Configuration, buffer, decompressedBuffer);
Span<L8> pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); break;
Span<byte> rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); }
rgbBytes.CopyTo(buffer[offset..]);
offset += rgbBytes.Length; case TiffPhotometricInterpretation.YCbCr:
case TiffPhotometricInterpretation.Rgb:
{
using SpectralConverter<Rgb24> spectralConverter = new TiffJpegSpectralConverter<Rgb24>(configuration, this.photometricInterpretation);
HuffmanScanDecoder scanDecoder = new(stream, spectralConverter, cancellationToken);
jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken);
using Buffer2D<Rgb24> decompressedBuffer = spectralConverter.GetPixelBuffer(cancellationToken);
JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverter.Configuration, buffer, decompressedBuffer);
break;
}
default:
TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported");
break;
} }
} }

87
src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs

@ -0,0 +1,87 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
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.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
internal sealed class OldJpegTiffCompression : TiffBaseDecompressor
{
private readonly JpegDecoderOptions options;
private readonly uint startOfImageMarker;
private readonly TiffPhotometricInterpretation photometricInterpretation;
public OldJpegTiffCompression(
JpegDecoderOptions options,
MemoryAllocator memoryAllocator,
int width,
int bitsPerPixel,
uint startOfImageMarker,
TiffPhotometricInterpretation photometricInterpretation)
: base(memoryAllocator, width, bitsPerPixel)
{
this.options = options;
this.startOfImageMarker = startOfImageMarker;
this.photometricInterpretation = photometricInterpretation;
}
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer, CancellationToken cancellationToken)
{
long stripOffset = stream.Position;
stream.Position = this.startOfImageMarker;
this.DecodeJpegData(stream, buffer, cancellationToken);
// Setting the stream position to the expected position.
// This is a workaround for some images having set the stripBytesCount not equal to the compressed jpeg data.
stream.Position = stripOffset + byteCount;
}
private void DecodeJpegData(BufferedReadStream stream, Span<byte> buffer, CancellationToken cancellationToken)
{
using JpegDecoderCore jpegDecoder = new(this.options);
Configuration configuration = this.options.GeneralOptions.Configuration;
switch (this.photometricInterpretation)
{
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
{
using SpectralConverter<L8> spectralConverterGray = new GrayJpegSpectralConverter<L8>(configuration);
jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken);
using Buffer2D<L8> decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken);
JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverterGray.Configuration, buffer, decompressedBuffer);
break;
}
case TiffPhotometricInterpretation.YCbCr:
case TiffPhotometricInterpretation.Rgb:
{
using SpectralConverter<Rgb24> spectralConverter = new TiffOldJpegSpectralConverter<Rgb24>(configuration, this.photometricInterpretation);
jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken);
using Buffer2D<Rgb24> decompressedBuffer = spectralConverter.GetPixelBuffer(cancellationToken);
JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverter.Configuration, buffer, decompressedBuffer);
break;
}
default:
TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported");
break;
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}

45
src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffOldJpegSpectralConverter{TPixel}.cs

@ -0,0 +1,45 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
/// <summary>
/// Spectral converter for YCbCr TIFF's which use the OldJPEG compression.
/// The jpeg data should be always treated as YCbCr color space.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
internal sealed class TiffOldJpegSpectralConverter<TPixel> : SpectralConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TiffPhotometricInterpretation photometricInterpretation;
/// <summary>
/// Initializes a new instance of the <see cref="TiffOldJpegSpectralConverter{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="photometricInterpretation">Tiff photometric interpretation.</param>
public TiffOldJpegSpectralConverter(Configuration configuration, TiffPhotometricInterpretation photometricInterpretation)
: base(configuration)
=> this.photometricInterpretation = photometricInterpretation;
/// <inheritdoc/>
protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData)
{
JpegColorSpace colorSpace = GetJpegColorSpaceFromPhotometricInterpretation(this.photometricInterpretation);
return JpegColorConverterBase.GetConverter(colorSpace, frame.Precision);
}
private static JpegColorSpace GetJpegColorSpaceFromPhotometricInterpretation(TiffPhotometricInterpretation interpretation)
=> interpretation switch
{
// Like libtiff: Always treat the pixel data as YCbCr when the data is compressed with old jpeg compression.
TiffPhotometricInterpretation.Rgb => JpegColorSpace.YCbCr,
TiffPhotometricInterpretation.YCbCr => JpegColorSpace.YCbCr,
_ => throw new InvalidImageContentException($"Invalid tiff photometric interpretation for jpeg encoding: {interpretation}"),
};
}

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

@ -52,4 +52,9 @@ internal enum TiffDecoderCompressionType
/// The image data is compressed as a WEBP stream. /// The image data is compressed as a WEBP stream.
/// </summary> /// </summary>
Webp = 8, Webp = 8,
/// <summary>
/// The image data is compressed as a OldJPEG compressed stream.
/// </summary>
OldJpeg = 9,
} }

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

@ -21,6 +21,7 @@ internal static class TiffDecompressorsFactory
TiffPredictor predictor, TiffPredictor predictor,
FaxCompressionOptions faxOptions, FaxCompressionOptions faxOptions,
byte[] jpegTables, byte[] jpegTables,
uint oldJpegStartOfImageMarker,
TiffFillOrder fillOrder, TiffFillOrder fillOrder,
ByteOrder byteOrder) ByteOrder byteOrder)
{ {
@ -60,6 +61,10 @@ internal static class TiffDecompressorsFactory
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new JpegTiffCompression(new() { GeneralOptions = options }, allocator, width, bitsPerPixel, jpegTables, photometricInterpretation); return new JpegTiffCompression(new() { GeneralOptions = options }, allocator, width, bitsPerPixel, jpegTables, photometricInterpretation);
case TiffDecoderCompressionType.OldJpeg:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new OldJpegTiffCompression(new() { GeneralOptions = options }, allocator, width, bitsPerPixel, oldJpegStartOfImageMarker, photometricInterpretation);
case TiffDecoderCompressionType.Webp: case TiffDecoderCompressionType.Webp:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new WebpTiffCompression(options, allocator, width, bitsPerPixel); return new WebpTiffCompression(options, allocator, width, bitsPerPixel);

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

@ -30,34 +30,34 @@
### Compression Formats ### Compression Formats
| |Encoder|Decoder|Comments | | |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------| |---------------------------|:-----:|:-----:|-----------------------------------|
|None | Y | Y | | |None | Y | Y | |
|Ccitt1D | Y | Y | | |Ccitt1D | Y | Y | |
|PackBits | Y | Y | | |PackBits | Y | Y | |
|CcittGroup3Fax | Y | Y | | |CcittGroup3Fax | Y | Y | |
|CcittGroup4Fax | Y | Y | | |CcittGroup4Fax | Y | Y | |
|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case. | |Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case. |
|Old Jpeg | | | We should not even try to support this. | |Old Jpeg | | Y | Only with chunky configuration. |
|Jpeg (Technote 2) | Y | Y | | |Jpeg (Technote 2) | Y | Y | |
|Deflate (Technote 2) | Y | Y | Based on PNG Deflate. | |Deflate (Technote 2) | Y | Y | Based on PNG Deflate. |
|Old Deflate (Technote 2) | | Y | | |Old Deflate (Technote 2) | | Y | |
|Webp | | Y | | |Webp | | Y | |
### Photometric Interpretation Formats ### Photometric Interpretation Formats
| |Encoder|Decoder|Comments | | |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------| |---------------------------|:-----:|:-----:|-------------------------------------------|
|WhiteIsZero | Y | Y | General + 1/4/8-bit optimised implementations | |WhiteIsZero | Y | Y | General + 1/4/8-bit optimised implementations. |
|BlackIsZero | Y | Y | General + 1/4/8-bit optimised implementations | |BlackIsZero | Y | Y | General + 1/4/8-bit optimised implementations. |
|Rgb (Chunky) | Y | Y | General + Rgb888 optimised implementation | |Rgb (Chunky) | Y | Y | General + Rgb888 optimised implementation.|
|Rgb (Planar) | | Y | General implementation only | |Rgb (Planar) | | Y | General implementation only. |
|PaletteColor | Y | Y | General implementation only | |PaletteColor | Y | Y | General implementation only. |
|TransparencyMask | | | | |TransparencyMask | | | |
|Separated (TIFF Extension) | | | | |Separated (TIFF Extension) | | | |
|YCbCr (TIFF Extension) | | Y | | |YCbCr (TIFF Extension) | | Y | |
|CieLab (TIFF Extension) | | Y | | |CieLab (TIFF Extension) | | Y | |
|IccLab (TechNote 1) | | | | |IccLab (TechNote 1) | | | |
### Baseline TIFF Tags ### Baseline TIFF Tags

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

@ -126,6 +126,11 @@ internal class TiffDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
public byte[] JpegTables { get; set; } public byte[] JpegTables { get; set; }
/// <summary>
/// Gets or sets the start of image marker for old Jpeg compression.
/// </summary>
public uint? OldJpegCompressionStartOfImageMarker { get; set; }
/// <summary> /// <summary>
/// Gets or sets the planar configuration type to use when decoding the image. /// Gets or sets the planar configuration type to use when decoding the image.
/// </summary> /// </summary>
@ -232,7 +237,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken) private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var imageFrameMetaData = new ImageFrameMetadata(); ImageFrameMetadata imageFrameMetaData = new();
if (!this.skipMetadata) if (!this.skipMetadata)
{ {
imageFrameMetaData.ExifProfile = tags; imageFrameMetaData.ExifProfile = tags;
@ -245,12 +250,12 @@ internal class TiffDecoderCore : IImageDecoderInternals
int width = GetImageWidth(tags); int width = GetImageWidth(tags);
int height = GetImageHeight(tags); int height = GetImageHeight(tags);
var frame = new ImageFrame<TPixel>(this.configuration, width, height, imageFrameMetaData); ImageFrame<TPixel> frame = new(this.configuration, width, height, imageFrameMetaData);
int rowsPerStrip = tags.GetValue(ExifTag.RowsPerStrip) != null ? (int)tags.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; int rowsPerStrip = tags.GetValue(ExifTag.RowsPerStrip) != null ? (int)tags.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity;
var stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue(); Array stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue();
var stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue(); Array stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue();
using IMemoryOwner<ulong> stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span<ulong> stripOffsets); using IMemoryOwner<ulong> stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span<ulong> stripOffsets);
using IMemoryOwner<ulong> stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span<ulong> stripByteCounts); using IMemoryOwner<ulong> stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span<ulong> stripByteCounts);
@ -358,7 +363,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
Buffer2D<TPixel> pixels = frame.PixelBuffer; Buffer2D<TPixel> pixels = frame.PixelBuffer;
var stripBuffers = new IMemoryOwner<byte>[stripsPerPixel]; IMemoryOwner<byte>[] stripBuffers = new IMemoryOwner<byte>[stripsPerPixel];
try try
{ {
@ -379,6 +384,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
this.Predictor, this.Predictor,
this.FaxCompressionOptions, this.FaxCompressionOptions,
this.JpegTables, this.JpegTables,
this.OldJpegCompressionStartOfImageMarker.GetValueOrDefault(),
this.FillOrder, this.FillOrder,
this.byteOrder); this.byteOrder);
@ -460,6 +466,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
this.Predictor, this.Predictor,
this.FaxCompressionOptions, this.FaxCompressionOptions,
this.JpegTables, this.JpegTables,
this.OldJpegCompressionStartOfImageMarker.GetValueOrDefault(),
this.FillOrder, this.FillOrder,
this.byteOrder); this.byteOrder);

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

@ -37,7 +37,7 @@ internal static class TiffDecoderOptionsParser
TiffThrowHelper.ThrowNotSupported("ExtraSamples is only supported with one extra sample for alpha data."); TiffThrowHelper.ThrowNotSupported("ExtraSamples is only supported with one extra sample for alpha data.");
} }
var extraSamplesType = (TiffExtraSampleType)extraSamples[0]; TiffExtraSampleType extraSamplesType = (TiffExtraSampleType)extraSamples[0];
options.ExtraSamplesType = extraSamplesType; options.ExtraSamplesType = extraSamplesType;
if (extraSamplesType is not (TiffExtraSampleType.UnassociatedAlphaData or TiffExtraSampleType.AssociatedAlphaData)) if (extraSamplesType is not (TiffExtraSampleType.UnassociatedAlphaData or TiffExtraSampleType.AssociatedAlphaData))
{ {
@ -99,6 +99,7 @@ internal static class TiffDecoderOptionsParser
options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value;
options.FillOrder = fillOrder; options.FillOrder = fillOrder;
options.JpegTables = exifProfile.GetValue(ExifTag.JPEGTables)?.Value; options.JpegTables = exifProfile.GetValue(ExifTag.JPEGTables)?.Value;
options.OldJpegCompressionStartOfImageMarker = exifProfile.GetValue(ExifTag.JPEGInterchangeFormat)?.Value;
options.ParseColorType(exifProfile); options.ParseColorType(exifProfile);
options.ParseCompression(frameMetadata.Compression, exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile);
@ -473,6 +474,30 @@ internal static class TiffDecoderOptionsParser
break; break;
} }
case TiffCompression.OldJpeg:
{
if (!options.OldJpegCompressionStartOfImageMarker.HasValue)
{
TiffThrowHelper.ThrowNotSupported("Missing SOI marker offset for tiff with old jpeg compression");
}
if (options.PlanarConfiguration is TiffPlanarConfiguration.Planar)
{
TiffThrowHelper.ThrowNotSupported("Old Jpeg compression is not supported with planar configuration");
}
options.CompressionType = TiffDecoderCompressionType.OldJpeg;
if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr)
{
// Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data.
options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb;
options.ColorType = TiffColorType.Rgb;
}
break;
}
case TiffCompression.Jpeg: case TiffCompression.Jpeg:
{ {
options.CompressionType = TiffDecoderCompressionType.Jpeg; options.CompressionType = TiffDecoderCompressionType.Jpeg;

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

@ -664,6 +664,28 @@ public class TiffDecoderTests : TiffDecoderBaseTester
public void TiffDecoder_CanDecode_JpegCompressed<TPixel>(TestImageProvider<TPixel> provider) public void TiffDecoder_CanDecode_JpegCompressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false); where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false);
[Theory]
[WithFile(RgbOldJpegCompressed, PixelTypes.Rgba32)]
[WithFile(RgbOldJpegCompressed2, PixelTypes.Rgba32)]
[WithFile(RgbOldJpegCompressed3, PixelTypes.Rgba32)]
[WithFile(RgbOldJpegCompressedGray, PixelTypes.Rgba32)]
[WithFile(YCbCrOldJpegCompressed, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_OldJpegCompressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions decoderOptions = new()
{
MaxFrames = 1
};
using Image<TPixel> image = provider.GetImage(TiffDecoder, decoderOptions);
image.DebugSave(provider);
image.CompareToOriginal(
provider,
ImageComparer.Tolerant(0.001f),
ReferenceDecoder,
decoderOptions);
}
[Theory] [Theory]
[WithFile(WebpCompressed, PixelTypes.Rgba32)] [WithFile(WebpCompressed, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_WebpCompressed<TPixel>(TestImageProvider<TPixel> provider) public void TiffDecoder_CanDecode_WebpCompressed<TPixel>(TestImageProvider<TPixel> provider)

5
tests/ImageSharp.Tests/TestImages.cs

@ -800,6 +800,11 @@ public static class TestImages
public const string RgbDeflateMultistrip = "Tiff/rgb_deflate_multistrip.tiff"; public const string RgbDeflateMultistrip = "Tiff/rgb_deflate_multistrip.tiff";
public const string RgbJpegCompressed = "Tiff/rgb_jpegcompression.tiff"; public const string RgbJpegCompressed = "Tiff/rgb_jpegcompression.tiff";
public const string RgbJpegCompressed2 = "Tiff/twain-rgb-jpeg-with-bogus-ycbcr-subsampling.tiff"; public const string RgbJpegCompressed2 = "Tiff/twain-rgb-jpeg-with-bogus-ycbcr-subsampling.tiff";
public const string RgbOldJpegCompressed = "Tiff/OldJpegCompression.tiff";
public const string RgbOldJpegCompressed2 = "Tiff/OldJpegCompression2.tiff";
public const string RgbOldJpegCompressed3 = "Tiff/OldJpegCompression3.tiff";
public const string RgbOldJpegCompressedGray = "Tiff/OldJpegCompressionGray.tiff";
public const string YCbCrOldJpegCompressed = "Tiff/YCbCrOldJpegCompressed.tiff";
public const string RgbWithStripsJpegCompressed = "Tiff/rgb_jpegcompressed_stripped.tiff"; public const string RgbWithStripsJpegCompressed = "Tiff/rgb_jpegcompressed_stripped.tiff";
public const string RgbJpegCompressedNoJpegTable = "Tiff/rgb_jpegcompressed_nojpegtable.tiff"; public const string RgbJpegCompressedNoJpegTable = "Tiff/rgb_jpegcompressed_nojpegtable.tiff";
public const string RgbLzwPredictor = "Tiff/rgb_lzw_predictor.tiff"; public const string RgbLzwPredictor = "Tiff/rgb_lzw_predictor.tiff";

1
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -34,6 +34,7 @@ public class MagickReferenceDecoder : IImageDecoder
}; };
var settings = new MagickReadSettings(); var settings = new MagickReadSettings();
settings.FrameCount = (int)options.MaxFrames;
settings.SetDefines(bmpReadDefines); settings.SetDefines(bmpReadDefines);
using var magickImageCollection = new MagickImageCollection(stream, settings); using var magickImageCollection = new MagickImageCollection(stream, settings);

13
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -518,7 +518,8 @@ public static class TestImageExtensions
this Image<TPixel> image, this Image<TPixel> image,
ITestImageProvider provider, ITestImageProvider provider,
ImageComparer comparer, ImageComparer comparer,
IImageDecoder referenceDecoder = null) IImageDecoder referenceDecoder = null,
DecoderOptions referenceDecoderOptions = null)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
string path = TestImageProvider<TPixel>.GetFilePathOrNull(provider); string path = TestImageProvider<TPixel>.GetFilePathOrNull(provider);
@ -527,12 +528,12 @@ public static class TestImageExtensions
throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); throw new InvalidOperationException("CompareToOriginal() works only with file providers!");
} }
var testFile = TestFile.Create(path); TestFile testFile = TestFile.Create(path);
referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path);
using var stream = new MemoryStream(testFile.Bytes); using MemoryStream stream = new(testFile.Bytes);
using (Image<TPixel> original = referenceDecoder.Decode<TPixel>(DecoderOptions.Default, stream, default)) using (Image<TPixel> original = referenceDecoder.Decode<TPixel>(referenceDecoderOptions ?? DecoderOptions.Default, stream, default))
{ {
comparer.VerifySimilarity(original, image); comparer.VerifySimilarity(original, image);
} }
@ -553,11 +554,11 @@ public static class TestImageExtensions
throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); throw new InvalidOperationException("CompareToOriginal() works only with file providers!");
} }
var testFile = TestFile.Create(path); TestFile testFile = TestFile.Create(path);
referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path);
using var stream = new MemoryStream(testFile.Bytes); using MemoryStream stream = new(testFile.Bytes);
using (Image<TPixel> original = referenceDecoder.Decode<TPixel>(DecoderOptions.Default, stream, default)) using (Image<TPixel> original = referenceDecoder.Decode<TPixel>(DecoderOptions.Default, stream, default))
{ {
comparer.VerifySimilarity(original, image); comparer.VerifySimilarity(original, image);

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

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

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

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

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

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

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

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

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

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