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 0000000000..a52c7e5293 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegCompressionUtils.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; + +internal static class JpegCompressionUtils +{ + public 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; + } + } + + public 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; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index 37ed0845f5..818bb3d6dc 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; @@ -14,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; /// /// Class to handle cases where TIFF image data is compressed as a jpeg stream. /// -internal class JpegTiffCompression : TiffBaseDecompressor +internal sealed class JpegTiffCompression : TiffBaseDecompressor { private readonly JpegDecoderOptions options; @@ -70,16 +69,16 @@ internal class JpegTiffCompression : TiffBaseDecompressor { if (this.jpegTables != null) { - this.DecodeJpegData(stream, buffer, true, cancellationToken); + this.DecodeJpegData(stream, buffer, cancellationToken); } else { using Image image = Image.Load(stream); - CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); + JpegCompressionUtils.CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); } } - protected void DecodeJpegData(BufferedReadStream stream, Span buffer, bool loadTables, CancellationToken cancellationToken) + private void DecodeJpegData(BufferedReadStream stream, Span buffer, CancellationToken cancellationToken) { using JpegDecoderCore jpegDecoder = new(this.options); Configuration configuration = this.options.GeneralOptions.Configuration; @@ -91,15 +90,11 @@ internal class JpegTiffCompression : TiffBaseDecompressor using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(configuration); HuffmanScanDecoder scanDecoderGray = new(stream, spectralConverterGray, cancellationToken); - if (loadTables) - { - jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); - } - + jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); jpegDecoder.ParseStream(stream, spectralConverterGray, cancellationToken); using Buffer2D decompressedBuffer = spectralConverterGray.GetPixelBuffer(cancellationToken); - CopyImageBytesToBuffer(buffer, decompressedBuffer); + JpegCompressionUtils.CopyImageBytesToBuffer(buffer, decompressedBuffer); break; } @@ -109,15 +104,11 @@ internal class JpegTiffCompression : TiffBaseDecompressor using SpectralConverter spectralConverter = new TiffJpegSpectralConverter(configuration, this.photometricInterpretation); HuffmanScanDecoder scanDecoder = new(stream, spectralConverter, cancellationToken); - if (loadTables) - { - jpegDecoder.LoadTables(this.jpegTables, scanDecoder); - } - + jpegDecoder.LoadTables(this.jpegTables, scanDecoder); jpegDecoder.ParseStream(stream, spectralConverter, cancellationToken); using Buffer2D decompressedBuffer = spectralConverter.GetPixelBuffer(cancellationToken); - CopyImageBytesToBuffer(buffer, decompressedBuffer); + JpegCompressionUtils.CopyImageBytesToBuffer(buffer, decompressedBuffer); break; } @@ -127,30 +118,6 @@ internal class JpegTiffCompression : TiffBaseDecompressor } } - 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; - } - } - - 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; - } - } - /// protected override void Dispose(bool disposing) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs index c9fa9e9255..e6ddf95708 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/OldJpegTiffCompression.cs @@ -2,16 +2,22 @@ // 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 : JpegTiffCompression +internal sealed class OldJpegTiffCompression : TiffBaseDecompressor { + private readonly JpegDecoderOptions options; + private readonly uint startOfImageMarker; + private readonly TiffPhotometricInterpretation photometricInterpretation; + public OldJpegTiffCompression( MemoryAllocator memoryAllocator, int width, @@ -19,12 +25,58 @@ internal sealed class OldJpegTiffCompression : JpegTiffCompression JpegDecoderOptions options, uint startOfImageMarker, TiffPhotometricInterpretation photometricInterpretation) - : base(memoryAllocator, width, bitsPerPixel, options, photometricInterpretation) => this.startOfImageMarker = startOfImageMarker; + : 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) { stream.Position = this.startOfImageMarker; - this.DecodeJpegData(stream, buffer, false, cancellationToken); + this.DecodeJpegData(stream, buffer, cancellationToken); + } + + 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(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(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 0000000000..457c8d79cb --- /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/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 2198811e56..85cabdc494 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)) { @@ -478,18 +478,14 @@ internal static class TiffDecoderOptionsParser { options.CompressionType = TiffDecoderCompressionType.OldJpeg; + // Like libtiff: always assume PhotometricInterpretation to be YCbCr, if the compression is old jpeg. + options.PhotometricInterpretation = TiffPhotometricInterpretation.YCbCr; + 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; }