diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs index 30da537eb..d038e9c8b 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs @@ -2,8 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; -using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Memory; @@ -13,222 +11,35 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors /// /// Bitwriter for writing compressed CCITT T4 1D data. /// - internal sealed class T4BitCompressor : TiffBaseCompressor + internal sealed class T4BitCompressor : TiffCcittCompressor { - private const uint WhiteZeroRunTermCode = 0x35; - - private const uint BlackZeroRunTermCode = 0x37; - - private static readonly uint[] MakeupRunLength = - { - 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792, 1856, 1920, 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560 - }; - - private static readonly Dictionary WhiteLen4TermCodes = new Dictionary() - { - { 2, 0x7 }, { 3, 0x8 }, { 4, 0xB }, { 5, 0xC }, { 6, 0xE }, { 7, 0xF } - }; - - private static readonly Dictionary WhiteLen5TermCodes = new Dictionary() - { - { 8, 0x13 }, { 9, 0x14 }, { 10, 0x7 }, { 11, 0x8 } - }; - - private static readonly Dictionary WhiteLen6TermCodes = new Dictionary() - { - { 1, 0x7 }, { 12, 0x8 }, { 13, 0x3 }, { 14, 0x34 }, { 15, 0x35 }, { 16, 0x2A }, { 17, 0x2B } - }; - - private static readonly Dictionary WhiteLen7TermCodes = new Dictionary() - { - { 18, 0x27 }, { 19, 0xC }, { 20, 0x8 }, { 21, 0x17 }, { 22, 0x3 }, { 23, 0x4 }, { 24, 0x28 }, { 25, 0x2B }, { 26, 0x13 }, - { 27, 0x24 }, { 28, 0x18 } - }; - - private static readonly Dictionary WhiteLen8TermCodes = new Dictionary() - { - { 0, WhiteZeroRunTermCode }, { 29, 0x2 }, { 30, 0x3 }, { 31, 0x1A }, { 32, 0x1B }, { 33, 0x12 }, { 34, 0x13 }, { 35, 0x14 }, - { 36, 0x15 }, { 37, 0x16 }, { 38, 0x17 }, { 39, 0x28 }, { 40, 0x29 }, { 41, 0x2A }, { 42, 0x2B }, { 43, 0x2C }, { 44, 0x2D }, - { 45, 0x4 }, { 46, 0x5 }, { 47, 0xA }, { 48, 0xB }, { 49, 0x52 }, { 50, 0x53 }, { 51, 0x54 }, { 52, 0x55 }, { 53, 0x24 }, - { 54, 0x25 }, { 55, 0x58 }, { 56, 0x59 }, { 57, 0x5A }, { 58, 0x5B }, { 59, 0x4A }, { 60, 0x4B }, { 61, 0x32 }, { 62, 0x33 }, - { 63, 0x34 } - }; - - private static readonly Dictionary BlackLen2TermCodes = new Dictionary() - { - { 2, 0x3 }, { 3, 0x2 } - }; - - private static readonly Dictionary BlackLen3TermCodes = new Dictionary() - { - { 1, 0x2 }, { 4, 0x3 } - }; - - private static readonly Dictionary BlackLen4TermCodes = new Dictionary() - { - { 5, 0x3 }, { 6, 0x2 } - }; - - private static readonly Dictionary BlackLen5TermCodes = new Dictionary() - { - { 7, 0x3 } - }; - - private static readonly Dictionary BlackLen6TermCodes = new Dictionary() - { - { 8, 0x5 }, { 9, 0x4 } - }; - - private static readonly Dictionary BlackLen7TermCodes = new Dictionary() - { - { 10, 0x4 }, { 11, 0x5 }, { 12, 0x7 } - }; - - private static readonly Dictionary BlackLen8TermCodes = new Dictionary() - { - { 13, 0x4 }, { 14, 0x7 } - }; - - private static readonly Dictionary BlackLen9TermCodes = new Dictionary() - { - { 15, 0x18 } - }; - - private static readonly Dictionary BlackLen10TermCodes = new Dictionary() - { - { 0, BlackZeroRunTermCode }, { 16, 0x17 }, { 17, 0x18 }, { 18, 0x8 } - }; - - private static readonly Dictionary BlackLen11TermCodes = new Dictionary() - { - { 19, 0x67 }, { 20, 0x68 }, { 21, 0x6C }, { 22, 0x37 }, { 23, 0x28 }, { 24, 0x17 }, { 25, 0x18 } - }; - - private static readonly Dictionary BlackLen12TermCodes = new Dictionary() - { - { 26, 0xCA }, { 27, 0xCB }, { 28, 0xCC }, { 29, 0xCD }, { 30, 0x68 }, { 31, 0x69 }, { 32, 0x6A }, { 33, 0x6B }, { 34, 0xD2 }, - { 35, 0xD3 }, { 36, 0xD4 }, { 37, 0xD5 }, { 38, 0xD6 }, { 39, 0xD7 }, { 40, 0x6C }, { 41, 0x6D }, { 42, 0xDA }, { 43, 0xDB }, - { 44, 0x54 }, { 45, 0x55 }, { 46, 0x56 }, { 47, 0x57 }, { 48, 0x64 }, { 49, 0x65 }, { 50, 0x52 }, { 51, 0x53 }, { 52, 0x24 }, - { 53, 0x37 }, { 54, 0x38 }, { 55, 0x27 }, { 56, 0x28 }, { 57, 0x58 }, { 58, 0x59 }, { 59, 0x2B }, { 60, 0x2C }, { 61, 0x5A }, - { 62, 0x66 }, { 63, 0x67 } - }; - - private static readonly Dictionary WhiteLen5MakeupCodes = new Dictionary() - { - { 64, 0x1B }, { 128, 0x12 } - }; - - private static readonly Dictionary WhiteLen6MakeupCodes = new Dictionary() - { - { 192, 0x17 }, { 1664, 0x18 } - }; - - private static readonly Dictionary WhiteLen8MakeupCodes = new Dictionary() - { - { 320, 0x36 }, { 384, 0x37 }, { 448, 0x64 }, { 512, 0x65 }, { 576, 0x68 }, { 640, 0x67 } - }; - - private static readonly Dictionary WhiteLen7MakeupCodes = new Dictionary() - { - { 256, 0x37 } - }; - - private static readonly Dictionary WhiteLen9MakeupCodes = new Dictionary() - { - { 704, 0xCC }, { 768, 0xCD }, { 832, 0xD2 }, { 896, 0xD3 }, { 960, 0xD4 }, { 1024, 0xD5 }, { 1088, 0xD6 }, - { 1152, 0xD7 }, { 1216, 0xD8 }, { 1280, 0xD9 }, { 1344, 0xDA }, { 1408, 0xDB }, { 1472, 0x98 }, { 1536, 0x99 }, - { 1600, 0x9A }, { 1728, 0x9B } - }; - - private static readonly Dictionary WhiteLen11MakeupCodes = new Dictionary() - { - { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } - }; - - private static readonly Dictionary WhiteLen12MakeupCodes = new Dictionary() - { - { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, - { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } - }; - - private static readonly Dictionary BlackLen10MakeupCodes = new Dictionary() - { - { 64, 0xF } - }; - - private static readonly Dictionary BlackLen11MakeupCodes = new Dictionary() - { - { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } - }; - - private static readonly Dictionary BlackLen12MakeupCodes = new Dictionary() - { - { 128, 0xC8 }, { 192, 0xC9 }, { 256, 0x5B }, { 320, 0x33 }, { 384, 0x34 }, { 448, 0x35 }, - { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, - { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } - }; - - private static readonly Dictionary BlackLen13MakeupCodes = new Dictionary() - { - { 512, 0x6C }, { 576, 0x6D }, { 640, 0x4A }, { 704, 0x4B }, { 768, 0x4C }, { 832, 0x4D }, { 896, 0x72 }, - { 960, 0x73 }, { 1024, 0x74 }, { 1088, 0x75 }, { 1152, 0x76 }, { 1216, 0x77 }, { 1280, 0x52 }, { 1344, 0x53 }, - { 1408, 0x54 }, { 1472, 0x55 }, { 1536, 0x5A }, { 1600, 0x5B }, { 1664, 0x64 }, { 1728, 0x65 } - }; - /// /// The modified huffman is basically the same as CCITT T4, but without EOL markers and padding at the end of the rows. /// private readonly bool useModifiedHuffman; - private IMemoryOwner compressedDataBuffer; - - private int bytePosition; - - private byte bitPosition; - /// /// Initializes a new instance of the class. /// - /// The output. - /// The allocator. - /// The width. + /// The output stream to write the compressed data. + /// The memory allocator. + /// The width of the image. /// The bits per pixel. /// Indicates if the modified huffman RLE should be used. public T4BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, bool useModifiedHuffman = false) - : base(output, allocator, width, bitsPerPixel) - { - this.bytePosition = 0; - this.bitPosition = 0; - this.useModifiedHuffman = useModifiedHuffman; - } + : base(output, allocator, width, bitsPerPixel) => this.useModifiedHuffman = useModifiedHuffman; /// public override TiffCompression Method => this.useModifiedHuffman ? TiffCompression.Ccitt1D : TiffCompression.CcittGroup3Fax; - /// - public override void Initialize(int rowsPerStrip) - { - // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. - int maxNeededBytes = this.Width * rowsPerStrip; - this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); - } - /// - /// Writes a image compressed with CCITT T4 to the stream. + /// Writes a image compressed with CCITT T4 to the output buffer. /// /// The pixels as 8-bit gray array. /// The strip height. - public override void CompressStrip(Span pixelsAsGray, int height) + /// The destination for the compressed data. + protected override void CompressStrip(Span pixelsAsGray, int height, Span compressedData) { - DebugGuard.IsTrue(pixelsAsGray.Length / height == this.Width, "Values must be equals"); - DebugGuard.IsTrue(pixelsAsGray.Length % height == 0, "Values must be equals"); - - this.compressedDataBuffer.Clear(); - Span compressedData = this.compressedDataBuffer.GetSpan(); - - this.bytePosition = 0; - this.bitPosition = 0; - if (!this.useModifiedHuffman) { // An EOL code is expected at the start of the data. @@ -315,25 +126,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors this.WriteEndOfLine(compressedData); } - - // Write the compressed data to the stream. - int bytesToWrite = this.bitPosition != 0 ? this.bytePosition + 1 : this.bytePosition; - this.Output.Write(compressedData.Slice(0, bytesToWrite)); } - protected override void Dispose(bool disposing) => this.compressedDataBuffer?.Dispose(); - private void WriteEndOfLine(Span compressedData) { if (this.useModifiedHuffman) { - // Check if padding is necessary. - if (this.bitPosition % 8 != 0) - { - // Skip padding bits, move to next byte. - this.bytePosition++; - this.bitPosition = 0; - } + this.PadByte(); } else { @@ -341,254 +140,5 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors this.WriteCode(12, 1, compressedData); } } - - private void WriteCode(uint codeLength, uint code, Span compressedData) - { - while (codeLength > 0) - { - int bitNumber = (int)codeLength; - bool bit = (code & (1 << (bitNumber - 1))) != 0; - if (bit) - { - BitWriterUtils.WriteBit(compressedData, this.bytePosition, this.bitPosition); - } - else - { - BitWriterUtils.WriteZeroBit(compressedData, this.bytePosition, this.bitPosition); - } - - this.bitPosition++; - if (this.bitPosition == 8) - { - this.bytePosition++; - this.bitPosition = 0; - } - - codeLength--; - } - } - - private uint GetBestFittingMakeupRunLength(uint runLength) - { - for (int i = 0; i < MakeupRunLength.Length - 1; i++) - { - if (MakeupRunLength[i] <= runLength && MakeupRunLength[i + 1] > runLength) - { - return MakeupRunLength[i]; - } - } - - return MakeupRunLength[MakeupRunLength.Length - 1]; - } - - private uint GetTermCode(uint runLength, out uint codeLength, bool isWhiteRun) - { - if (isWhiteRun) - { - return this.GetWhiteTermCode(runLength, out codeLength); - } - - return this.GetBlackTermCode(runLength, out codeLength); - } - - private uint GetMakeupCode(uint runLength, out uint codeLength, bool isWhiteRun) - { - if (isWhiteRun) - { - return this.GetWhiteMakeupCode(runLength, out codeLength); - } - - return this.GetBlackMakeupCode(runLength, out codeLength); - } - - private uint GetWhiteMakeupCode(uint runLength, out uint codeLength) - { - codeLength = 0; - - if (WhiteLen5MakeupCodes.ContainsKey(runLength)) - { - codeLength = 5; - return WhiteLen5MakeupCodes[runLength]; - } - - if (WhiteLen6MakeupCodes.ContainsKey(runLength)) - { - codeLength = 6; - return WhiteLen6MakeupCodes[runLength]; - } - - if (WhiteLen7MakeupCodes.ContainsKey(runLength)) - { - codeLength = 7; - return WhiteLen7MakeupCodes[runLength]; - } - - if (WhiteLen8MakeupCodes.ContainsKey(runLength)) - { - codeLength = 8; - return WhiteLen8MakeupCodes[runLength]; - } - - if (WhiteLen9MakeupCodes.ContainsKey(runLength)) - { - codeLength = 9; - return WhiteLen9MakeupCodes[runLength]; - } - - if (WhiteLen11MakeupCodes.ContainsKey(runLength)) - { - codeLength = 11; - return WhiteLen11MakeupCodes[runLength]; - } - - if (WhiteLen12MakeupCodes.ContainsKey(runLength)) - { - codeLength = 12; - return WhiteLen12MakeupCodes[runLength]; - } - - return 0; - } - - private uint GetBlackMakeupCode(uint runLength, out uint codeLength) - { - codeLength = 0; - - if (BlackLen10MakeupCodes.ContainsKey(runLength)) - { - codeLength = 10; - return BlackLen10MakeupCodes[runLength]; - } - - if (BlackLen11MakeupCodes.ContainsKey(runLength)) - { - codeLength = 11; - return BlackLen11MakeupCodes[runLength]; - } - - if (BlackLen12MakeupCodes.ContainsKey(runLength)) - { - codeLength = 12; - return BlackLen12MakeupCodes[runLength]; - } - - if (BlackLen13MakeupCodes.ContainsKey(runLength)) - { - codeLength = 13; - return BlackLen13MakeupCodes[runLength]; - } - - return 0; - } - - private uint GetWhiteTermCode(uint runLength, out uint codeLength) - { - codeLength = 0; - - if (WhiteLen4TermCodes.ContainsKey(runLength)) - { - codeLength = 4; - return WhiteLen4TermCodes[runLength]; - } - - if (WhiteLen5TermCodes.ContainsKey(runLength)) - { - codeLength = 5; - return WhiteLen5TermCodes[runLength]; - } - - if (WhiteLen6TermCodes.ContainsKey(runLength)) - { - codeLength = 6; - return WhiteLen6TermCodes[runLength]; - } - - if (WhiteLen7TermCodes.ContainsKey(runLength)) - { - codeLength = 7; - return WhiteLen7TermCodes[runLength]; - } - - if (WhiteLen8TermCodes.ContainsKey(runLength)) - { - codeLength = 8; - return WhiteLen8TermCodes[runLength]; - } - - return 0; - } - - private uint GetBlackTermCode(uint runLength, out uint codeLength) - { - codeLength = 0; - - if (BlackLen2TermCodes.ContainsKey(runLength)) - { - codeLength = 2; - return BlackLen2TermCodes[runLength]; - } - - if (BlackLen3TermCodes.ContainsKey(runLength)) - { - codeLength = 3; - return BlackLen3TermCodes[runLength]; - } - - if (BlackLen4TermCodes.ContainsKey(runLength)) - { - codeLength = 4; - return BlackLen4TermCodes[runLength]; - } - - if (BlackLen5TermCodes.ContainsKey(runLength)) - { - codeLength = 5; - return BlackLen5TermCodes[runLength]; - } - - if (BlackLen6TermCodes.ContainsKey(runLength)) - { - codeLength = 6; - return BlackLen6TermCodes[runLength]; - } - - if (BlackLen7TermCodes.ContainsKey(runLength)) - { - codeLength = 7; - return BlackLen7TermCodes[runLength]; - } - - if (BlackLen8TermCodes.ContainsKey(runLength)) - { - codeLength = 8; - return BlackLen8TermCodes[runLength]; - } - - if (BlackLen9TermCodes.ContainsKey(runLength)) - { - codeLength = 9; - return BlackLen9TermCodes[runLength]; - } - - if (BlackLen10TermCodes.ContainsKey(runLength)) - { - codeLength = 10; - return BlackLen10TermCodes[runLength]; - } - - if (BlackLen11TermCodes.ContainsKey(runLength)) - { - codeLength = 11; - return BlackLen11TermCodes[runLength]; - } - - if (BlackLen12TermCodes.ContainsKey(runLength)) - { - codeLength = 12; - return BlackLen12TermCodes[runLength]; - } - - return 0; - } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs new file mode 100644 index 000000000..9e03d2764 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + /// + /// Bitwriter for writing compressed CCITT T6 2D data. + /// + internal sealed class T6BitCompressor : TiffCcittCompressor + { + /// + /// Vertical codes from -3 to +3. + /// + private static readonly (uint Length, uint Code)[] VerticalCodes = + { + (7u, 3u), + (6u, 3u), + (3u, 3u), + (1u, 1u), + (3u, 2u), + (6u, 2u), + (7u, 2u) + }; + + private IMemoryOwner referenceLineBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed data. + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + public T6BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel) + : base(output, allocator, width, bitsPerPixel) + { + } + + /// + public override TiffCompression Method => TiffCompression.CcittGroup4Fax; + + /// + /// Writes a image compressed with CCITT T6 to the output buffer. + /// + /// The pixels as 8-bit gray array. + /// The strip height. + /// The destination for the compressed data. + protected override void CompressStrip(Span pixelsAsGray, int height, Span compressedData) + { + // Initial reference line is all white. + Span referenceLine = this.referenceLineBuffer.GetSpan(); + referenceLine.Fill(0xff); + + for (int y = 0; y < height; y++) + { + Span row = pixelsAsGray.Slice(y * this.Width, this.Width); + uint a0 = 0; + uint a1 = row[0] == 0 ? 0 : this.FindRunEnd(row, 0); + uint b1 = referenceLine[0] == 0 ? 0 : this.FindRunEnd(referenceLine, 0); + + while (true) + { + uint b2 = this.FindRunEnd(referenceLine, b1); + if (b2 < a1) + { + // Pass mode. + this.WriteCode(4, 1, compressedData); + a0 = b2; + } + else + { + int d = int.MaxValue; + if ((b1 >= a1) && (b1 - a1 <= 3)) + { + d = (int)(b1 - a1); + } + else if ((b1 < a1) && (a1 - b1 <= 3)) + { + d = -(int)(a1 - b1); + } + + if ((d >= -3) && (d <= 3)) + { + // Vertical mode. + (uint length, uint code) = VerticalCodes[d + 3]; + this.WriteCode(length, code, compressedData); + a0 = a1; + } + else + { + // Horizontal mode. + this.WriteCode(3, 1, compressedData); + + uint a2 = this.FindRunEnd(row, a1); + if ((a0 + a1 == 0) || (row[(int)a0] != 0)) + { + this.WriteRun(a1 - a0, true, compressedData); + this.WriteRun(a2 - a1, false, compressedData); + } + else + { + this.WriteRun(a1 - a0, false, compressedData); + this.WriteRun(a2 - a1, true, compressedData); + } + + a0 = a2; + } + } + + if (a0 >= row.Length) + { + break; + } + + byte thisPixel = row[(int)a0]; + a1 = this.FindRunEnd(row, a0, thisPixel); + b1 = this.FindRunEnd(referenceLine, a0, (byte)~thisPixel); + b1 = this.FindRunEnd(referenceLine, b1, thisPixel); + } + + // This row is now the reference line. + row.CopyTo(referenceLine); + } + + this.WriteCode(12, 1, compressedData); + this.WriteCode(12, 1, compressedData); + } + + /// + protected override void Dispose(bool disposing) + { + this.referenceLineBuffer?.Dispose(); + base.Dispose(disposing); + } + + /// + /// Finds the end of a pixel run. + /// + /// The row of pixels to examine. + /// The index of the first pixel in to examine. + /// Color of pixels in the run. If not specified, the color at + /// will be used. + /// The index of the first pixel at or after + /// that does not match , or the length of , + /// whichever comes first. + private uint FindRunEnd(Span row, uint startIndex, byte? color = null) + { + if (startIndex >= row.Length) + { + return (uint)row.Length; + } + + byte colorValue = color.GetValueOrDefault(row[(int)startIndex]); + for (int i = (int)startIndex; i < row.Length; i++) + { + if (row[i] != colorValue) + { + return (uint)i; + } + } + + return (uint)row.Length; + } + + /// + public override void Initialize(int rowsPerStrip) + { + base.Initialize(rowsPerStrip); + this.referenceLineBuffer = this.Allocator.Allocate(this.Width); + } + + /// + /// Writes a run to the output buffer. + /// + /// The length of the run. + /// If true the run is white pixels, + /// if false the run is black pixels. + /// The destination to write the run to. + private void WriteRun(uint runLength, bool isWhiteRun, Span compressedData) + { + uint code; + uint codeLength; + while (runLength > 63) + { + uint makeupLength = this.GetBestFittingMakeupRunLength(runLength); + code = this.GetMakeupCode(makeupLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + runLength -= makeupLength; + } + + code = this.GetTermCode(runLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs new file mode 100644 index 000000000..316610621 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs @@ -0,0 +1,536 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + /// + /// Common functionality for CCITT T4 and T6 Compression + /// + internal abstract class TiffCcittCompressor : TiffBaseCompressor + { + protected const uint WhiteZeroRunTermCode = 0x35; + + protected const uint BlackZeroRunTermCode = 0x37; + + private static readonly uint[] MakeupRunLength = + { + 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792, 1856, 1920, 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560 + }; + + private static readonly Dictionary WhiteLen4TermCodes = new Dictionary() + { + { 2, 0x7 }, { 3, 0x8 }, { 4, 0xB }, { 5, 0xC }, { 6, 0xE }, { 7, 0xF } + }; + + private static readonly Dictionary WhiteLen5TermCodes = new Dictionary() + { + { 8, 0x13 }, { 9, 0x14 }, { 10, 0x7 }, { 11, 0x8 } + }; + + private static readonly Dictionary WhiteLen6TermCodes = new Dictionary() + { + { 1, 0x7 }, { 12, 0x8 }, { 13, 0x3 }, { 14, 0x34 }, { 15, 0x35 }, { 16, 0x2A }, { 17, 0x2B } + }; + + private static readonly Dictionary WhiteLen7TermCodes = new Dictionary() + { + { 18, 0x27 }, { 19, 0xC }, { 20, 0x8 }, { 21, 0x17 }, { 22, 0x3 }, { 23, 0x4 }, { 24, 0x28 }, { 25, 0x2B }, { 26, 0x13 }, + { 27, 0x24 }, { 28, 0x18 } + }; + + private static readonly Dictionary WhiteLen8TermCodes = new Dictionary() + { + { 0, WhiteZeroRunTermCode }, { 29, 0x2 }, { 30, 0x3 }, { 31, 0x1A }, { 32, 0x1B }, { 33, 0x12 }, { 34, 0x13 }, { 35, 0x14 }, + { 36, 0x15 }, { 37, 0x16 }, { 38, 0x17 }, { 39, 0x28 }, { 40, 0x29 }, { 41, 0x2A }, { 42, 0x2B }, { 43, 0x2C }, { 44, 0x2D }, + { 45, 0x4 }, { 46, 0x5 }, { 47, 0xA }, { 48, 0xB }, { 49, 0x52 }, { 50, 0x53 }, { 51, 0x54 }, { 52, 0x55 }, { 53, 0x24 }, + { 54, 0x25 }, { 55, 0x58 }, { 56, 0x59 }, { 57, 0x5A }, { 58, 0x5B }, { 59, 0x4A }, { 60, 0x4B }, { 61, 0x32 }, { 62, 0x33 }, + { 63, 0x34 } + }; + + private static readonly Dictionary BlackLen2TermCodes = new Dictionary() + { + { 2, 0x3 }, { 3, 0x2 } + }; + + private static readonly Dictionary BlackLen3TermCodes = new Dictionary() + { + { 1, 0x2 }, { 4, 0x3 } + }; + + private static readonly Dictionary BlackLen4TermCodes = new Dictionary() + { + { 5, 0x3 }, { 6, 0x2 } + }; + + private static readonly Dictionary BlackLen5TermCodes = new Dictionary() + { + { 7, 0x3 } + }; + + private static readonly Dictionary BlackLen6TermCodes = new Dictionary() + { + { 8, 0x5 }, { 9, 0x4 } + }; + + private static readonly Dictionary BlackLen7TermCodes = new Dictionary() + { + { 10, 0x4 }, { 11, 0x5 }, { 12, 0x7 } + }; + + private static readonly Dictionary BlackLen8TermCodes = new Dictionary() + { + { 13, 0x4 }, { 14, 0x7 } + }; + + private static readonly Dictionary BlackLen9TermCodes = new Dictionary() + { + { 15, 0x18 } + }; + + private static readonly Dictionary BlackLen10TermCodes = new Dictionary() + { + { 0, BlackZeroRunTermCode }, { 16, 0x17 }, { 17, 0x18 }, { 18, 0x8 } + }; + + private static readonly Dictionary BlackLen11TermCodes = new Dictionary() + { + { 19, 0x67 }, { 20, 0x68 }, { 21, 0x6C }, { 22, 0x37 }, { 23, 0x28 }, { 24, 0x17 }, { 25, 0x18 } + }; + + private static readonly Dictionary BlackLen12TermCodes = new Dictionary() + { + { 26, 0xCA }, { 27, 0xCB }, { 28, 0xCC }, { 29, 0xCD }, { 30, 0x68 }, { 31, 0x69 }, { 32, 0x6A }, { 33, 0x6B }, { 34, 0xD2 }, + { 35, 0xD3 }, { 36, 0xD4 }, { 37, 0xD5 }, { 38, 0xD6 }, { 39, 0xD7 }, { 40, 0x6C }, { 41, 0x6D }, { 42, 0xDA }, { 43, 0xDB }, + { 44, 0x54 }, { 45, 0x55 }, { 46, 0x56 }, { 47, 0x57 }, { 48, 0x64 }, { 49, 0x65 }, { 50, 0x52 }, { 51, 0x53 }, { 52, 0x24 }, + { 53, 0x37 }, { 54, 0x38 }, { 55, 0x27 }, { 56, 0x28 }, { 57, 0x58 }, { 58, 0x59 }, { 59, 0x2B }, { 60, 0x2C }, { 61, 0x5A }, + { 62, 0x66 }, { 63, 0x67 } + }; + + private static readonly Dictionary WhiteLen5MakeupCodes = new Dictionary() + { + { 64, 0x1B }, { 128, 0x12 } + }; + + private static readonly Dictionary WhiteLen6MakeupCodes = new Dictionary() + { + { 192, 0x17 }, { 1664, 0x18 } + }; + + private static readonly Dictionary WhiteLen8MakeupCodes = new Dictionary() + { + { 320, 0x36 }, { 384, 0x37 }, { 448, 0x64 }, { 512, 0x65 }, { 576, 0x68 }, { 640, 0x67 } + }; + + private static readonly Dictionary WhiteLen7MakeupCodes = new Dictionary() + { + { 256, 0x37 } + }; + + private static readonly Dictionary WhiteLen9MakeupCodes = new Dictionary() + { + { 704, 0xCC }, { 768, 0xCD }, { 832, 0xD2 }, { 896, 0xD3 }, { 960, 0xD4 }, { 1024, 0xD5 }, { 1088, 0xD6 }, + { 1152, 0xD7 }, { 1216, 0xD8 }, { 1280, 0xD9 }, { 1344, 0xDA }, { 1408, 0xDB }, { 1472, 0x98 }, { 1536, 0x99 }, + { 1600, 0x9A }, { 1728, 0x9B } + }; + + private static readonly Dictionary WhiteLen11MakeupCodes = new Dictionary() + { + { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } + }; + + private static readonly Dictionary WhiteLen12MakeupCodes = new Dictionary() + { + { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, + { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } + }; + + private static readonly Dictionary BlackLen10MakeupCodes = new Dictionary() + { + { 64, 0xF } + }; + + private static readonly Dictionary BlackLen11MakeupCodes = new Dictionary() + { + { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } + }; + + private static readonly Dictionary BlackLen12MakeupCodes = new Dictionary() + { + { 128, 0xC8 }, { 192, 0xC9 }, { 256, 0x5B }, { 320, 0x33 }, { 384, 0x34 }, { 448, 0x35 }, + { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, + { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } + }; + + private static readonly Dictionary BlackLen13MakeupCodes = new Dictionary() + { + { 512, 0x6C }, { 576, 0x6D }, { 640, 0x4A }, { 704, 0x4B }, { 768, 0x4C }, { 832, 0x4D }, { 896, 0x72 }, + { 960, 0x73 }, { 1024, 0x74 }, { 1088, 0x75 }, { 1152, 0x76 }, { 1216, 0x77 }, { 1280, 0x52 }, { 1344, 0x53 }, + { 1408, 0x54 }, { 1472, 0x55 }, { 1536, 0x5A }, { 1600, 0x5B }, { 1664, 0x64 }, { 1728, 0x65 } + }; + + private int bytePosition; + + private byte bitPosition; + + private IMemoryOwner compressedDataBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The output. + /// The allocator. + /// The width. + /// The bits per pixel. + protected TiffCcittCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel) + : base(output, allocator, width, bitsPerPixel) + { + DebugGuard.IsTrue(bitsPerPixel == 1, nameof(bitsPerPixel), "CCITT compression requires one bit per pixel"); + this.bytePosition = 0; + this.bitPosition = 0; + } + + private uint GetWhiteMakeupCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (WhiteLen5MakeupCodes.ContainsKey(runLength)) + { + codeLength = 5; + return WhiteLen5MakeupCodes[runLength]; + } + + if (WhiteLen6MakeupCodes.ContainsKey(runLength)) + { + codeLength = 6; + return WhiteLen6MakeupCodes[runLength]; + } + + if (WhiteLen7MakeupCodes.ContainsKey(runLength)) + { + codeLength = 7; + return WhiteLen7MakeupCodes[runLength]; + } + + if (WhiteLen8MakeupCodes.ContainsKey(runLength)) + { + codeLength = 8; + return WhiteLen8MakeupCodes[runLength]; + } + + if (WhiteLen9MakeupCodes.ContainsKey(runLength)) + { + codeLength = 9; + return WhiteLen9MakeupCodes[runLength]; + } + + if (WhiteLen11MakeupCodes.ContainsKey(runLength)) + { + codeLength = 11; + return WhiteLen11MakeupCodes[runLength]; + } + + if (WhiteLen12MakeupCodes.ContainsKey(runLength)) + { + codeLength = 12; + return WhiteLen12MakeupCodes[runLength]; + } + + return 0; + } + + private uint GetBlackMakeupCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (BlackLen10MakeupCodes.ContainsKey(runLength)) + { + codeLength = 10; + return BlackLen10MakeupCodes[runLength]; + } + + if (BlackLen11MakeupCodes.ContainsKey(runLength)) + { + codeLength = 11; + return BlackLen11MakeupCodes[runLength]; + } + + if (BlackLen12MakeupCodes.ContainsKey(runLength)) + { + codeLength = 12; + return BlackLen12MakeupCodes[runLength]; + } + + if (BlackLen13MakeupCodes.ContainsKey(runLength)) + { + codeLength = 13; + return BlackLen13MakeupCodes[runLength]; + } + + return 0; + } + + private uint GetWhiteTermCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (WhiteLen4TermCodes.ContainsKey(runLength)) + { + codeLength = 4; + return WhiteLen4TermCodes[runLength]; + } + + if (WhiteLen5TermCodes.ContainsKey(runLength)) + { + codeLength = 5; + return WhiteLen5TermCodes[runLength]; + } + + if (WhiteLen6TermCodes.ContainsKey(runLength)) + { + codeLength = 6; + return WhiteLen6TermCodes[runLength]; + } + + if (WhiteLen7TermCodes.ContainsKey(runLength)) + { + codeLength = 7; + return WhiteLen7TermCodes[runLength]; + } + + if (WhiteLen8TermCodes.ContainsKey(runLength)) + { + codeLength = 8; + return WhiteLen8TermCodes[runLength]; + } + + return 0; + } + + private uint GetBlackTermCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (BlackLen2TermCodes.ContainsKey(runLength)) + { + codeLength = 2; + return BlackLen2TermCodes[runLength]; + } + + if (BlackLen3TermCodes.ContainsKey(runLength)) + { + codeLength = 3; + return BlackLen3TermCodes[runLength]; + } + + if (BlackLen4TermCodes.ContainsKey(runLength)) + { + codeLength = 4; + return BlackLen4TermCodes[runLength]; + } + + if (BlackLen5TermCodes.ContainsKey(runLength)) + { + codeLength = 5; + return BlackLen5TermCodes[runLength]; + } + + if (BlackLen6TermCodes.ContainsKey(runLength)) + { + codeLength = 6; + return BlackLen6TermCodes[runLength]; + } + + if (BlackLen7TermCodes.ContainsKey(runLength)) + { + codeLength = 7; + return BlackLen7TermCodes[runLength]; + } + + if (BlackLen8TermCodes.ContainsKey(runLength)) + { + codeLength = 8; + return BlackLen8TermCodes[runLength]; + } + + if (BlackLen9TermCodes.ContainsKey(runLength)) + { + codeLength = 9; + return BlackLen9TermCodes[runLength]; + } + + if (BlackLen10TermCodes.ContainsKey(runLength)) + { + codeLength = 10; + return BlackLen10TermCodes[runLength]; + } + + if (BlackLen11TermCodes.ContainsKey(runLength)) + { + codeLength = 11; + return BlackLen11TermCodes[runLength]; + } + + if (BlackLen12TermCodes.ContainsKey(runLength)) + { + codeLength = 12; + return BlackLen12TermCodes[runLength]; + } + + return 0; + } + + /// + /// Gets the best makeup run length for a given run length + /// + /// A run length needing a makeup code + /// The makeup length for . + protected uint GetBestFittingMakeupRunLength(uint runLength) + { + DebugGuard.MustBeGreaterThanOrEqualTo(runLength, MakeupRunLength[0], nameof(runLength)); + + for (int i = 0; i < MakeupRunLength.Length - 1; i++) + { + if (MakeupRunLength[i] <= runLength && MakeupRunLength[i + 1] > runLength) + { + return MakeupRunLength[i]; + } + } + + return MakeupRunLength[MakeupRunLength.Length - 1]; + } + + /// + /// Gets the terminating code for a run length. + /// + /// The run length to get the terminating code for. + /// The length of the terminating code. + /// If true, the run is of white pixels. + /// If false the run is of black pixels + /// The terminating code for a run of length + protected uint GetTermCode(uint runLength, out uint codeLength, bool isWhiteRun) + { + if (isWhiteRun) + { + return this.GetWhiteTermCode(runLength, out codeLength); + } + + return this.GetBlackTermCode(runLength, out codeLength); + } + + /// + /// Gets the makeup code for a run length. + /// + /// The run length to get the makeup code for. + /// The length of the makeup code. + /// If true, the run is of white pixels. + /// If false the run is of black pixels + /// The makeup code for a run of length + protected uint GetMakeupCode(uint runLength, out uint codeLength, bool isWhiteRun) + { + if (isWhiteRun) + { + return this.GetWhiteMakeupCode(runLength, out codeLength); + } + + return this.GetBlackMakeupCode(runLength, out codeLength); + } + + /// + /// Pads output to the next byte + /// + /// + /// If the output is not currently on a byte boundary, + /// zero-pad it to the next byte + /// + protected void PadByte() + { + // Check if padding is necessary. + if (this.bitPosition % 8 != 0) + { + // Skip padding bits, move to next byte. + this.bytePosition++; + this.bitPosition = 0; + } + } + + /// + /// Writes a code to the output. + /// + /// The length of the code to write. + /// The code to be written. + /// The destination buffer to write the code to. + protected void WriteCode(uint codeLength, uint code, Span compressedData) + { + while (codeLength > 0) + { + int bitNumber = (int)codeLength; + bool bit = (code & (1 << (bitNumber - 1))) != 0; + if (bit) + { + BitWriterUtils.WriteBit(compressedData, this.bytePosition, this.bitPosition); + } + else + { + BitWriterUtils.WriteZeroBit(compressedData, this.bytePosition, this.bitPosition); + } + + this.bitPosition++; + if (this.bitPosition == 8) + { + this.bytePosition++; + this.bitPosition = 0; + } + + codeLength--; + } + } + + /// + /// Writes a image compressed with CCITT T6 to the stream. + /// + /// The pixels as 8-bit gray array. + /// The strip height. + public override void CompressStrip(Span pixelsAsGray, int height) + { + DebugGuard.IsTrue(pixelsAsGray.Length / height == this.Width, "Values must be equals"); + DebugGuard.IsTrue(pixelsAsGray.Length % height == 0, "Values must be equals"); + + this.compressedDataBuffer.Clear(); + Span compressedData = this.compressedDataBuffer.GetSpan(); + + this.bytePosition = 0; + this.bitPosition = 0; + + this.CompressStrip(pixelsAsGray, height, compressedData); + + // Write the compressed data to the stream. + int bytesToWrite = this.bitPosition != 0 ? this.bytePosition + 1 : this.bytePosition; + this.Output.Write(compressedData.Slice(0, bytesToWrite)); + } + + /// + /// Compress a data strip + /// + /// The pixels as 8-bit gray array. + /// The strip height. + /// The destination for the compressed data. + protected abstract void CompressStrip(Span pixelsAsGray, int height, Span compressedData); + + /// + protected override void Dispose(bool disposing) => this.compressedDataBuffer?.Dispose(); + + /// + public override void Initialize(int rowsPerStrip) + { + // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. + int maxNeededBytes = this.Width * rowsPerStrip; + this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs index db2b935b7..904470e81 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs @@ -55,6 +55,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); return new T4BitCompressor(output, allocator, width, bitsPerPixel, false); + case TiffCompression.CcittGroup4Fax: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T6BitCompressor(output, allocator, width, bitsPerPixel); + case TiffCompression.Ccitt1D: DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs index 031494fc5..0baf4c89c 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs @@ -35,9 +35,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants /// /// T6-encoding: CCITT T.6 bi-level encoding (see Section 11 of the TIFF 6.0 specification). - /// - /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, - /// if this is choosen. /// CcittGroup4Fax = 4, diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index ff4aa52b0..488701e31 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 | | Y | | +|CcittGroup4Fax | Y | 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) | Y | Y | | diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index e54d029ab..04f3682b0 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -395,6 +395,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff case TiffCompression.CcittGroup3Fax: return (ushort)TiffCompression.CcittGroup3Fax; + case TiffCompression.CcittGroup4Fax: + return (ushort)TiffCompression.CcittGroup4Fax; + case TiffCompression.Ccitt1D: return (ushort)TiffCompression.Ccitt1D; diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs index 5fec09ef1..f21a465cd 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs @@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers { int width = this.Image.Width; - if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D) + if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D || compressor.Method == TiffCompression.CcittGroup4Fax) { // Special case for T4BitCompressor. int stripPixels = width * height; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index aded52cd9..93ca611c9 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -109,6 +109,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw, TiffBitsPerPixel.Bit8, TiffCompression.Lzw)] [InlineData(TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, TiffBitsPerPixel.Bit8, TiffCompression.Lzw)] [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax, TiffBitsPerPixel.Bit1, TiffCompression.CcittGroup3Fax)] + [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax, TiffBitsPerPixel.Bit1, TiffCompression.CcittGroup4Fax)] [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Ccitt1D, TiffBitsPerPixel.Bit1, TiffCompression.Ccitt1D)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT43, TiffBitsPerPixel.Bit24, TiffCompression.None)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT82, TiffBitsPerPixel.Bit24, TiffCompression.None)] @@ -228,8 +229,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.CcittGroup3Fax, TiffCompression.CcittGroup3Fax)] + [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.CcittGroup4Fax, TiffCompression.CcittGroup4Fax)] [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.Ccitt1D, TiffCompression.Ccitt1D)] [WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffCompression.CcittGroup3Fax, TiffCompression.CcittGroup3Fax)] + [WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffCompression.CcittGroup4Fax, TiffCompression.CcittGroup4Fax)] [WithFile(PaletteDeflateMultistrip, PixelTypes.L8, TiffCompression.Ccitt1D, TiffCompression.Ccitt1D)] public void TiffEncoder_EncodesWithCorrectBiColorModeCompression(TestImageProvider provider, TiffCompression compression, TiffCompression expectedCompression) where TPixel : unmanaged, IPixel @@ -405,6 +408,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_BlackIsZero_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax); + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_WhiteIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.CcittGroup4Fax); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_BlackIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax); + [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_WhiteIsZero_Works(TestImageProvider provider)