Browse Source

Add support for decoding tiff images with old jpeg compression (#2238)

pull/2266/head
Brian Popow 4 years ago
parent
commit
db51bd7495
  1. 2
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  2. 100
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
  3. 30
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs
  4. 4
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
  5. 5
      src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
  6. 9
      src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
  7. 2
      src/ImageSharp/Formats/Tiff/README.md
  8. 17
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  9. 20
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

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)

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

@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
/// <summary> /// <summary>
/// Class to handle cases where TIFF image data is compressed as a jpeg stream. /// Class to handle cases where TIFF image data is compressed as a jpeg stream.
/// </summary> /// </summary>
internal sealed class JpegTiffCompression : TiffBaseDecompressor internal class JpegTiffCompression : TiffBaseDecompressor
{ {
private readonly JpegDecoderOptions options; private readonly JpegDecoderOptions options;
@ -25,17 +25,17 @@ internal sealed class JpegTiffCompression : TiffBaseDecompressor
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JpegTiffCompression"/> class. /// Initializes a new instance of the <see cref="JpegTiffCompression"/> class.
/// </summary> /// </summary>
/// <param name="options">The specialized jpeg decoder options.</param>
/// <param name="memoryAllocator">The memoryAllocator to use for buffer allocations.</param> /// <param name="memoryAllocator">The memoryAllocator to use for buffer allocations.</param>
/// <param name="width">The image width.</param> /// <param name="width">The image width.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param> /// <param name="bitsPerPixel">The bits per pixel.</param>
/// <param name="options">The specialized jpeg decoder options.</param>
/// <param name="jpegTables">The JPEG tables containing the quantization and/or Huffman tables.</param> /// <param name="jpegTables">The JPEG tables containing the quantization and/or Huffman tables.</param>
/// <param name="photometricInterpretation">The photometric interpretation.</param> /// <param name="photometricInterpretation">The photometric interpretation.</param>
public JpegTiffCompression( public JpegTiffCompression(
JpegDecoderOptions options,
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
int width, int width,
int bitsPerPixel, int bitsPerPixel,
JpegDecoderOptions options,
byte[] jpegTables, byte[] jpegTables,
TiffPhotometricInterpretation photometricInterpretation) TiffPhotometricInterpretation photometricInterpretation)
: base(memoryAllocator, width, bitsPerPixel) : base(memoryAllocator, width, bitsPerPixel)
@ -45,51 +45,85 @@ internal sealed class JpegTiffCompression : TiffBaseDecompressor
this.photometricInterpretation = photometricInterpretation; this.photometricInterpretation = photometricInterpretation;
} }
/// <summary>
/// Initializes a new instance of the <see cref="JpegTiffCompression"/> class.
/// </summary>
/// <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="options">The specialized jpeg decoder options.</param>
/// <param name="photometricInterpretation">The photometric interpretation.</param>
public JpegTiffCompression(
MemoryAllocator memoryAllocator,
int width,
int bitsPerPixel,
JpegDecoderOptions options,
TiffPhotometricInterpretation photometricInterpretation)
: base(memoryAllocator, width, bitsPerPixel)
{
this.options = options;
this.photometricInterpretation = photometricInterpretation;
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer, CancellationToken cancellationToken) protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer, CancellationToken cancellationToken)
{ {
if (this.jpegTables != null) if (this.jpegTables != null)
{ {
using var jpegDecoder = new JpegDecoderCore(this.options); this.DecodeJpegData(stream, buffer, true, cancellationToken);
Configuration configuration = this.options.GeneralOptions.Configuration; }
switch (this.photometricInterpretation) else
{
using Image<Rgb24> image = Image.Load<Rgb24>(stream);
CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer);
}
}
protected void DecodeJpegData(BufferedReadStream stream, Span<byte> buffer, bool loadTables, CancellationToken cancellationToken)
{
using JpegDecoderCore jpegDecoder = new(this.options);
Configuration configuration = this.options.GeneralOptions.Configuration;
switch (this.photometricInterpretation)
{
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
{ {
case TiffPhotometricInterpretation.BlackIsZero: using SpectralConverter<L8> spectralConverterGray = new GrayJpegSpectralConverter<L8>(configuration);
case TiffPhotometricInterpretation.WhiteIsZero: HuffmanScanDecoder scanDecoderGray = new(stream, spectralConverterGray, cancellationToken);
if (loadTables)
{ {
using SpectralConverter<L8> spectralConverterGray = new GrayJpegSpectralConverter<L8>(configuration);
var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, cancellationToken);
jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray);
jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken);
using Buffer2D<L8> decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
break;
} }
case TiffPhotometricInterpretation.YCbCr: jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken);
case TiffPhotometricInterpretation.Rgb:
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);
HuffmanScanDecoder scanDecoder = new(stream, spectralConverter, cancellationToken);
if (loadTables)
{ {
using SpectralConverter<Rgb24> spectralConverter =
new TiffJpegSpectralConverter<Rgb24>(configuration, this.photometricInterpretation);
var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, cancellationToken);
jpegDecoder.LoadTables(this.jpegTables, scanDecoder); jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken);
using Buffer2D<Rgb24> decompressedBuffer = spectralConverter.GetPixelBuffer(cancellationToken);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
break;
} }
default: jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken);
TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported");
break; using Buffer2D<Rgb24> decompressedBuffer = spectralConverter.GetPixelBuffer(cancellationToken);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
break;
} }
}
else default:
{ TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported");
using var image = Image.Load<Rgb24>(stream); break;
CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer);
} }
} }

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

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
internal sealed class OldJpegTiffCompression : JpegTiffCompression
{
private readonly uint startOfImageMarker;
public OldJpegTiffCompression(
MemoryAllocator memoryAllocator,
int width,
int bitsPerPixel,
JpegDecoderOptions options,
uint startOfImageMarker,
TiffPhotometricInterpretation photometricInterpretation)
: base(memoryAllocator, width, bitsPerPixel, options, photometricInterpretation) => this.startOfImageMarker = startOfImageMarker;
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer, CancellationToken cancellationToken)
{
stream.Position = this.startOfImageMarker;
this.DecodeJpegData(stream, buffer, false, cancellationToken);
}
}

