From ea05900d3b93844c1b2b24c50aecc23289e0470f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 11 Aug 2021 12:36:27 +0200 Subject: [PATCH] Add decompressor for tiff's with jpeg compression --- .../Components/Decoder/HuffmanScanDecoder.cs | 12 ++- .../Formats/Jpeg/JpegDecoderCore.cs | 74 +++++++++++++++++-- .../Decompressors/JpegTiffCompression.cs | 70 ++++++++++++++++++ .../Compression/TiffDecoderCompressionType.cs | 5 ++ .../Compression/TiffDecompressorsFactory.cs | 6 ++ .../Formats/Tiff/TiffDecoderCore.cs | 9 +++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 7 ++ 7 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index 70a4465121..28ef6a96f7 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -38,10 +38,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// private int restartInterval; - // How many mcu's are left to do. + /// + /// How many mcu's are left to do. + /// private int todo; - // The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero. + /// + /// The End-Of-Block countdown for ending the sequence prematurely when the remaining coefficients are zero. + /// private int eobrun; /// @@ -54,7 +58,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// private readonly HuffmanTable[] acHuffmanTables; - // The unzig data. + /// + /// The unzig data. + /// private ZigZag dctZigZag; private HuffmanScanBuffer scanBuffer; diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 896e5f0aaf..4f54f5078a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Buffers.Binary; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -136,8 +137,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Finds the next file marker within the byte stream. /// - /// The buffer to read file markers to - /// The input stream + /// The buffer to read file markers to. + /// The input stream. /// The public static JpegFileMarker FindNextFileMarker(byte[] marker, BufferedReadStream stream) { @@ -200,6 +201,67 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata); } + /// + /// Load quantization and/or Huffman tables for subsequent use for jpeg's embedded in tiff's, + /// so those tables do not need to be duplicated with segmented tiff's (tiff's with multiple strips). + /// + /// The table bytes. + /// The scan decoder. + public void LoadTables(byte[] tableBytes, HuffmanScanDecoder huffmanScanDecoder) + { + this.Metadata = new ImageMetadata(); + this.QuantizationTables = new Block8x8F[4]; + this.scanDecoder = huffmanScanDecoder; + using var ms = new MemoryStream(tableBytes); + using var stream = new BufferedReadStream(this.Configuration, ms); + + // Check for the Start Of Image marker. + stream.Read(this.markerBuffer, 0, 2); + var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); + if (fileMarker.Marker != JpegConstants.Markers.SOI) + { + JpegThrowHelper.ThrowInvalidImageContentException("Missing SOI marker."); + } + + // Read next marker. + stream.Read(this.markerBuffer, 0, 2); + byte marker = this.markerBuffer[1]; + fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); + + while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) + { + if (!fileMarker.Invalid) + { + // Get the marker length. + int remaining = this.ReadUint16(stream) - 2; + + switch (fileMarker.Marker) + { + case JpegConstants.Markers.SOI: + break; + case JpegConstants.Markers.RST0: + case JpegConstants.Markers.RST7: + break; + case JpegConstants.Markers.DHT: + this.ProcessDefineHuffmanTablesMarker(stream, remaining); + break; + case JpegConstants.Markers.DQT: + this.ProcessDefineQuantizationTablesMarker(stream, remaining); + break; + case JpegConstants.Markers.DRI: + this.ProcessDefineRestartIntervalMarker(stream, remaining); + break; + case JpegConstants.Markers.EOI: + return; + } + } + + // Read next marker. + stream.Read(this.markerBuffer, 0, 2); + fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); + } + } + /// /// Parses the input stream for file markers. /// @@ -225,7 +287,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg stream.Read(this.markerBuffer, 0, 2); byte marker = this.markerBuffer[1]; fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); - this.QuantizationTables = new Block8x8F[4]; + this.QuantizationTables ??= new Block8x8F[4]; // Break only when we discover a valid EOI marker. // https://github.com/SixLabors/ImageSharp/issues/695 @@ -236,7 +298,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (!fileMarker.Invalid) { - // Get the marker length + // Get the marker length. int remaining = this.ReadUint16(stream) - 2; switch (fileMarker.Marker) @@ -345,7 +407,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Returns the correct colorspace based on the image component count + /// Returns the correct colorspace based on the image component count. /// /// The private JpegColorSpace DeduceJpegColorSpace(byte componentCount) @@ -576,7 +638,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop. - /// The content of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks. + /// The tableBytes of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks. /// /// The input stream. /// The remaining bytes in the segment block. diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs new file mode 100644 index 0000000000..5a26df95ac --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +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.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +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 + { + private readonly Configuration configuration; + + private readonly byte[] jpegTables; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The memoryAllocator to use for buffer allocations. + /// The image width. + /// The bits per pixel. + /// The JPEG tables containing the quantization and/or Huffman tables. + /// The predictor. + public JpegTiffCompression(Configuration configuration, MemoryAllocator memoryAllocator, int width, int bitsPerPixel, byte[] jpegTables, TiffPredictor predictor = TiffPredictor.None) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + this.configuration = configuration; + this.jpegTables = jpegTables; + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + { + var jpegDecoder = new JpegDecoderCore(this.configuration, new JpegDecoder()); + + // Should we pass through the CancellationToken from the tiff decoder? + using var spectralConverter = new SpectralConverter(this.configuration, CancellationToken.None); + var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None); + jpegDecoder.LoadTables(this.jpegTables, scanDecoder); + scanDecoder.ResetInterval = 0; + jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None); + + var image = new Image(this.configuration, spectralConverter.PixelBuffer, new ImageMetadata()); + int offset = 0; + for (int y = 0; y < image.Height; y++) + { + Span pixelRowSpan = image.GetPixelRowSpan(y); + Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); + rgbBytes.CopyTo(buffer.Slice(offset)); + offset += rgbBytes.Length; + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs index 80bc0af5ab..6dd19c5552 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs @@ -37,5 +37,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// Image data is compressed using modified huffman compression. /// HuffmanRle = 5, + + /// + /// The image data is compressed as a JPEG stream. + /// + Jpeg = 6, } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index b1562223aa..ee44a70219 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression internal static class TiffDecompressorsFactory { public static TiffBaseDecompressor Create( + Configuration configuration, TiffDecoderCompressionType method, MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation, @@ -19,6 +20,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression TiffColorType colorType, TiffPredictor predictor, FaxCompressionOptions faxOptions, + byte[] jpegTables, TiffFillOrder fillOrder, ByteOrder byteOrder) { @@ -50,6 +52,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); + case TiffDecoderCompressionType.Jpeg: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new JpegTiffCompression(configuration, allocator, width, bitsPerPixel, jpegTables); + default: throw TiffThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 28afe4c6f2..5b8c974b46 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -105,6 +105,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffFillOrder FillOrder { get; set; } + /// + /// Gets or sets the JPEG tables when jpeg compression is used. + /// + public byte[] JpegTables { get; set; } + /// /// Gets or sets the planar configuration type to use when decoding the image. /// @@ -290,6 +295,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff } using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( + this.Configuration, this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, @@ -298,6 +304,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.ColorType, this.Predictor, this.FaxCompressionOptions, + this.JpegTables, this.FillOrder, this.byteOrder); @@ -350,6 +357,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Buffer2D pixels = frame.PixelBuffer; using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( + this.Configuration, this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, @@ -358,6 +366,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.ColorType, this.Predictor, this.FaxCompressionOptions, + this.JpegTables, this.FillOrder, this.byteOrder); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index bb435affcc..2a2be6d742 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -87,6 +87,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value; options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; options.FillOrder = fillOrder; + options.JpegTables = exifProfile.GetValue(ExifTag.JPEGTables)?.Value; options.ParseColorType(exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile); @@ -424,6 +425,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } + case TiffCompression.Jpeg: + { + options.CompressionType = TiffDecoderCompressionType.Jpeg; + break; + } + default: { TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported");