From 8122bed91b8b520a165f616aca3c83849188e24f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 22 Nov 2020 20:07:37 +0100 Subject: [PATCH] Add support for decompressing huffman encoded tiffs --- .../Formats/Tiff/Compression/T4BitReader.cs | 47 ++++++++++-- .../Tiff/Compression/T4TiffCompression.cs | 9 +-- .../Tiff/Compression/TiffBaseCompression.cs | 7 +- .../Compression/TiffCompressionFactory.cs | 6 +- .../Tiff/Compression/TiffCompressionType.cs | 5 ++ .../TiffModifiedHuffmanCompression.cs | 72 +++++++++++++++++++ .../Formats/Tiff/TiffDecoderCore.cs | 13 ++-- .../Formats/Tiff/TiffDecoderHelpers.cs | 6 ++ tests/ImageSharp.Tests/TestImages.cs | 3 +- .../Input/Tiff/Calliphora_huffman_rle.tif | 3 + 10 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/Compression/TiffModifiedHuffmanCompression.cs create mode 100644 tests/Images/Input/Tiff/Calliphora_huffman_rle.tif diff --git a/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs index 043e5b313..ed2fad7ed 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs @@ -61,6 +61,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// private bool isStartOfRow; + private readonly bool isModifiedHuffmanRle; + private readonly int dataLength; private const int MinCodeLength = 2; @@ -223,11 +225,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// The compressed input stream. /// The number of bytes to read from the stream. /// The memory allocator. - public T4BitReader(Stream input, int bytesToRead, MemoryAllocator allocator) + /// Indicates, if its the modified huffman code variation. + public T4BitReader(Stream input, int bytesToRead, MemoryAllocator allocator, bool isModifiedHuffman = false) { this.Data = allocator.Allocate(bytesToRead); this.ReadImageDataFromStream(input, bytesToRead, allocator); + this.isModifiedHuffmanRle = isModifiedHuffman; this.dataLength = bytesToRead; this.bitsRead = 0; this.value = 0; @@ -302,7 +306,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression this.Reset(); - if (this.isFirstScanLine) + if (this.isFirstScanLine && !this.isModifiedHuffmanRle) { // We expect an EOL before the first data. this.value = this.ReadValue(12); @@ -372,9 +376,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression if (this.IsEndOfScanLine) { - // Each new row starts with a white run. - this.isWhiteRun = true; - this.isStartOfRow = true; + this.StartNewRow(); } } while (!this.IsEndOfScanLine); @@ -382,6 +384,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression this.isFirstScanLine = false; } + public void StartNewRow() + { + // Each new row starts with a white run. + this.isWhiteRun = true; + this.isStartOfRow = true; + this.terminationCodeFound = false; + + if (this.isModifiedHuffmanRle) + { + int pad = 8 - (this.bitsRead % 8); + if (pad != 8) + { + // Skip padding bits, move to next byte. + this.position++; + this.bitsRead = 0; + } + } + } + /// public void Dispose() { @@ -601,6 +622,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression case 12: { + if (this.isModifiedHuffmanRle) + { + if (this.value == 1) + { + return true; + } + } + return WhiteLen12MakeupCodes.ContainsKey(this.value); } } @@ -619,6 +648,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression case 11: { + if (this.isModifiedHuffmanRle) + { + if (this.value == 0) + { + return true; + } + } + return BlackLen11MakeupCodes.ContainsKey(this.value); } diff --git a/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs index 8c16cde68..6aeb5af81 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs @@ -17,8 +17,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// /// The memory allocator. /// The photometric interpretation. - public T4TiffCompression(MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation) - : base(allocator, photometricInterpretation) + /// The image width. + public T4TiffCompression(MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation, int width) + : base(allocator, photometricInterpretation, width) { } @@ -63,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } - private void WriteBits(Span buffer, int pos, uint count, int value) + protected void WriteBits(Span buffer, int pos, uint count, int value) { int bitPos = pos % 8; int bufferPos = pos / 8; @@ -83,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } - private void WriteBit(Span buffer, int bufferPos, int bitPos, int value) + protected void WriteBit(Span buffer, int bufferPos, int bitPos, int value) { buffer[bufferPos] |= (byte)(value << (7 - bitPos)); } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs index 1a2b814fe..fb05a9f25 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs @@ -16,18 +16,23 @@ namespace SixLabors.ImageSharp.Formats.Tiff private TiffPhotometricInterpretation photometricInterpretation; + private int width; + public TiffBaseCompression(MemoryAllocator allocator) => this.allocator = allocator; - public TiffBaseCompression(MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation) + public TiffBaseCompression(MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation, int width) { this.allocator = allocator; this.photometricInterpretation = photometricInterpretation; + this.width = width; } protected MemoryAllocator Allocator => this.allocator; protected TiffPhotometricInterpretation PhotometricInterpretation => this.photometricInterpretation; + protected int Width => this.width; + /// /// Decompresses image data into the supplied buffer. /// diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs index e15cc451f..3a0e5e6da 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff { internal static class TiffCompressionFactory { - public static TiffBaseCompression Create(TiffCompressionType compressionType, MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation) + public static TiffBaseCompression Create(TiffCompressionType compressionType, MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation, int width) { switch (compressionType) { @@ -21,7 +21,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff case TiffCompressionType.Lzw: return new LzwTiffCompression(allocator); case TiffCompressionType.T4: - return new T4TiffCompression(allocator, photometricInterpretation); + return new T4TiffCompression(allocator, photometricInterpretation, width); + case TiffCompressionType.HuffmanRle: + return new TiffModifiedHuffmanCompression(allocator, photometricInterpretation, width); default: throw TiffThrowHelper.NotSupportedCompression(nameof(compressionType)); } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs index 665e4aca2..8a33948f9 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs @@ -32,5 +32,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Image data is compressed using T4-encoding: CCITT T.4. /// T4 = 4, + + /// + /// Image data is compressed using modified huffman compression. + /// + HuffmanRle = 5, } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffModifiedHuffmanCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffModifiedHuffmanCompression.cs new file mode 100644 index 000000000..1201ab66a --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffModifiedHuffmanCompression.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + /// + /// Class to handle cases where TIFF image data is compressed using Modified Huffman Compression. + /// + internal class TiffModifiedHuffmanCompression : T4TiffCompression + { + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The photometric interpretation. + /// The image width. + public TiffModifiedHuffmanCompression(MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation, int width) + : base(allocator, photometricInterpretation, width) + { + } + + /// + public override void Decompress(Stream stream, int byteCount, Span buffer) + { + bool isWhiteZero = this.PhotometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; + int whiteValue = isWhiteZero ? 0 : 1; + int blackValue = isWhiteZero ? 1 : 0; + + using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, isModifiedHuffman: true); + + uint bitsWritten = 0; + uint pixelsWritten = 0; + while (bitReader.HasMoreData) + { + bitReader.ReadNextRun(); + + if (bitReader.RunLength > 0) + { + if (bitReader.IsWhiteRun) + { + this.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, whiteValue); + bitsWritten += bitReader.RunLength; + pixelsWritten += bitReader.RunLength; + } + else + { + this.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, blackValue); + bitsWritten += bitReader.RunLength; + pixelsWritten += bitReader.RunLength; + } + } + + if (pixelsWritten % this.Width == 0) + { + bitReader.StartNewRow(); + + // Write padding bytes, if necessary. + uint pad = 8 - (bitsWritten % 8); + if (pad != 8) + { + this.WriteBits(buffer, (int)bitsWritten, pad, 0); + bitsWritten += pad; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 5c253d4c8..bbf361e0d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -210,11 +210,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { - this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts); + this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts, width); } else { - this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts); + this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts, width); } return frame; @@ -258,7 +258,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The number of rows per strip of data. /// An array of byte offsets to each strip in the image. /// An array of the size of each strip (in bytes). - private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts) + /// The image width. + private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width) where TPixel : unmanaged, IPixel { int stripsPerPixel = this.BitsPerSample.Length; @@ -276,7 +277,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff stripBuffers[stripIndex] = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize); } - TiffBaseCompression decompressor = TiffCompressionFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation); + TiffBaseCompression decompressor = TiffCompressionFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, width); RgbPlanarTiffColor colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap); @@ -304,7 +305,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts) + private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width) where TPixel : unmanaged, IPixel { int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); @@ -313,7 +314,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Buffer2D pixels = frame.PixelBuffer; - TiffBaseCompression decompressor = TiffCompressionFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation); + TiffBaseCompression decompressor = TiffCompressionFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, width); TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs index 5348be8ce..6db776039 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs @@ -352,6 +352,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } + case TiffCompression.Ccitt1D: + { + options.CompressionType = TiffCompressionType.HuffmanRle; + break; + } + default: { TiffThrowHelper.ThrowNotSupported("The specified TIFF compression format is not supported: " + compression); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index fa9e4e290..7f16f4aa7 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -512,6 +512,7 @@ namespace SixLabors.ImageSharp.Tests public const string Calliphora_RgbPackbits = "Tiff/Calliphora_rgb_packbits.tiff"; public const string Calliphora_RgbUncompressed = "Tiff/Calliphora_rgb_uncompressed.tiff"; public const string Calliphora_Fax3Compressed = "Tiff/Calliphora_ccitt_fax3.tif"; + public const string Calliphora_HuffmanCompressed = "Tiff/Calliphora_huffman_rle.tif"; public const string CcittFax3AllTermCodes = "Tiff/ccitt_fax3_all_terminating_codes.tif"; public const string CcittFax3AllMakupCodes = "Tiff/ccitt_fax3_all_makeupcodes_codes.tif"; @@ -545,7 +546,7 @@ namespace SixLabors.ImageSharp.Tests public const string SampleMetadata = "Tiff/metadata_sample.tiff"; - public static readonly string[] All = { Calliphora_GrayscaleUncompressed, Calliphora_PaletteUncompressed, /*Calliphora_RgbDeflate_Predictor, Calliphora_RgbLzwe_Predictor, */ Calliphora_RgbPackbits, Calliphora_RgbUncompressed, Calliphora_Fax3Compressed, CcittFax3AllTermCodes, CcittFax3AllMakupCodes, GrayscaleDeflateMultistrip, GrayscaleUncompressed, PaletteDeflateMultistrip, PaletteUncompressed, /*RgbDeflate_Predictor,*/ RgbDeflateMultistrip, /*RgbJpeg,*/ /*RgbLzw_Predictor, RgbLzwMultistrip_Predictor,*/ RgbLzw_NoPredictor_Multistrip, RgbLzw_NoPredictor_Multistrip_Motorola, RgbLzw_NoPredictor_Singlestrip_Motorola, RgbPackbits, RgbPackbitsMultistrip, RgbUncompressed, /* MultiframeLzw_Predictor, MultiFrameDifferentVariants, SampleMetadata,*/ SmallRgbDeflate, SmallRgbLzw, }; + public static readonly string[] All = { Calliphora_GrayscaleUncompressed, Calliphora_PaletteUncompressed, /*Calliphora_RgbDeflate_Predictor, Calliphora_RgbLzwe_Predictor, */ Calliphora_RgbPackbits, Calliphora_RgbUncompressed, Calliphora_HuffmanCompressed, Calliphora_Fax3Compressed, CcittFax3AllTermCodes, CcittFax3AllMakupCodes, GrayscaleDeflateMultistrip, GrayscaleUncompressed, PaletteDeflateMultistrip, PaletteUncompressed, /*RgbDeflate_Predictor,*/ RgbDeflateMultistrip, /*RgbJpeg,*/ /*RgbLzw_Predictor, RgbLzwMultistrip_Predictor,*/ RgbLzw_NoPredictor_Multistrip, RgbLzw_NoPredictor_Multistrip_Motorola, RgbLzw_NoPredictor_Singlestrip_Motorola, RgbPackbits, RgbPackbitsMultistrip, RgbUncompressed, /* MultiframeLzw_Predictor, MultiFrameDifferentVariants, SampleMetadata,*/ SmallRgbDeflate, SmallRgbLzw, }; public static readonly string[] Multiframes = { MultiframeDeflateWithPreview /*MultiframeLzw_Predictor, MultiFrameDifferentSize, MultiframeDifferentSizeTiled, MultiFrameDifferentVariants,*/ }; diff --git a/tests/Images/Input/Tiff/Calliphora_huffman_rle.tif b/tests/Images/Input/Tiff/Calliphora_huffman_rle.tif new file mode 100644 index 000000000..e0a39d248 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_huffman_rle.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a4f687de9925863b1c9f32f53b6c05fb121f9d7b02ff5869113c4745433f10d +size 124644