diff --git a/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs
index ed2fad7ed9..672f4a008f 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs
@@ -6,6 +6,7 @@ using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
diff --git a/src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs b/src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs
index 0dd79410fa..ee924fc77e 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs
@@ -5,6 +5,7 @@ using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
+
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -183,17 +184,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
private byte bitPosition;
+ ///
+ /// The modified huffman is basically the same as CCITT T4, but without EOL markers and padding at the end of the rows.
+ ///
+ private bool useModifiedHuffman;
+
///
/// Initializes a new instance of the class.
///
/// The memory allocator.
/// The configuration.
- public T4BitWriter(MemoryAllocator memoryAllocator, Configuration configuration)
+ /// Indicates if the modified huffman RLE should be used.
+ public T4BitWriter(MemoryAllocator memoryAllocator, Configuration configuration, bool useModifiedHuffman = false)
{
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
this.bytePosition = 0;
this.bitPosition = 0;
+ this.useModifiedHuffman = useModifiedHuffman;
}
///
@@ -215,9 +223,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
this.bytePosition = 0;
this.bitPosition = 0;
- // An EOL code is expected at the start of the data.
- this.WriteCode(12, 1, compressedData);
+ if (!this.useModifiedHuffman)
+ {
+ // An EOL code is expected at the start of the data.
+ this.WriteCode(12, 1, compressedData);
+ }
+ uint pixelsWritten = 0;
for (int y = 0; y < image.Height; y++)
{
bool isWhiteRun = true;
@@ -268,6 +280,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
code = this.GetTermCode(runLength, out codeLength, isWhiteRun);
this.WriteCode(codeLength, code, compressedData);
x += (int)runLength;
+ pixelsWritten += runLength;
}
else
{
@@ -275,6 +288,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
code = this.GetMakeupCode(runLength, out codeLength, isWhiteRun);
this.WriteCode(codeLength, code, compressedData);
x += (int)runLength;
+ pixelsWritten += runLength;
// If we are at the end of the line with a makeup code, we need to write a final term code with a length of zero.
if (x == image.Width)
@@ -296,8 +310,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
isWhiteRun = !isWhiteRun;
}
- // Write EOL.
- this.WriteCode(12, 1, compressedData);
+ this.WriteEndOfLine(compressedData);
}
// Write the compressed data to the stream.
@@ -306,6 +319,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
return this.bytePosition;
}
+ 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;
+ }
+ }
+ else
+ {
+ // Write EOL.
+ this.WriteCode(12, 1, compressedData);
+ }
+ }
+
private void WriteCode(uint codeLength, uint code, Span compressedData)
{
while (codeLength > 0)
diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md
index feefdd55a0..6cfda9df95 100644
--- a/src/ImageSharp/Formats/Tiff/README.md
+++ b/src/ImageSharp/Formats/Tiff/README.md
@@ -41,9 +41,9 @@
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------|
|None | Y | Y | |
-|Ccitt1D | | Y | |
+|Ccitt1D | Y | Y | |
|PackBits | | Y | |
-|CcittGroup3Fax | | Y | |
+|CcittGroup3Fax | Y | Y | |
|CcittGroup4Fax | | | |
|Lzw | | 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 |
@@ -55,8 +55,8 @@
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------|
-|WhiteIsZero | | Y | General + 1/4/8-bit optimised implementations |
-|BlackIsZero | | Y | General + 1/4/8-bit optimised implementations |
+|WhiteIsZero | Y | Y | General + 1/4/8-bit optimised implementations |
+|BlackIsZero | Y | Y | General + 1/4/8-bit optimised implementations |
|Rgb (Chunky) | Y | Y | General + Rgb888 optimised implementation |
|Rgb (Planar) | | Y | General implementation only |
|PaletteColor | Y | Y | General implementation only |
diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
index 502c2e425c..fe53a1bd3e 100644
--- a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
@@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
public enum TiffBitsPerPixel
{
+ ///
+ /// 1 bits per pixel, bi-color image. Each pixel consists of 1 bit.
+ ///
+ Pixel1 = 1,
+
///
/// 8 bits per pixel, grayscale image. Each pixel consists of 1 byte.
///
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 16f64e3506..e3806ee543 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -159,6 +159,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
this.tiffMetaData.BitsPerPixel = TiffBitsPerPixel.Pixel8;
}
+ else if (bitsPerPixel == 1)
+ {
+ this.tiffMetaData.BitsPerPixel = TiffBitsPerPixel.Pixel1;
+ }
}
///
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs
index 30702641a0..c76935b3a2 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs
@@ -22,5 +22,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// Use CCITT T4 1D compression. Note: This is only valid for bi-level images.
///
CcittGroup3Fax,
+
+ ///
+ /// Use the modified Huffman RLE. Note: This is only valid for bi-level images.
+ ///
+ ModifiedHuffman,
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index 742c2da424..f4e5161680 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
@@ -98,6 +98,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
this.Mode = TiffEncodingMode.Gray;
}
+ else if (this.bitsPerPixel == TiffBitsPerPixel.Pixel1)
+ {
+ this.Mode = TiffEncodingMode.BiColor;
+ }
}
this.SetPhotometricInterpretation();
@@ -341,7 +345,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor;
break;
case TiffEncodingMode.BiColor:
- if (this.CompressionType == TiffEncoderCompression.CcittGroup3Fax)
+ if (this.CompressionType == TiffEncoderCompression.CcittGroup3Fax || this.CompressionType == TiffEncoderCompression.ModifiedHuffman)
{
// The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero.
this.PhotometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero;
@@ -431,6 +435,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
return (ushort)TiffCompression.CcittGroup3Fax;
}
+ if (this.CompressionType == TiffEncoderCompression.ModifiedHuffman && this.Mode == TiffEncodingMode.BiColor)
+ {
+ return (ushort)TiffCompression.Ccitt1D;
+ }
+
return (ushort)TiffCompression.None;
}
}
diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
index eaa71c953f..245bdb74e2 100644
--- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
+++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
@@ -376,18 +376,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff
Span pixelRowAsGraySpan = pixelRowAsGray.GetSpan();
// Convert image to black and white.
- using Image imageClone = image.Clone();
- imageClone.Mutate(img => img.BinaryDither(default(ErrorDither)));
+ // TODO: Should we allow to skip this by the user, if its known to be black and white already?
+ using Image imageBlackWhite = image.Clone();
+ imageBlackWhite.Mutate(img => img.BinaryDither(default(ErrorDither)));
if (compression == TiffEncoderCompression.Deflate)
{
- return this.WriteBiColorDeflate(image, pixelRowAsGraySpan, outputRow);
+ return this.WriteBiColorDeflate(imageBlackWhite, pixelRowAsGraySpan, outputRow);
}
if (compression == TiffEncoderCompression.CcittGroup3Fax)
{
var bitWriter = new T4BitWriter(this.memoryAllocator, this.configuration);
- return bitWriter.CompressImage(image, pixelRowAsGraySpan, this.output);
+ return bitWriter.CompressImage(imageBlackWhite, pixelRowAsGraySpan, this.output);
+ }
+
+ if (compression == TiffEncoderCompression.ModifiedHuffman)
+ {
+ var bitWriter = new T4BitWriter(this.memoryAllocator, this.configuration, useModifiedHuffman: true);
+ return bitWriter.CompressImage(imageBlackWhite, pixelRowAsGraySpan, this.output);
}
int bytesWritten = 0;
@@ -395,7 +402,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
int bitIndex = 0;
int byteIndex = 0;
- Span pixelRow = imageClone.GetPixelRowSpan(y);
+ Span pixelRow = imageBlackWhite.GetPixelRowSpan(y);
PixelOperations.Instance.ToL8(this.configuration, pixelRow, pixelRowAsGraySpan);
for (int x = 0; x < pixelRow.Length; x++)
{
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
index 03d0b2eef2..5b09324dfe 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
@@ -21,6 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public static readonly TheoryData TiffBitsPerPixelFiles =
new TheoryData
{
+ { TestImages.Tiff.Calliphora_BiColor, TiffBitsPerPixel.Pixel1 },
{ TestImages.Tiff.GrayscaleUncompressed, TiffBitsPerPixel.Pixel8 },
{ TestImages.Tiff.RgbUncompressed, TiffBitsPerPixel.Pixel24 },
};
@@ -85,12 +86,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Theory]
[WithFile(TestImages.Tiff.Calliphora_BiColor, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeBiColor_WithDeflateCompression_Works(TestImageProvider provider)
- where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.BiColor, TiffEncoderCompression.Deflate);
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel1, TiffEncodingMode.BiColor, TiffEncoderCompression.Deflate);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_BiColor, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_Works(TestImageProvider provider)
- where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.BiColor, TiffEncoderCompression.CcittGroup3Fax);
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel1, TiffEncodingMode.BiColor, TiffEncoderCompression.CcittGroup3Fax);
+
+ [Theory]
+ [WithFile(TestImages.Tiff.Calliphora_BiColor, PixelTypes.Rgba32)]
+ public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel1, TiffEncodingMode.BiColor, TiffEncoderCompression.ModifiedHuffman);
private static void TestTiffEncoderCore(
TestImageProvider provider,