Browse Source

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

pull/2267/head
James Jackson-South 3 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
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// <see cref="Configuration"/> instance associated with current
/// decoding routine.
/// </summary>
private readonly Configuration configuration;
private JpegFrame frame;
private IRawJpegData jpegData;
@ -81,11 +75,15 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
/// <param name="targetSize">Optional target size for decoded image.</param>
public SpectralConverter(Configuration configuration, Size? targetSize = null)
{
this.configuration = configuration;
this.Configuration = configuration;
this.targetSize = targetSize;
}
/// <summary>
/// Gets the configuration instance associated with current decoding routine.
/// </summary>
public Configuration Configuration { get; }
/// <summary>
/// Gets converted pixel buffer.
/// </summary>
@ -134,7 +132,7 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
{
int y = yy - this.pixelRowCounter;
var values = new JpegColorConverterBase.ComponentValues(this.componentProcessors, y);
JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y);
this.colorConverter.ConvertToRgbInplace(values);
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.");
MemoryAllocator allocator = this.configuration.MemoryAllocator;
MemoryAllocator allocator = this.Configuration.MemoryAllocator;
// color converter from RGB to TPixel
JpegColorConverterBase converter = this.GetColorConverter(this.frame, this.jpegData);
@ -196,14 +194,14 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
this.pixelBuffer = allocator.Allocate2D<TPixel>(
pixelSize.Width,
pixelSize.Height,
this.configuration.PreferContiguousImageBuffers);
this.Configuration.PreferContiguousImageBuffers);
this.paddedProxyPixelRow = allocator.Allocate<TPixel>(pixelSize.Width + 3);
// component processors from spectral to RGB
int bufferWidth = majorBlockWidth * blockPixelSize;
int batchSize = converter.ElementsPerBatch;
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);
// 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)
{
MemoryAllocator allocator = this.configuration.MemoryAllocator;
var componentProcessors = new ComponentProcessor[frame.Components.Length];
MemoryAllocator allocator = this.Configuration.MemoryAllocator;
ComponentProcessor[] componentProcessors = new ComponentProcessor[frame.Components.Length];
for (int i = 0; i < componentProcessors.Length; i++)
{
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.
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
// to uint to avoid sign extension
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.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
@ -50,70 +49,52 @@ internal sealed class JpegTiffCompression : TiffBaseDecompressor
{
if (this.jpegTables != null)
{
using var jpegDecoder = new JpegDecoderCore(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);
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;
}
this.DecodeJpegData(stream, buffer, cancellationToken);
}
else
{
using var image = Image.Load<Rgb24>(stream);
CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer);
using Image<Rgb24> image = Image.Load<Rgb24>(this.options.GeneralOptions, stream);
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;
for (int y = 0; y < pixelBuffer.Height; y++)
using JpegDecoderCore jpegDecoder = new(this.options);
Configuration configuration = this.options.GeneralOptions.Configuration;
switch (this.photometricInterpretation)
{
Span<Rgb24> pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y);
Span<byte> rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan);
rgbBytes.CopyTo(buffer[offset..]);
offset += rgbBytes.Length;
}
}
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
{
using SpectralConverter<L8> spectralConverterGray = new GrayJpegSpectralConverter<L8>(configuration);
HuffmanScanDecoder scanDecoderGray = new(stream, spectralConverterGray, cancellationToken);
private static void CopyImageBytesToBuffer(Span<byte> buffer, Buffer2D<L8> pixelBuffer)
{
int offset = 0;
for (int y = 0; y < pixelBuffer.Height; y++)
{
Span<L8> pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y);
Span<byte> rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan);
rgbBytes.CopyTo(buffer[offset..]);
offset += rgbBytes.Length;
jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray);
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 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.
/// </summary>
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,
FaxCompressionOptions faxOptions,
byte[] jpegTables,
uint oldJpegStartOfImageMarker,
TiffFillOrder fillOrder,
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");
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:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new WebpTiffCompression(options, allocator, width, bitsPerPixel);

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

