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;