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

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

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

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>
/// Initializes a new instance of the <see cref="WebpTiffCompression"/> class.
/// </summary>
/// <param name="options">The general decoder options.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="width">The width of the image.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param>
/// <param name="options">The general decoder options.</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)
=> 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.
/// </summary>
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,
FaxCompressionOptions faxOptions,
byte[] jpegTables,
uint oldJpegStartOfImageMarker,
TiffFillOrder fillOrder,
ByteOrder byteOrder)
{
@ -58,11 +59,15 @@ internal static class TiffDecompressorsFactory
case TiffDecoderCompressionType.Jpeg:
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:
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:
throw TiffThrowHelper.NotSupportedDecompressor(nameof(method));

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

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

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

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

@ -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,25 @@ internal static class TiffDecoderOptionsParser
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:
{
options.CompressionType = TiffDecoderCompressionType.Jpeg;

Loading…
Cancel
Save