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)