diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittReferenceScanline.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittReferenceScanline.cs new file mode 100644 index 000000000..64da69402 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittReferenceScanline.cs @@ -0,0 +1,154 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Represents a reference scan line for CCITT 2D decoding. + /// + internal readonly ref struct CcittReferenceScanline + { + private readonly ReadOnlySpan scanLine; + private readonly int width; + private readonly byte whiteByte; + + /// + /// Initializes a new instance of the struct. + /// + /// Indicates, if white is zero, otherwise black is zero. + /// The scan line. + public CcittReferenceScanline(bool whiteIsZero, ReadOnlySpan scanLine) + { + this.scanLine = scanLine; + this.width = scanLine.Length; + this.whiteByte = whiteIsZero ? (byte)0 : (byte)255; + } + + /// + /// Initializes a new instance of the struct. + /// + /// Indicates, if white is zero, otherwise black is zero. + /// The width of the scanline. + public CcittReferenceScanline(bool whiteIsZero, int width) + { + this.scanLine = default; + this.width = width; + this.whiteByte = whiteIsZero ? (byte)0 : (byte)255; + } + + public bool IsEmpty => this.scanLine.IsEmpty; + + /// + /// Finds b1: The first changing element on the reference line to the right of a0 and of opposite color to a0. + /// + /// The reference or starting element om the coding line. + /// Fill byte. + /// Position of b1. + public int FindB1(int a0, byte a0Byte) + { + if (this.scanLine.IsEmpty) + { + return this.FindB1ForImaginaryWhiteLine(a0, a0Byte); + } + + return this.FindB1ForNormalLine(a0, a0Byte); + } + + /// + /// Finds b2: The next changing element to the right of b1 on the reference line. + /// + /// The first changing element on the reference line to the right of a0 and opposite of color to a0. + /// Position of b1. + public int FindB2(int b1) + { + if (this.scanLine.IsEmpty) + { + return this.FindB2ForImaginaryWhiteLine(); + } + + return this.FindB2ForNormalLine(b1); + } + + private int FindB1ForImaginaryWhiteLine(int a0, byte a0Byte) + { + if (a0 < 0) + { + if (a0Byte != this.whiteByte) + { + return 0; + } + } + + return this.width; + } + + private int FindB1ForNormalLine(int a0, byte a0Byte) + { + int offset = 0; + if (a0 < 0) + { + if (a0Byte != this.scanLine[0]) + { + return 0; + } + } + else + { + offset = a0; + } + + ReadOnlySpan searchSpace = this.scanLine.Slice(offset); + byte searchByte = (byte)~a0Byte; + int index = searchSpace.IndexOf(searchByte); + if (index < 0) + { + return this.scanLine.Length; + } + + if (index != 0) + { + return offset + index; + } + + searchByte = (byte)~searchSpace[0]; + index = searchSpace.IndexOf(searchByte); + if (index < 0) + { + return this.scanLine.Length; + } + + searchSpace = searchSpace.Slice(index); + offset += index; + index = searchSpace.IndexOf((byte)~searchByte); + if (index < 0) + { + return this.scanLine.Length; + } + + return index + offset; + } + + private int FindB2ForImaginaryWhiteLine() => this.width; + + private int FindB2ForNormalLine(int b1) + { + if (b1 >= this.scanLine.Length) + { + return this.scanLine.Length; + } + + byte searchByte = (byte)~this.scanLine[b1]; + int offset = b1 + 1; + ReadOnlySpan searchSpace = this.scanLine.Slice(offset); + int index = searchSpace.IndexOf(searchByte); + if (index == -1) + { + return this.scanLine.Length; + } + + return offset + index; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCode.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCode.cs new file mode 100644 index 000000000..74a17b907 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCode.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + [DebuggerDisplay("Type = {Type}")] + internal readonly struct CcittTwoDimensionalCode + { + private readonly ushort value; + + /// + /// Initializes a new instance of the struct. + /// + /// The type. + /// The bits required. + /// The extension bits. + public CcittTwoDimensionalCode(CcittTwoDimensionalCodeType type, int bitsRequired, int extensionBits = 0) + => this.value = (ushort)((byte)type | ((bitsRequired & 0b1111) << 8) | ((extensionBits & 0b111) << 11)); + + /// + /// Gets the code type. + /// + public CcittTwoDimensionalCodeType Type => (CcittTwoDimensionalCodeType)(this.value & 0b11111111); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCodeType.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCodeType.cs new file mode 100644 index 000000000..0bd04b4cd --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/CcittTwoDimensionalCodeType.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + internal enum CcittTwoDimensionalCodeType + { + None = 0, + + Pass = 1, + + Horizontal = 2, + + Vertical0 = 3, + + VerticalR1 = 4, + + VerticalR2 = 5, + + VerticalR3 = 6, + + VerticalL1 = 7, + + VerticalL2 = 8, + + VerticalL3 = 9, + + Extensions1D = 10, + + Extensions2D = 11, + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs index bb57853d5..917f83585 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors } /// - protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) { long pos = stream.Position; using (var deframeStream = new ZlibInflateStream( diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs index 2e8939607..77d7b765b 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors } /// - protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) { var decoder = new TiffLzwDecoder(stream); decoder.DecodePixels(buffer); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanBitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanBitReader.cs new file mode 100644 index 000000000..90ec0d6ca --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanBitReader.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Bit reader for data encoded with the modified huffman rle method. + /// See TIFF 6.0 specification, section 10. + /// + internal class ModifiedHuffmanBitReader : T4BitReader + { + /// + /// Initializes a new instance of the class. + /// + /// The compressed input stream. + /// The logical order of bits within a byte. + /// The number of bytes to read from the stream. + /// The memory allocator. + public ModifiedHuffmanBitReader(Stream input, TiffFillOrder fillOrder, int bytesToRead, MemoryAllocator allocator) + : base(input, fillOrder, bytesToRead, allocator) + { + } + + /// + public override bool HasMoreData => this.Position < (ulong)this.DataLength - 1 || (this.BitsRead > 0 && this.BitsRead < 7); + + /// + public override bool IsEndOfScanLine + { + get + { + if (this.IsWhiteRun && this.CurValueBitsRead == 12 && this.Value == 1) + { + return true; + } + + if (this.CurValueBitsRead == 11 && this.Value == 0) + { + // black run. + return true; + } + + return false; + } + } + + /// + public override void StartNewRow() + { + base.StartNewRow(); + + int pad = 8 - (this.BitsRead % 8); + if (pad != 8) + { + // Skip padding bits, move to next byte. + this.Position++; + this.ResetBitsRead(); + } + } + + /// + /// No EOL is expected at the start of a run for the modified huffman encoding. + /// + protected override void ReadEolBeforeFirstData() + { + // Nothing to do here. + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs index 9b12dc90f..65feaa427 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs @@ -35,9 +35,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors } /// - protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) { - using var bitReader = new T4BitReader(stream, this.FillOrder, byteCount, this.Allocator, eolPadding: false, isModifiedHuffman: true); + using var bitReader = new ModifiedHuffmanBitReader(stream, this.FillOrder, byteCount, this.Allocator); buffer.Clear(); uint bitsWritten = 0; @@ -51,20 +51,20 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors if (bitReader.IsWhiteRun) { BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue); - bitsWritten += bitReader.RunLength; - pixelsWritten += bitReader.RunLength; } else { BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue); - bitsWritten += bitReader.RunLength; - pixelsWritten += bitReader.RunLength; } + + bitsWritten += bitReader.RunLength; + pixelsWritten += bitReader.RunLength; } - if (pixelsWritten % this.Width == 0) + if (pixelsWritten == this.Width) { bitReader.StartNewRow(); + pixelsWritten = 0; // Write padding bits, if necessary. uint pad = 8 - (bitsWritten % 8); @@ -74,6 +74,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors bitsWritten += pad; } } + + if (pixelsWritten > this.Width) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error, decoded more pixels then image width"); + } } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs index 58a1c9878..c4a952430 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs @@ -25,7 +25,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors } /// - protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) => _ = stream.Read(buffer, 0, Math.Min(buffer.Length, byteCount)); + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) + => _ = stream.Read(buffer, 0, Math.Min(buffer.Length, byteCount)); /// protected override void Dispose(bool disposing) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs index e14736b73..8a001f571 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors } /// - protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) { if (this.compressedDataMemory == null) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs index 384be1cf2..5e83abf8f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs @@ -16,31 +16,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// internal class T4BitReader : IDisposable { - /// - /// Number of bits read. - /// - private int bitsRead; - /// /// The logical order of bits within a byte. /// private readonly TiffFillOrder fillOrder; - /// - /// Current value. - /// - private uint value; - - /// - /// Number of bits read for the current run value. - /// - private int curValueBitsRead; - - /// - /// Byte position in the buffer. - /// - private ulong position; - /// /// Indicates whether its the first line of data which is read from the image. /// @@ -57,20 +37,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// private bool isStartOfRow; - /// - /// Indicates whether the modified huffman compression, as specified in the TIFF spec in section 10, is used. - /// - private readonly bool isModifiedHuffmanRle; - /// /// Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false. /// private readonly bool eolPadding; - private readonly int dataLength; - + /// + /// The minimum code length in bits. + /// private const int MinCodeLength = 2; + /// + /// The maximum code length in bits. + /// private readonly int maxCodeLength = 13; private static readonly Dictionary WhiteLen4TermCodes = new Dictionary() @@ -231,19 +210,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// The number of bytes to read from the stream. /// The memory allocator. /// Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false. - /// Indicates, if its the modified huffman code variation. Defaults to false. - public T4BitReader(Stream input, TiffFillOrder fillOrder, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false, bool isModifiedHuffman = false) + public T4BitReader(Stream input, TiffFillOrder fillOrder, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false) { this.fillOrder = fillOrder; this.Data = allocator.Allocate(bytesToRead); this.ReadImageDataFromStream(input, bytesToRead); - this.isModifiedHuffmanRle = isModifiedHuffman; - this.dataLength = bytesToRead; - this.bitsRead = 0; - this.value = 0; - this.curValueBitsRead = 0; - this.position = 0; + this.DataLength = bytesToRead; + this.BitsRead = 0; + this.Value = 0; + this.CurValueBitsRead = 0; + this.Position = 0; this.IsWhiteRun = true; this.isFirstScanLine = true; this.isStartOfRow = true; @@ -257,6 +234,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors } } + /// + /// Gets the current value. + /// + protected uint Value { get; private set; } + + /// + /// Gets the number of bits read for the current run value. + /// + protected int CurValueBitsRead { get; private set; } + + /// + /// Gets the number of bits read. + /// + protected int BitsRead { get; private set; } + + /// + /// Gets the available data in bytes. + /// + protected int DataLength { get; } + + /// + /// Gets or sets the byte position in the buffer. + /// + protected ulong Position { get; set; } + /// /// Gets the compressed image data. /// @@ -265,23 +267,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// /// Gets a value indicating whether there is more data to read left. /// - public bool HasMoreData - { - get - { - if (this.isModifiedHuffmanRle) - { - return this.position < (ulong)this.dataLength - 1 || (this.bitsRead > 0 && this.bitsRead < 7); - } - - return this.position < (ulong)this.dataLength - 1; - } - } + public virtual bool HasMoreData => this.Position < (ulong)this.DataLength - 1; /// - /// Gets a value indicating whether the current run is a white pixel run, otherwise its a black pixel run. + /// Gets or sets a value indicating whether the current run is a white pixel run, otherwise its a black pixel run. /// - public bool IsWhiteRun { get; private set; } + public bool IsWhiteRun { get; protected set; } /// /// Gets the number of pixels in the current run. @@ -291,16 +282,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// /// Gets a value indicating whether the end of a pixel row has been reached. /// - public bool IsEndOfScanLine + public virtual bool IsEndOfScanLine { get { if (this.eolPadding) { - return this.curValueBitsRead >= 12 && this.value == 1; + return this.CurValueBitsRead >= 12 && this.Value == 1; } - return this.curValueBitsRead == 12 && this.value == 1; + return this.CurValueBitsRead == 12 && this.Value == 1; } } @@ -315,29 +306,20 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors this.terminationCodeFound = false; } + // Initialize for next run. this.Reset(); - if (this.isFirstScanLine && !this.isModifiedHuffmanRle) - { - // We expect an EOL before the first data. - this.value = this.ReadValue(this.eolPadding ? 16 : 12); - - if (!this.IsEndOfScanLine) - { - TiffThrowHelper.ThrowImageFormatException("t4 parsing error: expected start of data marker not found"); - } - - this.Reset(); - } + // We expect an EOL before the first data. + this.ReadEolBeforeFirstData(); // A code word must have at least 2 bits. - this.value = this.ReadValue(MinCodeLength); + this.Value = this.ReadValue(MinCodeLength); do { - if (this.curValueBitsRead > this.maxCodeLength) + if (this.CurValueBitsRead > this.maxCodeLength) { - TiffThrowHelper.ThrowImageFormatException("t4 parsing error: invalid code length read"); + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: invalid code length read"); } bool isMakeupCode = this.IsMakeupCode(); @@ -363,10 +345,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors // Each line starts with a white run. If the image starts with black, a white run with length zero is written. if (this.isStartOfRow && this.IsWhiteRun && this.WhiteTerminatingCodeRunLength() == 0) { - this.IsWhiteRun = !this.IsWhiteRun; this.Reset(); this.isStartOfRow = false; - continue; + this.terminationCodeFound = true; + this.RunLength = 0; + break; } if (this.IsWhiteRun) @@ -384,7 +367,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors } uint currBit = this.ReadValue(1); - this.value = (this.value << 1) | currBit; + this.Value = (this.Value << 1) | currBit; if (this.IsEndOfScanLine) { @@ -396,55 +379,106 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors this.isFirstScanLine = false; } - public void StartNewRow() + /// + /// Initialization for a new row. + /// + public virtual void StartNewRow() { // Each new row starts with a white run. this.IsWhiteRun = true; this.isStartOfRow = true; this.terminationCodeFound = false; + } - if (this.isModifiedHuffmanRle) + /// + public void Dispose() => this.Data.Dispose(); + + /// + /// An EOL is expected before the first data. + /// + protected virtual void ReadEolBeforeFirstData() + { + if (this.isFirstScanLine) { - int pad = 8 - (this.bitsRead % 8); - if (pad != 8) + this.Value = this.ReadValue(this.eolPadding ? 16 : 12); + + if (!this.IsEndOfScanLine) { - // Skip padding bits, move to next byte. - this.position++; - this.bitsRead = 0; + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: expected start of data marker not found"); } + + this.Reset(); } } - /// - public void Dispose() => this.Data.Dispose(); + /// + /// Resets the current value read and the number of bits read. + /// + /// if set to true resets also the run length. + protected void Reset(bool resetRunLength = true) + { + this.Value = 0; + this.CurValueBitsRead = 0; + + if (resetRunLength) + { + this.RunLength = 0; + } + } + + /// + /// Resets the bits read to 0. + /// + protected void ResetBitsRead() => this.BitsRead = 0; + + /// + /// Reads the next value. + /// + /// The number of bits to read. + /// The value read. + protected uint ReadValue(int nBits) + { + Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + + uint v = 0; + int shift = nBits; + while (shift-- > 0) + { + uint bit = this.GetBit(); + v |= bit << shift; + this.CurValueBitsRead++; + } + + return v; + } private uint WhiteTerminatingCodeRunLength() { - switch (this.curValueBitsRead) + switch (this.CurValueBitsRead) { case 4: { - return WhiteLen4TermCodes[this.value]; + return WhiteLen4TermCodes[this.Value]; } case 5: { - return WhiteLen5TermCodes[this.value]; + return WhiteLen5TermCodes[this.Value]; } case 6: { - return WhiteLen6TermCodes[this.value]; + return WhiteLen6TermCodes[this.Value]; } case 7: { - return WhiteLen7TermCodes[this.value]; + return WhiteLen7TermCodes[this.Value]; } case 8: { - return WhiteLen8TermCodes[this.value]; + return WhiteLen8TermCodes[this.Value]; } } @@ -453,61 +487,61 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors private uint BlackTerminatingCodeRunLength() { - switch (this.curValueBitsRead) + switch (this.CurValueBitsRead) { case 2: { - return BlackLen2TermCodes[this.value]; + return BlackLen2TermCodes[this.Value]; } case 3: { - return BlackLen3TermCodes[this.value]; + return BlackLen3TermCodes[this.Value]; } case 4: { - return BlackLen4TermCodes[this.value]; + return BlackLen4TermCodes[this.Value]; } case 5: { - return BlackLen5TermCodes[this.value]; + return BlackLen5TermCodes[this.Value]; } case 6: { - return BlackLen6TermCodes[this.value]; + return BlackLen6TermCodes[this.Value]; } case 7: { - return BlackLen7TermCodes[this.value]; + return BlackLen7TermCodes[this.Value]; } case 8: { - return BlackLen8TermCodes[this.value]; + return BlackLen8TermCodes[this.Value]; } case 9: { - return BlackLen9TermCodes[this.value]; + return BlackLen9TermCodes[this.Value]; } case 10: { - return BlackLen10TermCodes[this.value]; + return BlackLen10TermCodes[this.Value]; } case 11: { - return BlackLen11TermCodes[this.value]; + return BlackLen11TermCodes[this.Value]; } case 12: { - return BlackLen12TermCodes[this.value]; + return BlackLen12TermCodes[this.Value]; } } @@ -516,41 +550,41 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors private uint WhiteMakeupCodeRunLength() { - switch (this.curValueBitsRead) + switch (this.CurValueBitsRead) { case 5: { - return WhiteLen5MakeupCodes[this.value]; + return WhiteLen5MakeupCodes[this.Value]; } case 6: { - return WhiteLen6MakeupCodes[this.value]; + return WhiteLen6MakeupCodes[this.Value]; } case 7: { - return WhiteLen7MakeupCodes[this.value]; + return WhiteLen7MakeupCodes[this.Value]; } case 8: { - return WhiteLen8MakeupCodes[this.value]; + return WhiteLen8MakeupCodes[this.Value]; } case 9: { - return WhiteLen9MakeupCodes[this.value]; + return WhiteLen9MakeupCodes[this.Value]; } case 11: { - return WhiteLen11MakeupCodes[this.value]; + return WhiteLen11MakeupCodes[this.Value]; } case 12: { - return WhiteLen12MakeupCodes[this.value]; + return WhiteLen12MakeupCodes[this.Value]; } } @@ -559,26 +593,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors private uint BlackMakeupCodeRunLength() { - switch (this.curValueBitsRead) + switch (this.CurValueBitsRead) { case 10: { - return BlackLen10MakeupCodes[this.value]; + return BlackLen10MakeupCodes[this.Value]; } case 11: { - return BlackLen11MakeupCodes[this.value]; + return BlackLen11MakeupCodes[this.Value]; } case 12: { - return BlackLen12MakeupCodes[this.value]; + return BlackLen12MakeupCodes[this.Value]; } case 13: { - return BlackLen13MakeupCodes[this.value]; + return BlackLen13MakeupCodes[this.Value]; } } @@ -597,49 +631,41 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors private bool IsWhiteMakeupCode() { - switch (this.curValueBitsRead) + switch (this.CurValueBitsRead) { case 5: { - return WhiteLen5MakeupCodes.ContainsKey(this.value); + return WhiteLen5MakeupCodes.ContainsKey(this.Value); } case 6: { - return WhiteLen6MakeupCodes.ContainsKey(this.value); + return WhiteLen6MakeupCodes.ContainsKey(this.Value); } case 7: { - return WhiteLen7MakeupCodes.ContainsKey(this.value); + return WhiteLen7MakeupCodes.ContainsKey(this.Value); } case 8: { - return WhiteLen8MakeupCodes.ContainsKey(this.value); + return WhiteLen8MakeupCodes.ContainsKey(this.Value); } case 9: { - return WhiteLen9MakeupCodes.ContainsKey(this.value); + return WhiteLen9MakeupCodes.ContainsKey(this.Value); } case 11: { - return WhiteLen11MakeupCodes.ContainsKey(this.value); + return WhiteLen11MakeupCodes.ContainsKey(this.Value); } case 12: { - if (this.isModifiedHuffmanRle) - { - if (this.value == 1) - { - return true; - } - } - - return WhiteLen12MakeupCodes.ContainsKey(this.value); + return WhiteLen12MakeupCodes.ContainsKey(this.Value); } } @@ -648,34 +674,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors private bool IsBlackMakeupCode() { - switch (this.curValueBitsRead) + switch (this.CurValueBitsRead) { case 10: { - return BlackLen10MakeupCodes.ContainsKey(this.value); + return BlackLen10MakeupCodes.ContainsKey(this.Value); } case 11: { - if (this.isModifiedHuffmanRle) - { - if (this.value == 0) - { - return true; - } - } - - return BlackLen11MakeupCodes.ContainsKey(this.value); + return BlackLen11MakeupCodes.ContainsKey(this.Value); } case 12: { - return BlackLen12MakeupCodes.ContainsKey(this.value); + return BlackLen12MakeupCodes.ContainsKey(this.Value); } case 13: { - return BlackLen13MakeupCodes.ContainsKey(this.value); + return BlackLen13MakeupCodes.ContainsKey(this.Value); } } @@ -694,31 +712,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors private bool IsWhiteTerminatingCode() { - switch (this.curValueBitsRead) + switch (this.CurValueBitsRead) { case 4: { - return WhiteLen4TermCodes.ContainsKey(this.value); + return WhiteLen4TermCodes.ContainsKey(this.Value); } case 5: { - return WhiteLen5TermCodes.ContainsKey(this.value); + return WhiteLen5TermCodes.ContainsKey(this.Value); } case 6: { - return WhiteLen6TermCodes.ContainsKey(this.value); + return WhiteLen6TermCodes.ContainsKey(this.Value); } case 7: { - return WhiteLen7TermCodes.ContainsKey(this.value); + return WhiteLen7TermCodes.ContainsKey(this.Value); } case 8: { - return WhiteLen8TermCodes.ContainsKey(this.value); + return WhiteLen8TermCodes.ContainsKey(this.Value); } } @@ -727,117 +745,90 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors private bool IsBlackTerminatingCode() { - switch (this.curValueBitsRead) + switch (this.CurValueBitsRead) { case 2: { - return BlackLen2TermCodes.ContainsKey(this.value); + return BlackLen2TermCodes.ContainsKey(this.Value); } case 3: { - return BlackLen3TermCodes.ContainsKey(this.value); + return BlackLen3TermCodes.ContainsKey(this.Value); } case 4: { - return BlackLen4TermCodes.ContainsKey(this.value); + return BlackLen4TermCodes.ContainsKey(this.Value); } case 5: { - return BlackLen5TermCodes.ContainsKey(this.value); + return BlackLen5TermCodes.ContainsKey(this.Value); } case 6: { - return BlackLen6TermCodes.ContainsKey(this.value); + return BlackLen6TermCodes.ContainsKey(this.Value); } case 7: { - return BlackLen7TermCodes.ContainsKey(this.value); + return BlackLen7TermCodes.ContainsKey(this.Value); } case 8: { - return BlackLen8TermCodes.ContainsKey(this.value); + return BlackLen8TermCodes.ContainsKey(this.Value); } case 9: { - return BlackLen9TermCodes.ContainsKey(this.value); + return BlackLen9TermCodes.ContainsKey(this.Value); } case 10: { - return BlackLen10TermCodes.ContainsKey(this.value); + return BlackLen10TermCodes.ContainsKey(this.Value); } case 11: { - return BlackLen11TermCodes.ContainsKey(this.value); + return BlackLen11TermCodes.ContainsKey(this.Value); } case 12: { - return BlackLen12TermCodes.ContainsKey(this.value); + return BlackLen12TermCodes.ContainsKey(this.Value); } } return false; } - private void Reset(bool resetRunLength = true) - { - this.value = 0; - this.curValueBitsRead = 0; - - if (resetRunLength) - { - this.RunLength = 0; - } - } - - private uint ReadValue(int nBits) - { - Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); - - uint v = 0; - int shift = nBits; - while (shift-- > 0) - { - uint bit = this.GetBit(); - v |= bit << shift; - this.curValueBitsRead++; - } - - return v; - } - private uint GetBit() { - if (this.bitsRead >= 8) + if (this.BitsRead >= 8) { this.LoadNewByte(); } Span dataSpan = this.Data.GetSpan(); - int shift = 8 - this.bitsRead - 1; - uint bit = (uint)((dataSpan[(int)this.position] & (1 << shift)) != 0 ? 1 : 0); - this.bitsRead++; + int shift = 8 - this.BitsRead - 1; + uint bit = (uint)((dataSpan[(int)this.Position] & (1 << shift)) != 0 ? 1 : 0); + this.BitsRead++; return bit; } private void LoadNewByte() { - this.position++; - this.bitsRead = 0; + this.Position++; + this.ResetBitsRead(); - if (this.position >= (ulong)this.dataLength) + if (this.Position >= (ulong)this.DataLength) { - TiffThrowHelper.ThrowImageFormatException("tiff image has invalid t4 compressed data"); + TiffThrowHelper.ThrowImageFormatException("tiff image has invalid ccitt compressed data"); } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs index a79ef3fe5..e424d5290 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs @@ -31,7 +31,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors /// The number of bits per pixel. /// Fax compression options. /// The photometric interpretation. - public T4TiffCompression(MemoryAllocator allocator, TiffFillOrder fillOrder, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation) + public T4TiffCompression( + MemoryAllocator allocator, + TiffFillOrder fillOrder, + int width, + int bitsPerPixel, + FaxCompressionOptions faxOptions, + TiffPhotometricInterpretation photometricInterpretation) : base(allocator, width, bitsPerPixel) { this.faxCompressionOptions = faxOptions; @@ -48,7 +54,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors protected TiffFillOrder FillOrder { get; } /// - protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) { if (this.faxCompressionOptions.HasFlag(FaxCompressionOptions.TwoDimensionalCoding)) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs new file mode 100644 index 000000000..0ebaccf7e --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs @@ -0,0 +1,160 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.IO; + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Bit reader for reading CCITT T6 compressed fax data. + /// See: Facsimile Coding Schemes and Coding Control Functions for Group 4 Facsimile Apparatus, itu-t recommendation t.6 + /// + internal class T6BitReader : T4BitReader + { + private readonly int maxCodeLength = 12; + + private static readonly CcittTwoDimensionalCode None = new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.None, 0); + + private static readonly Dictionary Len1Codes = new Dictionary() + { + { 0b1, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.Vertical0, 1) } + }; + + private static readonly Dictionary Len3Codes = new Dictionary() + { + { 0b001, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.Horizontal, 3) }, + { 0b010, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.VerticalL1, 3) }, + { 0b011, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.VerticalR1, 3) } + }; + + private static readonly Dictionary Len4Codes = new Dictionary() + { + { 0b0001, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.Pass, 4) } + }; + + private static readonly Dictionary Len6Codes = new Dictionary() + { + { 0b000011, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.VerticalR2, 6) }, + { 0b000010, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.VerticalL2, 6) } + }; + + private static readonly Dictionary Len7Codes = new Dictionary() + { + { 0b0000011, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.VerticalR3, 7) }, + { 0b0000010, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.VerticalL3, 7) }, + { 0b0000001, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.Extensions2D, 7) }, + { 0b0000000, new CcittTwoDimensionalCode(CcittTwoDimensionalCodeType.Extensions1D, 7) } + }; + + /// + /// Initializes a new instance of the class. + /// + /// The compressed input stream. + /// The logical order of bits within a byte. + /// The number of bytes to read from the stream. + /// The memory allocator. + public T6BitReader(Stream input, TiffFillOrder fillOrder, int bytesToRead, MemoryAllocator allocator) + : base(input, fillOrder, bytesToRead, allocator) + { + } + + /// + public override bool HasMoreData => this.Position < (ulong)this.DataLength - 1 || (this.BitsRead > 0 && this.BitsRead < 7); + + /// + /// Gets or sets the two dimensional code. + /// + public CcittTwoDimensionalCode Code { get; internal set; } + + public bool ReadNextCodeWord() + { + this.Code = None; + this.Reset(); + uint value = this.ReadValue(1); + + do + { + if (this.CurValueBitsRead > this.maxCodeLength) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: invalid code length read"); + } + + switch (this.CurValueBitsRead) + { + case 1: + if (Len1Codes.ContainsKey(value)) + { + this.Code = Len1Codes[value]; + return false; + } + + break; + + case 3: + if (Len3Codes.ContainsKey(value)) + { + this.Code = Len3Codes[value]; + return false; + } + + break; + + case 4: + if (Len4Codes.ContainsKey(value)) + { + this.Code = Len4Codes[value]; + return false; + } + + break; + + case 6: + if (Len6Codes.ContainsKey(value)) + { + this.Code = Len6Codes[value]; + return false; + } + + break; + + case 7: + if (Len7Codes.ContainsKey(value)) + { + this.Code = Len7Codes[value]; + return false; + } + + break; + } + + uint currBit = this.ReadValue(1); + value = (value << 1) | currBit; + } + while (!this.IsEndOfScanLine); + + if (this.IsEndOfScanLine) + { + return true; + } + + return false; + } + + /// + /// No EOL is expected at the start of a run. + /// + protected override void ReadEolBeforeFirstData() + { + // Nothing to do here. + } + + /// + /// Swaps the white run to black run an vise versa. + /// + public void SwapColor() => this.IsWhiteRun = !this.IsWhiteRun; + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs new file mode 100644 index 000000000..87095d5ee --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs @@ -0,0 +1,253 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is compressed using CCITT T6 compression. + /// + internal class T6TiffCompression : TiffBaseDecompressor + { + private readonly bool isWhiteZero; + + private readonly byte whiteValue; + + private readonly byte blackValue; + + private readonly int width; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The logical order of bits within a byte. + /// The image width. + /// The number of bits per pixel. + /// The photometric interpretation. + public T6TiffCompression( + MemoryAllocator allocator, + TiffFillOrder fillOrder, + int width, + int bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation) + : base(allocator, width, bitsPerPixel) + { + this.FillOrder = fillOrder; + this.width = width; + this.isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; + this.whiteValue = (byte)(this.isWhiteZero ? 0 : 1); + this.blackValue = (byte)(this.isWhiteZero ? 1 : 0); + } + + /// + /// Gets the logical order of bits within a byte. + /// + private TiffFillOrder FillOrder { get; } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) + { + int height = stripHeight; + + using System.Buffers.IMemoryOwner scanLineBuffer = this.Allocator.Allocate(this.width * 2); + Span scanLine = scanLineBuffer.GetSpan().Slice(0, this.width); + Span referenceScanLineSpan = scanLineBuffer.GetSpan().Slice(this.width, this.width); + + using var bitReader = new T6BitReader(stream, this.FillOrder, byteCount, this.Allocator); + + var referenceScanLine = new CcittReferenceScanline(this.isWhiteZero, this.width); + uint bitsWritten = 0; + for (int y = 0; y < height; y++) + { + scanLine.Fill(0); + Decode2DScanline(bitReader, this.isWhiteZero, referenceScanLine, scanLine); + + bitsWritten = this.WriteScanLine(buffer, scanLine, bitsWritten); + + scanLine.CopyTo(referenceScanLineSpan); + referenceScanLine = new CcittReferenceScanline(this.isWhiteZero, referenceScanLineSpan); + } + } + + private uint WriteScanLine(Span buffer, Span scanLine, uint bitsWritten) + { + byte white = (byte)(this.isWhiteZero ? 0 : 255); + for (int i = 0; i < scanLine.Length; i++) + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, 1, scanLine[i] == white ? this.whiteValue : this.blackValue); + bitsWritten++; + } + + // Write padding bytes, if necessary. + uint pad = 8 - (bitsWritten % 8); + if (pad != 8) + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0); + bitsWritten += pad; + } + + return bitsWritten; + } + + private static void Decode2DScanline(T6BitReader bitReader, bool whiteIsZero, CcittReferenceScanline referenceScanline, Span scanline) + { + int width = scanline.Length; + bitReader.StartNewRow(); + + // 2D Encoding variables. + int a0 = -1; + byte fillByte = whiteIsZero ? (byte)0 : (byte)255; + + // Process every code word in this scanline. + int unpacked = 0; + while (true) + { + // Read next code word and advance pass it. + bool isEol = bitReader.ReadNextCodeWord(); + + // Special case handling for EOL. + if (isEol) + { + // If a TIFF reader encounters EOFB before the expected number of lines has been extracted, + // it is appropriate to assume that the missing rows consist entirely of white pixels. + scanline.Fill(whiteIsZero ? (byte)0 : (byte)255); + break; + } + + // Update 2D Encoding variables. + int b1 = referenceScanline.FindB1(a0, fillByte); + + // Switch on the code word. + int a1; + switch (bitReader.Code.Type) + { + case CcittTwoDimensionalCodeType.None: + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error, could not read a valid code word."); + break; + + case CcittTwoDimensionalCodeType.Pass: + int b2 = referenceScanline.FindB2(b1); + scanline.Slice(unpacked, b2 - unpacked).Fill(fillByte); + unpacked = b2; + a0 = b2; + break; + case CcittTwoDimensionalCodeType.Horizontal: + // Decode M(a0a1) + bitReader.ReadNextRun(); + int runLength = (int)bitReader.RunLength; + if (runLength > (uint)(scanline.Length - unpacked)) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error"); + } + + scanline.Slice(unpacked, runLength).Fill(fillByte); + unpacked += runLength; + fillByte = (byte)~fillByte; + + // Decode M(a1a2) + bitReader.ReadNextRun(); + runLength = (int)bitReader.RunLength; + if (runLength > (uint)(scanline.Length - unpacked)) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error"); + } + + scanline.Slice(unpacked, runLength).Fill(fillByte); + unpacked += runLength; + fillByte = (byte)~fillByte; + + // Prepare next a0 + a0 = unpacked; + break; + + case CcittTwoDimensionalCodeType.Vertical0: + a1 = b1; + scanline.Slice(unpacked, a1 - unpacked).Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalR1: + a1 = b1 + 1; + scanline.Slice(unpacked, a1 - unpacked).Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalR2: + a1 = b1 + 2; + scanline.Slice(unpacked, a1 - unpacked).Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalR3: + a1 = b1 + 3; + scanline.Slice(unpacked, a1 - unpacked).Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalL1: + a1 = b1 - 1; + scanline.Slice(unpacked, a1 - unpacked).Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalL2: + a1 = b1 - 2; + scanline.Slice(unpacked, a1 - unpacked).Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + case CcittTwoDimensionalCodeType.VerticalL3: + a1 = b1 - 3; + scanline.Slice(unpacked, a1 - unpacked).Fill(fillByte); + unpacked = a1; + a0 = a1; + fillByte = (byte)~fillByte; + bitReader.SwapColor(); + break; + + default: + throw new NotSupportedException("ccitt extensions are not supported."); + } + + // This line is fully unpacked. Should exit and process next line. + if (unpacked == width) + { + break; + } + + if (unpacked > width) + { + TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error, unpacked data > width"); + } + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs index a289e306a..28459d0c5 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs @@ -15,8 +15,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// internal abstract class TiffBaseDecompressor : TiffBaseCompression { - protected TiffBaseDecompressor(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) - : base(allocator, width, bitsPerPixel, predictor) + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + /// The predictor. + protected TiffBaseDecompressor(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(memoryAllocator, width, bitsPerPixel, predictor) { } @@ -26,8 +33,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// The to read image data from. /// The strip offset of stream. /// The number of bytes to read from the input stream. + /// The height of the strip. /// The output buffer for uncompressed data. - public void Decompress(BufferedReadStream stream, uint stripOffset, uint stripByteCount, Span buffer) + public void Decompress(BufferedReadStream stream, uint stripOffset, uint stripByteCount, int stripHeight, Span buffer) { if (stripByteCount > int.MaxValue) { @@ -35,7 +43,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } stream.Seek(stripOffset, SeekOrigin.Begin); - this.Decompress(stream, (int)stripByteCount, buffer); + this.Decompress(stream, (int)stripByteCount, stripHeight, buffer); if (stripOffset + stripByteCount < stream.Position) { @@ -48,7 +56,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// /// The to read image data from. /// The number of bytes to read from the input stream. + /// The height of the strip. /// The output buffer for uncompressed data. - protected abstract void Decompress(BufferedReadStream stream, int byteCount, Span buffer); + protected abstract void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer); } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs index 80bc0af5a..61b691eb8 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs @@ -29,10 +29,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression Lzw = 3, /// - /// Image data is compressed using T4-encoding: CCITT T.4. + /// Image data is compressed using CCITT T.4 fax compression. /// T4 = 4, + /// + /// Image data is compressed using CCITT T.6 fax compression. + /// + T6 = 6, + /// /// Image data is compressed using modified huffman compression. /// diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index b1562223a..735ea1aa2 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -46,6 +46,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); return new T4TiffCompression(allocator, fillOrder, width, bitsPerPixel, faxOptions, photometricInterpretation); + case TiffDecoderCompressionType.T6: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T6TiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); + case TiffDecoderCompressionType.HuffmanRle: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index ab3394c56..beac42db7 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -45,7 +45,7 @@ |Ccitt1D | Y | Y | | |PackBits | Y | Y | | |CcittGroup3Fax | Y | Y | | -|CcittGroup4Fax | | | | +|CcittGroup4Fax | | 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) | | | | diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 28afe4c6f..ff5f8923e 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -317,7 +317,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff int stripIndex = i; for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) { - decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffers[planeIndex].GetSpan()); + decompressor.Decompress( + this.inputStream, + (uint)stripOffsets[stripIndex], + (uint)stripByteCounts[stripIndex], + stripHeight, + stripBuffers[planeIndex].GetSpan()); stripIndex += stripsPerPlane; } @@ -385,7 +390,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } - decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBufferSpan); + decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripHeight, stripBufferSpan); colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index bb435affc..d8357d945 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -418,6 +418,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } + case TiffCompression.CcittGroup4Fax: + { + options.CompressionType = TiffDecoderCompressionType.T6; + options.FaxCompressionOptions = exifProfile.GetValue(ExifTag.T4Options) != null ? (FaxCompressionOptions)exifProfile.GetValue(ExifTag.T4Options).Value : FaxCompressionOptions.None; + + break; + } + case TiffCompression.Ccitt1D: { options.CompressionType = TiffDecoderCompressionType.HuffmanRle; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs index c93a2018d..ff7025b50 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression using var decompressor = new DeflateTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffColorType.BlackIsZero8, TiffPredictor.None, false); - decompressor.Decompress(stream, 0, (uint)stream.Length, buffer); + decompressor.Decompress(stream, 0, (uint)stream.Length, 1, buffer); Assert.Equal(data, buffer); } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs index 5ea75d9a8..08705738f 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs @@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression var buffer = new byte[data.Length]; using var decompressor = new LzwTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffColorType.BlackIsZero8, TiffPredictor.None, false); - decompressor.Decompress(stream, 0, (uint)stream.Length, buffer); + decompressor.Decompress(stream, 0, (uint)stream.Length, 1, buffer); Assert.Equal(data, buffer); } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs index 82ecb315b..d153e1ed2 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs @@ -17,10 +17,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression [InlineData(new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 }, 5, new byte[] { 10, 15, 20, 25, 30 })] public void Decompress_ReadsData(byte[] inputData, uint byteCount, byte[] expectedResult) { - var stream = new BufferedReadStream(Configuration.Default, new MemoryStream(inputData)); - var buffer = new byte[expectedResult.Length]; + using var memoryStream = new MemoryStream(inputData); + using var stream = new BufferedReadStream(Configuration.Default, memoryStream); + byte[] buffer = new byte[expectedResult.Length]; - new NoneTiffCompression(default, default, default).Decompress(stream, 0, byteCount, buffer); + using var decompressor = new NoneTiffCompression(default, default, default); + decompressor.Decompress(stream, 0, byteCount, 1, buffer); Assert.Equal(expectedResult, buffer); } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs index b67cb8325..b56c1e7c9 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs @@ -26,11 +26,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression [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) { - var stream = new BufferedReadStream(Configuration.Default, new MemoryStream(inputData)); - var buffer = new byte[expectedResult.Length]; + using var memoryStream = new MemoryStream(inputData); + using var stream = new BufferedReadStream(Configuration.Default, memoryStream); + byte[] buffer = new byte[expectedResult.Length]; using var decompressor = new PackBitsTiffCompression(new ArrayPoolMemoryAllocator(), default, default); - decompressor.Decompress(stream, 0, (uint)inputData.Length, buffer); + decompressor.Decompress(stream, 0, (uint)inputData.Length, 1, buffer); Assert.Equal(expectedResult, buffer); } @@ -41,7 +42,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression { // arrange Span input = inputData.AsSpan(); - var compressed = new byte[expectedResult.Length]; + byte[] compressed = new byte[expectedResult.Length]; // act PackBitsWriter.PackBits(input, compressed);