diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index 11a9bc5578..149aad07b0 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/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)
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
index d67a61355d..37ed0845f5 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
@@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
///
/// Class to handle cases where TIFF image data is compressed as a jpeg stream.
///
-internal sealed class JpegTiffCompression : TiffBaseDecompressor
+internal class JpegTiffCompression : TiffBaseDecompressor
{
private readonly JpegDecoderOptions options;
@@ -25,17 +25,17 @@ internal sealed class JpegTiffCompression : TiffBaseDecompressor
///
/// Initializes a new instance of the class.
///
- /// The specialized jpeg decoder options.
/// The memoryAllocator to use for buffer allocations.
/// The image width.
/// The bits per pixel.
+ /// The specialized jpeg decoder options.
/// The JPEG tables containing the quantization and/or Huffman tables.
/// The photometric interpretation.
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;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The memoryAllocator to use for buffer allocations.
+ /// The image width.
+ /// The bits per pixel.
+ /// The specialized jpeg decoder options.
+ /// The photometric interpretation.
+ public JpegTiffCompression(
+ MemoryAllocator memoryAllocator,
+ int width,
+ int bitsPerPixel,
+ JpegDecoderOptions options,
+ TiffPhotometricInterpretation photometricInterpretation)
+ : base(memoryAllocator, width, bitsPerPixel)
+ {
+ this.options = options;
+ this.photometricInterpretation = photometricInterpretation;
+ }
+
///
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span 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 image = Image.Load(stream);
+ CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer);
+ }
+ }
+
+ protected void DecodeJpegData(BufferedReadStream stream, Span 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 spectralConverterGray = new GrayJpegSpectralConverter(configuration);
+ HuffmanScanDecoder scanDecoderGray = new(stream, spectralConverterGray, cancellationToken);
+
+ if (loadTables)
{
- using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(configuration);
- var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, cancellationToken);
jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray);
- jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken);
-
- using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken);
- CopyImageBytesToBuffer(buffer, decompressedBuffer);
- break;
}
- case TiffPhotometricInterpretation.YCbCr:
- case TiffPhotometricInterpretation.Rgb:
+ jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken);
+
+ using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken);
+ CopyImageBytesToBuffer(buffer, decompressedBuffer);
+ break;
+ }
+
+ case TiffPhotometricInterpretation.YCbCr:
+ case TiffPhotometricInterpretation.Rgb:
+ {
+ using SpectralConverter spectralConverter = new TiffJpegSpectralConverter(configuration, this.photometricInterpretation);
+ HuffmanScanDecoder scanDecoder = new(stream, spectralConverter, cancellationToken);
+
+ if (loadTables)
{
- using SpectralConverter spectralConverter =
- new TiffJpegSpectralConverter(configuration, this.photometricInterpretation);
- var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, cancellationToken);
jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
- jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken);
-
- using Buffer2D 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 decompressedBuffer = spectralConverter.GetPixelBuffer(cancellationToken);
+ CopyImageBytesToBuffer(buffer, decompressedBuffer);
+ break;
}
- }
- else
- {
- using var image = Image.Load(stream);
- CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer);
+
+ default:
+ TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported");
+ break;
}
}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs
new file mode 100644
index 0000000000..c9fa9e9255
--- /dev/null
+++ b/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 buffer, CancellationToken cancellationToken)
+ {
+ stream.Position = this.startOfImageMarker;
+
+ this.DecodeJpegData(stream, buffer, false, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
index 4e1c9c2f84..347f09522c 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
@@ -20,12 +20,12 @@ internal class WebpTiffCompression : TiffBaseDecompressor
///
/// Initializes a new instance of the class.
///
- /// The general decoder options.
/// The memory allocator.
/// The width of the image.
/// The bits per pixel.
+ /// The general decoder options.
/// The predictor.
- 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;
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
index 34f0ed2dbd..70b9fec07b 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
@@ -52,4 +52,9 @@ internal enum TiffDecoderCompressionType
/// The image data is compressed as a WEBP stream.
///
Webp = 8,
+
+ ///
+ /// The image data is compressed as a OldJPEG compressed stream.
+ ///
+ OldJpeg = 9,
}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
index e09b93c02a..7b1f67b126 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
+++ b/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));
diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md
index 00d46c4157..d64e3339c9 100644
--- a/src/ImageSharp/Formats/Tiff/README.md
+++ b/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 | |
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 60bce2174d..59d8b6ecb6 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -126,6 +126,11 @@ internal class TiffDecoderCore : IImageDecoderInternals
///
public byte[] JpegTables { get; set; }
+ ///
+ /// Gets or sets the start of image marker for old Jpeg compression.
+ ///
+ public uint? OldJpegCompressionStartOfImageMarker { get; set; }
+
///
/// Gets or sets the planar configuration type to use when decoding the image.
///
@@ -232,7 +237,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
private ImageFrame DecodeFrame(ExifProfile tags, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- 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(this.configuration, width, height, imageFrameMetaData);
+ ImageFrame 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 stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span stripOffsets);
using IMemoryOwner stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span stripByteCounts);
@@ -358,7 +363,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
Buffer2D pixels = frame.PixelBuffer;
- var stripBuffers = new IMemoryOwner[stripsPerPixel];
+ IMemoryOwner[] stripBuffers = new IMemoryOwner[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);
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
index 7593840bbb..2198811e56 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
+++ b/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;