diff --git a/src/ImageSharp/Formats/Tiff/Compression/PackBitsTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/PackBitsTiffCompression.cs new file mode 100644 index 0000000000..0ac30e8f17 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/PackBitsTiffCompression.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Tiff +{ + using System; + using System.Buffers; + using System.IO; + using System.Runtime.CompilerServices; + + /// + /// Class to handle cases where TIFF image data is compressed using PackBits compression. + /// + internal static class PackBitsTiffCompression + { + /// + /// Decompresses image data into the supplied buffer. + /// + /// The to read image data from. + /// The number of bytes to read from the input stream. + /// The output buffer for uncompressed data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Decompress(Stream stream, int byteCount, byte[] buffer) + { + byte[] compressedData = ArrayPool.Shared.Rent(byteCount); + + try + { + stream.ReadFull(compressedData, byteCount); + int compressedOffset = 0; + int decompressedOffset = 0; + + while (compressedOffset < byteCount) + { + byte headerByte = compressedData[compressedOffset]; + + if (headerByte <= (byte)127) + { + int literalOffset = compressedOffset + 1; + int literalLength = compressedData[compressedOffset] + 1; + + Array.Copy(compressedData, literalOffset, buffer, decompressedOffset, literalLength); + + compressedOffset += literalLength + 1; + decompressedOffset += literalLength; + } + else if (headerByte == (byte)0x80) + { + compressedOffset += 1; + } + else + { + byte repeatData = compressedData[compressedOffset + 1]; + int repeatLength = 257 - headerByte; + + ArrayCopyRepeat(repeatData, buffer, decompressedOffset, repeatLength); + + compressedOffset += 2; + decompressedOffset += repeatLength; + } + } + } + finally + { + ArrayPool.Shared.Return(compressedData); + } + } + + private static void ArrayCopyRepeat(byte value, byte[] destinationArray, int destinationIndex, int length) + { + for (int i = 0; i < length; i++) + { + destinationArray[i + destinationIndex] = value; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs index c661ea8947..6f9ce8f870 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs @@ -13,6 +13,11 @@ namespace ImageSharp.Formats.Tiff /// /// Image data is stored uncompressed in the TIFF file. /// - None = 0 + None = 0, + + /// + /// Image data is compressed using PackBits compression. + /// + PackBits = 1, } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index cc2d0f8b7d..7c8efad0f4 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -267,6 +267,12 @@ namespace ImageSharp.Formats break; } + case TiffCompression.PackBits: + { + this.CompressionType = TiffCompressionType.PackBits; + break; + } + default: { throw new NotSupportedException("The specified TIFF compression format is not supported."); @@ -377,6 +383,9 @@ namespace ImageSharp.Formats case TiffCompressionType.None: NoneTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer); break; + case TiffCompressionType.PackBits: + PackBitsTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer); + break; default: throw new InvalidOperationException(); } diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs new file mode 100644 index 0000000000..85a7bd729f --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System.IO; + using Xunit; + + using ImageSharp.Formats.Tiff; + + public class PackBitsTiffCompressionTests + { + [Theory] + [InlineData(new byte[] { }, new byte[] { })] + [InlineData(new byte[] { 0x00, 0x2A }, new byte[] { 0x2A })] // Read one byte + [InlineData(new byte[] { 0x01, 0x15, 0x32 }, new byte[] { 0x15, 0x32 })] // Read two bytes + [InlineData(new byte[] { 0xFF, 0x2A }, new byte[] { 0x2A, 0x2A })] // Repeat two bytes + [InlineData(new byte[] { 0xFE, 0x2A }, new byte[] { 0x2A, 0x2A, 0x2A })] // Repeat three bytes + [InlineData(new byte[] { 0x80 }, new byte[] { })] // Read a 'No operation' byte + [InlineData(new byte[] { 0x01, 0x15, 0x32, 0x80, 0xFF, 0xA2 }, new byte[] { 0x15, 0x32, 0xA2, 0xA2 })] // Read two bytes, nop, repeat two bytes + [InlineData(new byte[] { 0xFE, 0xAA, 0x02, 0x80, 0x00, 0x2A, 0xFD, 0xAA, 0x03, 0x80, 0x00, 0x2A, 0x22, 0xF7, 0xAA }, + new byte[] { 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0xAA, 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA })] // Apple PackBits sample + public void Decompress_ReadsData(byte[] inputData, byte[] expectedResult) + { + Stream stream = new MemoryStream(inputData); + byte[] buffer = new byte[expectedResult.Length]; + + PackBitsTiffCompression.Decompress(stream, inputData.Length, buffer); + + Assert.Equal(expectedResult, buffer); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs index f302c17b30..642cc2c0ed 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs @@ -123,6 +123,8 @@ namespace ImageSharp.Tests [Theory] [InlineData(false, TiffCompression.None, TiffCompressionType.None)] [InlineData(true, TiffCompression.None, TiffCompressionType.None)] + [InlineData(false, TiffCompression.PackBits, TiffCompressionType.PackBits)] + [InlineData(true, TiffCompression.PackBits, TiffCompressionType.PackBits)] public void ReadImageFormat_DeterminesCorrectCompressionImplementation(bool isLittleEndian, ushort compression, int compressionType) { Stream stream = CreateTiffGenIfd() @@ -147,7 +149,6 @@ namespace ImageSharp.Tests [InlineData(false, TiffCompression.Lzw)] [InlineData(false, TiffCompression.OldDeflate)] [InlineData(false, TiffCompression.OldJpeg)] - [InlineData(false, TiffCompression.PackBits)] [InlineData(false, 999)] [InlineData(true, TiffCompression.Ccitt1D)] [InlineData(true, TiffCompression.CcittGroup3Fax)] @@ -159,7 +160,6 @@ namespace ImageSharp.Tests [InlineData(true, TiffCompression.Lzw)] [InlineData(true, TiffCompression.OldDeflate)] [InlineData(true, TiffCompression.OldJpeg)] - [InlineData(true, TiffCompression.PackBits)] [InlineData(true, 999)] public void ReadImageFormat_ThrowsExceptionForUnsupportedCompression(bool isLittleEndian, ushort compression) {