@ -30,34 +30,34 @@
### Compression Formats
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------|
|None | Y | Y | |
|Ccitt1D | Y | Y | |
|PackBits | Y | Y | |
|CcittGroup3Fax | Y | Y | |
|CcittGroup4Fax | Y | Y | |
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|-----------------------------------|
|None | Y | Y | |
|Ccitt1D | Y | Y | |
|PackBits | Y | Y | |
|CcittGroup3Fax | 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. |
|Old Jpeg | | | We should not even try to support this. |
|Jpeg (Technote 2) | Y | Y | |
|Deflate (Technote 2) | Y | Y | Based on PNG Deflate. |
|Old Deflate (Technote 2) | | Y | |
|Webp | | Y | |
|Old Jpeg | | Y | Only with chunky configuration. |
|Jpeg (Technote 2) | Y | Y | |
|Deflate (Technote 2) | Y | Y | Based on PNG Deflate. |
|Old Deflate (Technote 2) | | Y | |
|Webp | | Y | |
### Photometric Interpretation Formats
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------|
|WhiteIsZero | 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 (Planar) | | Y | General implementation only |
|PaletteColor | Y | Y | General implementation only |
|TransparencyMask | | | |
|Separated (TIFF Extension) | | | |
|YCbCr (TIFF Extension) | | Y | |
|CieLab (TIFF Extension) | | Y | |
|IccLab (TechNote 1) | | | |
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|-------------------------------------------|
|WhiteIsZero | 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 (Planar) | | Y | General implementation only. |
|PaletteColor | Y | Y | General implementation only. |
|TransparencyMask | | | |
|Separated (TIFF Extension) | | | |
|YCbCr (TIFF Extension) | | Y | |
|CieLab (TIFF Extension) | | Y | |
|IccLab (TechNote 1) | | | |
### Baseline TIFF Tags

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

@ -126,6 +126,11 @@ internal class TiffDecoderCore : IImageDecoderInternals
/// </summary>
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>
/// Gets or sets the planar configuration type to use when decoding the image.
/// </summary>
@ -232,7 +237,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
var imageFrameMetaData = new ImageFrameMetadata();
ImageFrameMetadata imageFrameMetaData = new();
if (!this.skipMetadata)
{
imageFrameMetaData.ExifProfile = tags;
@ -245,12 +250,12 @@ internal class TiffDecoderCore : IImageDecoderInternals
int width = GetImageWidth(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;
var stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue();
var stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue();
Array stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue();
Array stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue();
using IMemoryOwner<ulong> stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span<ulong> stripOffsets);
using IMemoryOwner<ulong> stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span<ulong> stripByteCounts);
@ -358,7 +363,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
Buffer2D<TPixel> pixels = frame.PixelBuffer;
var stripBuffers = new IMemoryOwner<byte>[stripsPerPixel];
IMemoryOwner<byte>[] stripBuffers = new IMemoryOwner<byte>[stripsPerPixel];
try
{
@ -379,6 +384,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
this.Predictor,
this.FaxCompressionOptions,
this.JpegTables,
this.OldJpegCompressionStartOfImageMarker.GetValueOrDefault(),
this.FillOrder,
this.byteOrder);
@ -460,6 +466,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
this.Predictor,
this.FaxCompressionOptions,
this.JpegTables,
this.OldJpegCompressionStartOfImageMarker.GetValueOrDefault(),
this.FillOrder,
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.");
}
var extraSamplesType = (TiffExtraSampleType)extraSamples[0];
TiffExtraSampleType extraSamplesType = (TiffExtraSampleType)extraSamples[0];
options.ExtraSamplesType = extraSamplesType;
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.FillOrder = fillOrder;
options.JpegTables = exifProfile.GetValue(ExifTag.JPEGTables)?.Value;
options.OldJpegCompressionStartOfImageMarker = exifProfile.GetValue(ExifTag.JPEGInterchangeFormat)?.Value;
options.ParseColorType(exifProfile);
options.ParseCompression(frameMetadata.Compression, exifProfile);
@ -473,6 +474,30 @@ internal static class TiffDecoderOptionsParser
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:
{
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)
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]
[WithFile(WebpCompressed, PixelTypes.Rgba32)]
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 RgbJpegCompressed = "Tiff/rgb_jpegcompression.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 RgbJpegCompressedNoJpegTable = "Tiff/rgb_jpegcompressed_nojpegtable.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();
settings.FrameCount = (int)options.MaxFrames;
settings.SetDefines(bmpReadDefines);
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,
ITestImageProvider provider,
ImageComparer comparer,
IImageDecoder referenceDecoder = null)
IImageDecoder referenceDecoder = null,
DecoderOptions referenceDecoderOptions = null)
where TPixel : unmanaged, IPixel<TPixel>
{
string path = TestImageProvider<TPixel>.GetFilePathOrNull(provider);
@ -527,12 +528,12 @@ public static class TestImageExtensions
throw new InvalidOperationException("CompareToOriginal() works only with file providers!");
}
var testFile = TestFile.Create(path);
TestFile testFile = TestFile.Create(path);
referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path);
using var stream = new MemoryStream(testFile.Bytes);
using (Image<TPixel> original = referenceDecoder.Decode<TPixel>(DecoderOptions.Default, stream, default))
using MemoryStream stream = new(testFile.Bytes);
using (Image<TPixel> original = referenceDecoder.Decode<TPixel>(referenceDecoderOptions ?? DecoderOptions.Default, stream, default))
{
comparer.VerifySimilarity(original, image);
}
@ -553,11 +554,11 @@ public static class TestImageExtensions
throw new InvalidOperationException("CompareToOriginal() works only with file providers!");
}
var testFile = TestFile.Create(path);
TestFile testFile = TestFile.Create(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))
{
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