diff --git a/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs
new file mode 100644
index 0000000000..c37de8031a
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs
@@ -0,0 +1,1110 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using System.Linq;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.Compression
+{
+ ///
+ /// Bitreader for reading compressed CCITT T4 1D data.
+ ///
+ internal class T4BitReader
+ {
+ ///
+ /// Number of bits read.
+ ///
+ private int bitsRead;
+
+ ///
+ /// 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, if the current run are white pixels.
+ ///
+ private bool isWhiteRun;
+
+ ///
+ /// Indicates whether its the first line of data which is read from the image.
+ ///
+ private bool isFirstScanLine;
+
+ private bool terminationCodeFound;
+
+ ///
+ /// Number of pixels in the current run.
+ ///
+ private uint runLength;
+
+ private const int MinCodeLength = 2;
+
+ private const int MaxCodeLength = 13;
+
+ public T4BitReader(Stream input, int bytesToRead)
+ {
+ // TODO: use memory allocator
+ this.Data = new byte[bytesToRead];
+ this.ReadImageDataFromStream(input, bytesToRead);
+
+ this.bitsRead = 0;
+ this.value = 0;
+ this.curValueBitsRead = 0;
+ this.position = 0;
+ this.isWhiteRun = true;
+ this.isFirstScanLine = true;
+ this.terminationCodeFound = false;
+ this.runLength = 0;
+ }
+
+ ///
+ /// Gets the compressed image data.
+ ///
+ public byte[] Data { get; }
+
+ ///
+ /// Gets a value indicating whether there is more data to read left.
+ ///
+ public bool HasMoreData
+ {
+ get
+ {
+ return this.position < (ulong)this.Data.Length - 1;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the current run is a white pixel run, otherwise its a black pixel run.
+ ///
+ public bool IsWhiteRun
+ {
+ get
+ {
+ return this.isWhiteRun;
+ }
+ }
+
+ ///
+ /// Gets the number of pixels in the current run.
+ ///
+ public uint RunLength
+ {
+ get
+ {
+ return this.runLength;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the end of a pixel row has been reached.
+ ///
+ public bool IsEndOfScanLine
+ {
+ get
+ {
+ return this.curValueBitsRead == 12 && this.value == 1;
+ }
+ }
+
+ ///
+ /// Read the next run of pixels.
+ ///
+ public void ReadNextRun()
+ {
+ if (this.terminationCodeFound)
+ {
+ this.isWhiteRun = !this.IsWhiteRun;
+ this.terminationCodeFound = false;
+ }
+
+ this.Reset();
+
+ if (this.isFirstScanLine)
+ {
+ // We expect an EOL before the first data.
+ this.value = this.ReadValue(12);
+ if (!this.IsEndOfScanLine)
+ {
+ TiffThrowHelper.ThrowImageFormatException("t4 parsing error: expected start of data marker not found");
+ }
+
+ this.Reset();
+ }
+
+ // A code word must have at least 2 bits.
+ this.value = this.ReadValue(MinCodeLength);
+
+ do
+ {
+ if (this.curValueBitsRead > MaxCodeLength)
+ {
+ TiffThrowHelper.ThrowImageFormatException("t4 parsing error: invalid code length read");
+ }
+
+ bool isTerminatingCode = this.IsTerminatingCode();
+ if (isTerminatingCode)
+ {
+ // Each line starts with a white run. If the image starts with black, a white run with length zero is written.
+ if (this.IsWhiteRun && this.WhiteTerminatingCodeRunLength() == 0)
+ {
+ this.isWhiteRun = !this.IsWhiteRun;
+ this.Reset();
+ continue;
+ }
+
+ if (this.IsWhiteRun)
+ {
+ this.runLength += this.WhiteTerminatingCodeRunLength();
+ }
+ else
+ {
+ this.runLength += this.BlackTerminatingCodeRunLength();
+ }
+
+ this.terminationCodeFound = true;
+ break;
+ }
+
+ bool isMakeupCode = this.IsMakeupCode();
+ if (isMakeupCode)
+ {
+ if (this.IsWhiteRun)
+ {
+ this.runLength += this.WhiteMakeupCodeRunLength();
+ }
+ else
+ {
+ this.runLength += this.BlackMakeupCodeRunLength();
+ }
+
+ this.Reset(false);
+ continue;
+ }
+
+ var currBit = this.ReadValue(1);
+ this.value = (this.value << 1) | currBit;
+
+ if (this.IsEndOfScanLine)
+ {
+ // Each new row starts with a white run.
+ this.isWhiteRun = true;
+ }
+ }
+ while (!this.IsEndOfScanLine);
+
+ this.isFirstScanLine = false;
+ }
+
+ private uint WhiteTerminatingCodeRunLength()
+ {
+ switch (this.curValueBitsRead)
+ {
+ case 4:
+ {
+ switch (this.value)
+ {
+ case 0x7:
+ return 2;
+ case 0x8:
+ return 3;
+ case 0xB:
+ return 4;
+ case 0xC:
+ return 5;
+ case 0xE:
+ return 6;
+ case 0xF:
+ return 7;
+ }
+
+ break;
+ }
+
+ case 5:
+ {
+ switch (this.value)
+ {
+ case 0x13:
+ return 8;
+ case 0x14:
+ return 9;
+ case 0x7:
+ return 10;
+ case 0x8:
+ return 11;
+ }
+
+ break;
+ }
+
+ case 6:
+ {
+ switch (this.value)
+ {
+ case 0x7:
+ return 1;
+ case 0x8:
+ return 12;
+ case 0x3:
+ return 13;
+ case 0x34:
+ return 14;
+ case 0x35:
+ return 15;
+ case 0x2A:
+ return 16;
+ case 0x2B:
+ return 17;
+ }
+
+ break;
+ }
+
+ case 7:
+ {
+ switch (this.value)
+ {
+ case 0x27:
+ return 18;
+ case 0xC:
+ return 19;
+ case 0x8:
+ return 20;
+ case 0x17:
+ return 21;
+ case 0x3:
+ return 22;
+ case 0x4:
+ return 23;
+ case 0x28:
+ return 24;
+ case 0x2B:
+ return 25;
+ case 0x13:
+ return 26;
+ case 0x24:
+ return 27;
+ case 0x18:
+ return 28;
+ }
+
+ break;
+ }
+
+ case 8:
+ {
+ switch (this.value)
+ {
+ case 0x35:
+ return 0;
+ case 0x2:
+ return 29;
+ case 0x3:
+ return 30;
+ case 0x1A:
+ return 31;
+ case 0x1B:
+ return 32;
+ case 0x12:
+ return 33;
+ case 0x13:
+ return 34;
+ case 0x14:
+ return 35;
+ case 0x15:
+ return 36;
+ case 0x16:
+ return 37;
+ case 0x17:
+ return 38;
+ case 0x28:
+ return 39;
+ case 0x29:
+ return 40;
+ case 0x2A:
+ return 41;
+ case 0x2B:
+ return 42;
+ case 0x2C:
+ return 43;
+ case 0x2D:
+ return 44;
+ case 0x4:
+ return 45;
+ case 0x5:
+ return 46;
+ case 0xA:
+ return 47;
+ case 0xB:
+ return 48;
+ case 0x52:
+ return 49;
+ case 0x53:
+ return 50;
+ case 0x54:
+ return 51;
+ case 0x55:
+ return 52;
+ case 0x24:
+ return 53;
+ case 0x25:
+ return 54;
+ case 0x58:
+ return 55;
+ case 0x59:
+ return 56;
+ case 0x5A:
+ return 57;
+ case 0x5B:
+ return 58;
+ case 0x4A:
+ return 59;
+ case 0x4B:
+ return 60;
+ case 0x32:
+ return 61;
+ case 0x33:
+ return 62;
+ case 0x34:
+ return 63;
+ }
+
+ break;
+ }
+ }
+
+ return 0;
+ }
+
+ private uint BlackTerminatingCodeRunLength()
+ {
+ switch (this.curValueBitsRead)
+ {
+ case 2:
+ {
+ switch (this.value)
+ {
+ case 0x3:
+ return 2;
+ case 0x2:
+ return 3;
+ }
+ break;
+ }
+
+ case 3:
+ {
+ switch (this.value)
+ {
+ case 0x2:
+ return 1;
+ case 0x3:
+ return 4;
+ }
+
+ break;
+ }
+
+ case 4:
+ {
+ switch (this.value)
+ {
+ case 0x3:
+ return 5;
+ case 0x2:
+ return 6;
+ }
+
+ break;
+ }
+
+ case 5:
+ {
+ switch (this.value)
+ {
+ case 0x3:
+ return 7;
+ }
+
+ break;
+ }
+
+ case 6:
+ {
+ switch (this.value)
+ {
+ case 0x5:
+ return 8;
+ case 0x4:
+ return 9;
+ }
+
+ break;
+ }
+
+ case 7:
+ {
+ switch (this.value)
+ {
+ case 0x4:
+ return 10;
+ case 0x5:
+ return 11;
+ case 0x7:
+ return 12;
+ }
+
+ break;
+ }
+
+ case 8:
+ {
+ switch (this.value)
+ {
+ case 0x4:
+ return 13;
+ case 0x7:
+ return 14;
+ }
+
+ break;
+ }
+
+ case 9:
+ {
+ switch (this.value)
+ {
+ case 0x18:
+ return 15;
+ }
+
+ break;
+ }
+
+ case 10:
+ {
+ switch (this.value)
+ {
+ case 0x37:
+ return 0;
+ case 0x17:
+ return 16;
+ case 0x18:
+ return 17;
+ case 0x8:
+ return 18;
+ }
+
+ break;
+ }
+
+ case 11:
+ {
+ switch (this.value)
+ {
+ case 0x67:
+ return 19;
+ case 0x68:
+ return 20;
+ case 0x6C:
+ return 21;
+ case 0x37:
+ return 22;
+ case 0x28:
+ return 23;
+ case 0x17:
+ return 24;
+ case 0x18:
+ return 25;
+ }
+
+ break;
+ }
+
+ case 12:
+ {
+ switch (this.value)
+ {
+ case 0xCA:
+ return 26;
+ case 0xCB:
+ return 27;
+ case 0xCC:
+ return 28;
+ case 0xCD:
+ return 29;
+ case 0x68:
+ return 30;
+ case 0x69:
+ return 31;
+ case 0x6A:
+ return 32;
+ case 0x6B:
+ return 33;
+ case 0xD2:
+ return 34;
+ case 0xD3:
+ return 35;
+ case 0xD4:
+ return 36;
+ case 0xD5:
+ return 37;
+ case 0xD6:
+ return 38;
+ case 0xD7:
+ return 39;
+ case 0x6C:
+ return 40;
+ case 0x6D:
+ return 41;
+ case 0xDA:
+ return 42;
+ case 0xDB:
+ return 43;
+ case 0x54:
+ return 44;
+ case 0x55:
+ return 45;
+ case 0x56:
+ return 46;
+ case 0x57:
+ return 47;
+ case 0x64:
+ return 48;
+ case 0x65:
+ return 49;
+ case 0x52:
+ return 50;
+ case 0x53:
+ return 51;
+ case 0x24:
+ return 52;
+ case 0x37:
+ return 53;
+ case 0x38:
+ return 54;
+ case 0x27:
+ return 55;
+ case 0x28:
+ return 56;
+ case 0x58:
+ return 57;
+ case 0x59:
+ return 58;
+ case 0x2B:
+ return 59;
+ case 0x2C:
+ return 60;
+ case 0x5A:
+ return 61;
+ case 0x66:
+ return 62;
+ case 0x67:
+ return 63;
+ }
+
+ break;
+ }
+ }
+
+ return 0;
+ }
+
+ private uint WhiteMakeupCodeRunLength()
+ {
+ switch (this.curValueBitsRead)
+ {
+ case 5:
+ {
+ switch (this.value)
+ {
+ case 0x1B:
+ return 64;
+ case 0x12:
+ return 128;
+ }
+
+ break;
+ }
+
+ case 6:
+ {
+ switch (this.value)
+ {
+ case 0x17:
+ return 192;
+ case 0x18:
+ return 1664;
+ }
+
+ break;
+ }
+
+ case 7:
+ {
+ switch (this.value)
+ {
+ case 0x37:
+ return 256;
+ }
+
+ break;
+ }
+
+ case 8:
+ {
+ switch (this.value)
+ {
+ case 0x36:
+ return 320;
+ case 0x37:
+ return 348;
+ case 0x64:
+ return 448;
+ case 0x65:
+ return 512;
+ case 0x68:
+ return 576;
+ case 0x67:
+ return 640;
+ }
+
+ break;
+ }
+
+ case 9:
+ {
+ switch (this.value)
+ {
+ case 0xCC:
+ return 704;
+ case 0xCD:
+ return 768;
+ case 0xD2:
+ return 832;
+ case 0xD3:
+ return 896;
+ case 0xD4:
+ return 960;
+ case 0xD5:
+ return 1024;
+ case 0xD6:
+ return 1088;
+ case 0xD7:
+ return 1152;
+ case 0xD8:
+ return 1216;
+ case 0xD9:
+ return 1280;
+ case 0xDA:
+ return 1344;
+ case 0xDB:
+ return 1408;
+ case 0x98:
+ return 1472;
+ case 0x99:
+ return 1536;
+ case 0x9A:
+ return 1600;
+ case 0x9B:
+ return 1728;
+ }
+
+ break;
+ }
+ }
+
+ return 0;
+ }
+
+ private uint BlackMakeupCodeRunLength()
+ {
+ switch (this.curValueBitsRead)
+ {
+ case 10:
+ {
+ switch (this.value)
+ {
+ case 0xF:
+ return 64;
+ }
+ }
+
+ break;
+
+ case 12:
+ {
+ switch (this.value)
+ {
+ case 0xC8:
+ return 128;
+ case 0xC9:
+ return 192;
+ case 0x5B:
+ return 256;
+ case 0x33:
+ return 320;
+ case 0x34:
+ return 384;
+ case 0x35:
+ return 448;
+ }
+ }
+
+ break;
+
+ case 13:
+ {
+ switch (this.value)
+ {
+ case 0x6C:
+ return 512;
+ case 0x6D:
+ return 576;
+ case 0x4A:
+ return 640;
+ case 0x4B:
+ return 704;
+ case 0x4C:
+ return 768;
+ case 0x4D:
+ return 832;
+ case 0x72:
+ return 896;
+ case 0x73:
+ return 960;
+ case 0x74:
+ return 1024;
+ case 0x75:
+ return 1088;
+ case 0x76:
+ return 1152;
+ case 0x77:
+ return 1216;
+ case 0x52:
+ return 1280;
+ case 0x53:
+ return 1344;
+ case 0x54:
+ return 1408;
+ case 0x55:
+ return 1472;
+ case 0x5A:
+ return 1536;
+ case 0x5B:
+ return 1600;
+ case 0x64:
+ return 1664;
+ case 0x65:
+ return 1728;
+ }
+
+ break;
+ }
+ }
+
+ return 0;
+ }
+
+ private bool IsMakeupCode()
+ {
+ if (this.IsWhiteRun)
+ {
+ return this.IsWhiteMakeupCode();
+ }
+
+ return this.IsBlackMakeupCode();
+ }
+
+ private bool IsWhiteMakeupCode()
+ {
+ switch (this.curValueBitsRead)
+ {
+ case 5:
+ {
+ uint[] codes = { 0x1B, 0x12 };
+ return codes.Contains(this.value);
+ }
+
+ case 6:
+ {
+ uint[] codes = { 0x17, 0x18 };
+ return codes.Contains(this.value);
+ }
+
+ case 7:
+ {
+ uint[] codes = { 0x37 };
+ return codes.Contains(this.value);
+ }
+
+ case 8:
+ {
+ uint[] codes = { 0x36, 0x37, 0x64, 0x65, 0x68, 0x67 };
+ return codes.Contains(this.value);
+ }
+
+ case 9:
+ {
+ uint[] codes =
+ {
+ 0xCC, 0xCD, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0x98,
+ 0x99, 0x9A, 0x9B
+ };
+ return codes.Contains(this.value);
+ }
+ }
+
+ return false;
+ }
+
+ private bool IsBlackMakeupCode()
+ {
+ switch (this.curValueBitsRead)
+ {
+ case 10:
+ {
+ uint[] codes = { 0xF };
+ return codes.Contains(this.value);
+ }
+
+ case 12:
+ {
+ uint[] codes = { 0xC8, 0xC9, 0x5B, 0x33, 0x34, 0x35 };
+ return codes.Contains(this.value);
+ }
+
+ case 13:
+ {
+ uint[] codes =
+ {
+ 0x6C, 0x6D, 0x4A, 0x4B, 0x4C, 0x4D, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x52,
+ 0x53, 0x54, 0x55, 0x5A, 0x5B, 0x64, 0x65
+ };
+ return codes.Contains(this.value);
+ }
+ }
+
+ return false;
+ }
+
+ private bool IsTerminatingCode()
+ {
+ if (this.IsWhiteRun)
+ {
+ return this.IsWhiteTerminatingCode();
+ }
+
+ return this.IsBlackTerminatingCode();
+ }
+
+ private bool IsWhiteTerminatingCode()
+ {
+ switch (this.curValueBitsRead)
+ {
+ case 4:
+ {
+ uint[] codes = { 0x7, 0x8, 0xB, 0xC, 0xE, 0xF };
+ return codes.Contains(this.value);
+ }
+
+ case 5:
+ {
+ uint[] codes = { 0x13, 0x14, 0x7, 0x8 };
+ return codes.Contains(this.value);
+ }
+
+ case 6:
+ {
+ uint[] codes = { 0x7, 0x8, 0x3, 0x34, 0x35, 0x2A, 0x2B };
+ return codes.Contains(this.value);
+ }
+
+ case 7:
+ {
+ uint[] codes = { 0x27, 0xC, 0x8, 0x17, 0x3, 0x4, 0x28, 0x2B, 0x13, 0x24, 0x18 };
+ return codes.Contains(this.value);
+ }
+
+ case 8:
+ {
+ uint[] codes =
+ {
+ 0x35, 0x2, 0x3, 0x1A, 0x1B, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x28, 0x29,
+ 0x2A, 0x2B, 0x2C, 0x2D, 0x4, 0x5, 0xA, 0xB, 0x52, 0x53, 0x54, 0x55, 0x24, 0x25,
+ 0x58, 0x59, 0x5A, 0x5B, 0x4A, 0x4B, 0x32, 0x33, 0x34
+ };
+ return codes.Contains(this.value);
+ }
+ }
+
+ return false;
+ }
+
+ private bool IsBlackTerminatingCode()
+ {
+ switch (this.curValueBitsRead)
+ {
+ case 2:
+ {
+ uint[] codes = {0x3, 0x2};
+ return codes.Contains(this.value);
+ }
+
+ case 3:
+ {
+ uint[] codes = {0x02, 0x03};
+ return codes.Contains(this.value);
+ }
+
+ case 4:
+ {
+ uint[] codes = {0x03, 0x02};
+ return codes.Contains(this.value);
+ }
+
+ case 5:
+ {
+ uint[] codes = {0x03};
+ return codes.Contains(this.value);
+ }
+
+ case 6:
+ {
+ uint[] codes = {0x5, 0x4};
+ return codes.Contains(this.value);
+ }
+
+ case 7:
+ {
+ uint[] codes = { 0x4, 0x5, 0x7 };
+ return codes.Contains(this.value);
+ }
+
+ case 8:
+ {
+ uint[] codes = { 0x4, 0x7 };
+ return codes.Contains(this.value);
+ }
+
+ case 9:
+ {
+ uint[] codes = { 0x18 };
+ return codes.Contains(this.value);
+ }
+
+ case 10:
+ {
+ uint[] codes = { 0x37, 0x17, 0x18, 0x8 };
+ return codes.Contains(this.value);
+ }
+
+ case 11:
+ {
+ uint[] codes = { 0x67, 0x68, 0x6C, 0x37, 0x28, 0x17, 0x18 };
+ return codes.Contains(this.value);
+ }
+
+ case 12:
+ {
+ uint[] codes =
+ {
+ 0xCA, 0xCB, 0xCC, 0xCD, 0x68, 0x69, 0x6A, 0x6B, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6,
+ 0xD7, 0x6C, 0x6D, 0xDA, 0xDB, 0x54, 0x55, 0x56, 0x57, 0x64, 0x65, 0x52, 0x53,
+ 0x24, 0x37, 0x38, 0x27, 0x28, 0x58, 0x59, 0x2B, 0x2C, 0x5A, 0x66, 0x67
+ };
+ return codes.Contains(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));
+ Guard.MustBeLessThanOrEqualTo(nBits, 12, 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)
+ {
+ this.LoadNewByte();
+ }
+
+ int shift = 8 - this.bitsRead - 1;
+ var bit = (uint)((this.Data[this.position] & (1 << shift)) != 0 ? 1 : 0);
+ this.bitsRead++;
+
+ return bit;
+ }
+
+ private void LoadNewByte()
+ {
+ this.position++;
+ this.bitsRead = 0;
+
+ if (this.position >= (ulong)this.Data.Length)
+ {
+ TiffThrowHelper.ThrowImageFormatException("tiff image has invalid t4 compressed data");
+ }
+ }
+
+ private void ReadImageDataFromStream(Stream input, int bytesToRead)
+ {
+ var buffer = new byte[4096];
+
+ Span bufferSpan = buffer.AsSpan();
+ Span dataSpan = this.Data.AsSpan();
+
+ int read;
+ while (bytesToRead > 0 &&
+ (read = input.Read(buffer, 0, Math.Min(bufferSpan.Length, bytesToRead))) > 0)
+ {
+ buffer.AsSpan(0, read).CopyTo(dataSpan);
+ bytesToRead -= read;
+ dataSpan = dataSpan.Slice(read);
+ }
+
+ if (bytesToRead > 0)
+ {
+ TiffThrowHelper.ThrowImageFormatException("tiff image file has insufficient data");
+ }
+ }
+}
+}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs
new file mode 100644
index 0000000000..52b11613b6
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.Compression
+{
+ ///
+ /// Class to handle cases where TIFF image data is compressed using CCITT T4 compression.
+ ///
+ internal class T4TiffCompression : TiffBaseCompression
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The memory allocator.
+ public T4TiffCompression(MemoryAllocator allocator)
+ : base(allocator)
+ {
+ }
+
+ ///
+ public override void Decompress(Stream stream, int byteCount, Span buffer)
+ {
+ // TODO: handle case when white is not zero.
+ bool isWhiteZero = true;
+ int whiteValue = isWhiteZero ? 0 : 1;
+ int blackValue = isWhiteZero ? 1 : 0;
+
+ var bitReader = new T4BitReader(stream, byteCount);
+
+ uint bitsWritten = 0;
+ uint pixels = 0;
+ while (bitReader.HasMoreData)
+ {
+ bitReader.ReadNextRun();
+
+ if (bitReader.IsEndOfScanLine)
+ {
+ // Write padding bytes, if necessary.
+ uint pad = 8 - (bitsWritten % 8);
+ if (pad != 8)
+ {
+ this.WriteBits(buffer, (int)bitsWritten, pad, 0);
+ bitsWritten += pad;
+ }
+
+ continue;
+ }
+
+ if (bitReader.IsWhiteRun)
+ {
+ this.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, whiteValue);
+ bitsWritten += bitReader.RunLength;
+ pixels += bitReader.RunLength;
+ }
+ else
+ {
+ this.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, blackValue);
+ bitsWritten += bitReader.RunLength;
+ pixels += bitReader.RunLength;
+ }
+ }
+
+ int foo = 0;
+ }
+
+ private void WriteBits(Span buffer, int pos, uint count, int value)
+ {
+ int bitPos = pos % 8;
+ int bufferPos = pos / 8;
+ int startIdx = bufferPos + bitPos;
+ int endIdx = (int)(startIdx + count);
+
+ for (int i = startIdx; i < endIdx; i++)
+ {
+ this.WriteBit(buffer, bufferPos, bitPos, value);
+
+ bitPos++;
+ if (bitPos >= 8)
+ {
+ bitPos = 0;
+ bufferPos++;
+ }
+ }
+ }
+
+ private void WriteBit(Span buffer, int bufferPos, int bitPos, int value)
+ {
+ buffer[bufferPos] |= (byte)(value << (7 - bitPos));
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs
index 0f893448d3..cedbbe35b0 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs
@@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
-using System;
+using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff
@@ -20,6 +20,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
return new DeflateTiffCompression(allocator);
case TiffCompressionType.Lzw:
return new LzwTiffCompression(allocator);
+ case TiffCompressionType.T4:
+ return new T4TiffCompression(allocator);
default:
throw TiffThrowHelper.NotSupportedCompression(nameof(compressionType));
}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs
index b62def1ea3..665e4aca20 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff
@@ -27,5 +27,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// Image data is compressed using LZW compression.
///
Lzw = 3,
+
+ ///
+ /// Image data is compressed using T4-encoding: CCITT T.4.
+ ///
+ T4 = 4,
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 468989d19c..7eced53bd1 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -1,8 +1,6 @@
// 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 System.Linq;
@@ -311,7 +309,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
- using IManagedByteBuffer stripBuffer = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize);
+ using IManagedByteBuffer stripBuffer = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize, AllocationOptions.Clean);
Buffer2D pixels = frame.PixelBuffer;
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs
index 86a7560cf4..5348be8ce9 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs
@@ -1,14 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
-using System;
using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
-using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
{
@@ -348,6 +346,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
break;
}
+ case TiffCompression.CcittGroup3Fax:
+ {
+ options.CompressionType = TiffCompressionType.T4;
+ break;
+ }
+
default:
{
TiffThrowHelper.ThrowNotSupported("The specified TIFF compression format is not supported: " + compression);
diff --git a/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs b/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs
index 3254744bc8..96a3e8dbc7 100644
--- a/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs
@@ -12,6 +12,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
internal static class TiffThrowHelper
{
+ ///
+ /// Cold path optimization for throwing -s
+ ///
+ /// The error message for the exception.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static void ThrowImageFormatException(string errorMessage)
+ {
+ throw new ImageFormatException(errorMessage);
+ }
+
[MethodImpl(InliningOptions.ColdPath)]
public static Exception TagNotFound(string tagName)
=> new ArgumentException("Required tag is not found.", tagName);