4
src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs

@ -20,12 +20,12 @@ internal class WebpTiffCompression : TiffBaseDecompressor
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebpTiffCompression"/> class. /// Initializes a new instance of the <see cref="WebpTiffCompression"/> class.
/// </summary> /// </summary>
/// <param name="options">The general decoder options.</param>
/// <param name="memoryAllocator">The memory allocator.</param> /// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param> /// <param name="bitsPerPixel">The bits per pixel.</param>
/// <param name="options">The general decoder options.</param>
/// <param name="predictor">The predictor.</param> /// <param name="predictor">The predictor.</param>
public WebpTiffCompression(DecoderOptions options, MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) public WebpTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, DecoderOptions options, TiffPredictor predictor = TiffPredictor.None)
: base(memoryAllocator, width, bitsPerPixel, predictor) : base(memoryAllocator, width, bitsPerPixel, predictor)
=> this.options = options; => this.options = options;

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,
} }

9
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)
{ {
@ -58,11 +59,15 @@ internal static class TiffDecompressorsFactory
case TiffDecoderCompressionType.Jpeg: case TiffDecoderCompressionType.Jpeg:
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(allocator, width, bitsPerPixel, new() { GeneralOptions = options }, jpegTables, photometricInterpretation);
case TiffDecoderCompressionType.OldJpeg:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new OldJpegTiffCompression(allocator, width, bitsPerPixel, new() { GeneralOptions = options }, 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(allocator, width, bitsPerPixel, options);
default: default:
throw TiffThrowHelper.NotSupportedDecompressor(nameof(method)); throw TiffThrowHelper.NotSupportedDecompressor(nameof(method));

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

@ -38,7 +38,7 @@
|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 | |
|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 | |

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);

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

@ -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,25 @@ internal static class TiffDecoderOptionsParser
break; break;
} }
case TiffCompression.OldJpeg:
{
options.CompressionType = TiffDecoderCompressionType.OldJpeg;
if (!options.OldJpegCompressionStartOfImageMarker.HasValue)
{
TiffThrowHelper.ThrowNotSupported("Missing SOI marker offset for tiff with old jpeg compression");
}
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;

Loading…
Cancel
Save