From c9766005208185a9a4c989e7250d53d91ac23903 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 29 Nov 2020 13:24:51 +0100 Subject: [PATCH] Add support for encoding modified huffman RLE --- .../Formats/Tiff/Compression/T4BitReader.cs | 1 + .../Formats/Tiff/Compression/T4BitWriter.cs | 42 ++++++++++++++++--- src/ImageSharp/Formats/Tiff/README.md | 8 ++-- .../Formats/Tiff/TiffBitsPerPixel.cs | 5 +++ .../Formats/Tiff/TiffDecoderCore.cs | 4 ++ .../Formats/Tiff/TiffEncoderCompression.cs | 5 +++ .../Formats/Tiff/TiffEncoderCore.cs | 11 ++++- .../Formats/Tiff/Utils/TiffWriter.cs | 17 +++++--- .../Formats/Tiff/TiffEncoderTests.cs | 10 ++++- 9 files changed, 86 insertions(+), 17 deletions(-) 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,