diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 8240a7458..510635cbb 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -21,12 +21,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; internal class SpectralConverter : SpectralConverter, IDisposable where TPixel : unmanaged, IPixel { - /// - /// instance associated with current - /// decoding routine. - /// - private readonly Configuration configuration; - private JpegFrame frame; private IRawJpegData jpegData; @@ -81,11 +75,15 @@ internal class SpectralConverter : SpectralConverter, IDisposable /// Optional target size for decoded image. public SpectralConverter(Configuration configuration, Size? targetSize = null) { - this.configuration = configuration; - + this.Configuration = configuration; this.targetSize = targetSize; } + /// + /// Gets the configuration instance associated with current decoding routine. + /// + public Configuration Configuration { get; } + /// /// Gets converted pixel buffer. /// @@ -134,7 +132,7 @@ internal class SpectralConverter : 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 : 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 : SpectralConverter, IDisposable this.pixelBuffer = allocator.Allocate2D( pixelSize.Width, pixelSize.Height, - this.configuration.PreferContiguousImageBuffers); + this.Configuration.PreferContiguousImageBuffers); this.paddedProxyPixelRow = allocator.Allocate(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 : 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 diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 11a9bc557..149aad07b 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/JpegCompressionUtils.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegCompressionUtils.cs new file mode 100644 index 000000000..4577a2492 --- /dev/null +++ b/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 buffer, Buffer2D pixelBuffer) + { + int offset = 0; + for (int y = 0; y < pixelBuffer.Height; y++) + { + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgb24Bytes(configuration, pixelRowSpan, buffer[offset..], pixelRowSpan.Length); + offset += Unsafe.SizeOf() * pixelRowSpan.Length; + } + } + + public static void CopyImageBytesToBuffer(Configuration configuration, Span buffer, Buffer2D pixelBuffer) + { + int offset = 0; + for (int y = 0; y < pixelBuffer.Height; y++) + { + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToL8Bytes(configuration, pixelRowSpan, buffer[offset..], pixelRowSpan.Length); + offset += Unsafe.SizeOf() * pixelRowSpan.Length; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index d67a61355..82b26232a 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/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 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: - { - 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; - } + this.DecodeJpegData(stream, buffer, cancellationToken); } else { - using var image = Image.Load(stream); - CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); + using Image image = Image.Load(this.options.GeneralOptions, stream); + JpegCompressionUtils.CopyImageBytesToBuffer(this.options.GeneralOptions.Configuration, buffer, image.Frames.RootFrame.PixelBuffer); } } - private static void CopyImageBytesToBuffer(Span buffer, Buffer2D pixelBuffer) + private void DecodeJpegData(BufferedReadStream stream, Span 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 pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); - Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); - rgbBytes.CopyTo(buffer[offset..]); - offset += rgbBytes.Length; - } - } + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + { + using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(configuration); + HuffmanScanDecoder scanDecoderGray = new(stream, spectralConverterGray, cancellationToken); - private static void CopyImageBytesToBuffer(Span buffer, Buffer2D pixelBuffer) - { - int offset = 0; - for (int y = 0; y < pixelBuffer.Height; y++) - { - Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); - Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); - rgbBytes.CopyTo(buffer[offset..]); - offset += rgbBytes.Length; + jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); + jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken); + + using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken); + JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverterGray.Configuration, buffer, decompressedBuffer); + break; + } + + case TiffPhotometricInterpretation.YCbCr: + case TiffPhotometricInterpretation.Rgb: + { + using SpectralConverter spectralConverter = new TiffJpegSpectralConverter(configuration, this.photometricInterpretation); + HuffmanScanDecoder scanDecoder = new(stream, spectralConverter, cancellationToken); + + jpegDecoder.LoadTables(this.jpegTables, scanDecoder); + jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken); + + using Buffer2D 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; } } 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 000000000..b58183ff6 --- /dev/null +++ b/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 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 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 spectralConverterGray = new GrayJpegSpectralConverter(configuration); + + jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken); + + using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken); + JpegCompressionUtils.CopyImageBytesToBuffer(spectralConverterGray.Configuration, buffer, decompressedBuffer); + break; + } + + case TiffPhotometricInterpretation.YCbCr: + case TiffPhotometricInterpretation.Rgb: + { + using SpectralConverter spectralConverter = new TiffOldJpegSpectralConverter(configuration, this.photometricInterpretation); + + jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken); + + using Buffer2D 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; + } + } + + /// + protected override void Dispose(bool disposing) + { + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffOldJpegSpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffOldJpegSpectralConverter{TPixel}.cs new file mode 100644 index 000000000..457c8d79c --- /dev/null +++ b/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; + +/// +/// Spectral converter for YCbCr TIFF's which use the OldJPEG compression. +/// The jpeg data should be always treated as YCbCr color space. +/// +/// The type of the pixel. +internal sealed class TiffOldJpegSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel +{ + private readonly TiffPhotometricInterpretation photometricInterpretation; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// Tiff photometric interpretation. + public TiffOldJpegSpectralConverter(Configuration configuration, TiffPhotometricInterpretation photometricInterpretation) + : base(configuration) + => this.photometricInterpretation = photometricInterpretation; + + /// + 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}"), + }; +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs index 34f0ed2db..70b9fec07 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 e09b93c02..b9a1f3155 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) { @@ -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); diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 00d46c415..0ce467e3f 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/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 diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 60bce2174..59d8b6ecb 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 7593840bb..648b4a093 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/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; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index bcfd759a0..73c5c00bc 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -664,6 +664,28 @@ public class TiffDecoderTests : TiffDecoderBaseTester public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => 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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions decoderOptions = new() + { + MaxFrames = 1 + }; + using Image 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(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 767ec3423..193e07353 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/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"; diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 57d5a8af4..3392d6814 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/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); diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 020184835..cd16fe4b2 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -518,7 +518,8 @@ public static class TestImageExtensions this Image image, ITestImageProvider provider, ImageComparer comparer, - IImageDecoder referenceDecoder = null) + IImageDecoder referenceDecoder = null, + DecoderOptions referenceDecoderOptions = null) where TPixel : unmanaged, IPixel { string path = TestImageProvider.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 original = referenceDecoder.Decode(DecoderOptions.Default, stream, default)) + using MemoryStream stream = new(testFile.Bytes); + using (Image original = referenceDecoder.Decode(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 original = referenceDecoder.Decode(DecoderOptions.Default, stream, default)) { comparer.VerifySimilarity(original, image); diff --git a/tests/Images/Input/Tiff/OldJpegCompression.tiff b/tests/Images/Input/Tiff/OldJpegCompression.tiff new file mode 100644 index 000000000..f58af0bf0 --- /dev/null +++ b/tests/Images/Input/Tiff/OldJpegCompression.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a5a02fb888a47ed51aa6d72122f9d1996311e943be62bef4829bc1aa9f457e8 +size 214023 diff --git a/tests/Images/Input/Tiff/OldJpegCompression2.tiff b/tests/Images/Input/Tiff/OldJpegCompression2.tiff new file mode 100644 index 000000000..1df25125d --- /dev/null +++ b/tests/Images/Input/Tiff/OldJpegCompression2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8f722bc322b9a7621d1d858bec36ea78d31b85221f314a2b9872d3af91d600a +size 5052 diff --git a/tests/Images/Input/Tiff/OldJpegCompression3.tiff b/tests/Images/Input/Tiff/OldJpegCompression3.tiff new file mode 100644 index 000000000..38dce9d50 --- /dev/null +++ b/tests/Images/Input/Tiff/OldJpegCompression3.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b2b73290a40c49a4a29e51ad53dadea65c1ceafc6a5384430f7092d613830db +size 1390339 diff --git a/tests/Images/Input/Tiff/OldJpegCompressionGray.tiff b/tests/Images/Input/Tiff/OldJpegCompressionGray.tiff new file mode 100644 index 000000000..23a697a11 --- /dev/null +++ b/tests/Images/Input/Tiff/OldJpegCompressionGray.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:790662b0fc48d5604e80a9689eb28cfa231be3eadc33f34c9bf43c211cce5c16 +size 440613 diff --git a/tests/Images/Input/Tiff/YCbCrOldJpegCompressed.tiff b/tests/Images/Input/Tiff/YCbCrOldJpegCompressed.tiff new file mode 100644 index 000000000..26eff36d5 --- /dev/null +++ b/tests/Images/Input/Tiff/YCbCrOldJpegCompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6527e69a3cce3e6d425c35695319711ad36d939f934902faa47f6ee1f1c7e08 +size 10076700