From 5d00bf2430bdd00a5106021be60a3dc5644fc016 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 28 Nov 2020 19:23:50 +0100 Subject: [PATCH] Add T4 BitWriter: So far only works for run length up to 63 --- .../Tiff/Compression/BitWriterUtils.cs | 47 +++ ...n.cs => ModifiedHuffmanTiffCompression.cs} | 18 +- .../Formats/Tiff/Compression/T4BitWriter.cs | 365 ++++++++++++++++++ .../Tiff/Compression/T4TiffCompression.cs | 35 +- .../Compression/TiffCompressionFactory.cs | 2 +- .../Formats/Tiff/TiffEncoderCompression.cs | 7 +- .../Formats/Tiff/TiffEncoderCore.cs | 5 + .../Formats/Tiff/Utils/TiffWriter.cs | 7 + 8 files changed, 445 insertions(+), 41 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs rename src/ImageSharp/Formats/Tiff/Compression/{TiffModifiedHuffmanCompression.cs => ModifiedHuffmanTiffCompression.cs} (75%) create mode 100644 src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs diff --git a/src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs b/src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs new file mode 100644 index 0000000000..05efb0423b --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + internal static class BitWriterUtils + { + public static void WriteBits(Span buffer, int pos, uint count, byte 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++) + { + if (value == 1) + { + WriteBit(buffer, bufferPos, bitPos); + } + else + { + WriteZeroBit(buffer, bufferPos, bitPos); + } + + bitPos++; + if (bitPos >= 8) + { + bitPos = 0; + bufferPos++; + } + } + } + + public static void WriteBit(Span buffer, int bufferPos, int bitPos) + { + buffer[bufferPos] |= (byte)(1 << (7 - bitPos)); + } + + public static void WriteZeroBit(Span buffer, int bufferPos, int bitPos) + { + buffer[bufferPos] = (byte)(buffer[bufferPos] & ~(1 << (7 - bitPos))); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffModifiedHuffmanCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/ModifiedHuffmanTiffCompression.cs similarity index 75% rename from src/ImageSharp/Formats/Tiff/Compression/TiffModifiedHuffmanCompression.cs rename to src/ImageSharp/Formats/Tiff/Compression/ModifiedHuffmanTiffCompression.cs index f742c11764..63908ff2f0 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffModifiedHuffmanCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/ModifiedHuffmanTiffCompression.cs @@ -10,15 +10,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// /// Class to handle cases where TIFF image data is compressed using Modified Huffman Compression. /// - internal class TiffModifiedHuffmanCompression : T4TiffCompression + internal class ModifiedHuffmanTiffCompression : T4TiffCompression { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The memory allocator. /// The photometric interpretation. /// The image width. - public TiffModifiedHuffmanCompression(MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation, int width) + public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, TiffPhotometricInterpretation photometricInterpretation, int width) : base(allocator, photometricInterpretation, width) { } @@ -27,8 +27,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression public override void Decompress(Stream stream, int byteCount, Span buffer) { bool isWhiteZero = this.PhotometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; - int whiteValue = isWhiteZero ? 0 : 1; - int blackValue = isWhiteZero ? 1 : 0; + byte whiteValue = (byte)(isWhiteZero ? 0 : 1); + byte blackValue = (byte)(isWhiteZero ? 1 : 0); using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, isModifiedHuffman: true); @@ -43,13 +43,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression { if (bitReader.IsWhiteRun) { - this.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, whiteValue); + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, whiteValue); bitsWritten += bitReader.RunLength; pixelsWritten += bitReader.RunLength; } else { - this.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, blackValue); + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, blackValue); bitsWritten += bitReader.RunLength; pixelsWritten += bitReader.RunLength; } @@ -59,11 +59,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression { bitReader.StartNewRow(); - // Write padding bytes, if necessary. + // Write padding bits, if necessary. uint pad = 8 - (bitsWritten % 8); if (pad != 8) { - this.WriteBits(buffer, (int)bitsWritten, pad, 0); + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0); bitsWritten += pad; } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs b/src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs new file mode 100644 index 0000000000..f302a3d4f7 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs @@ -0,0 +1,365 @@ +// 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; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + /// + /// Bitwriter for writing compressed CCITT T4 1D data. + /// + internal class T4BitWriter + { + 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, 0x35 }, { 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, 0x37 }, { 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 readonly MemoryAllocator memoryAllocator; + + private readonly Configuration configuration; + + private int bytePosition = 0; + + private byte bitPosition = 0; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The configuration. + public T4BitWriter(MemoryAllocator memoryAllocator, Configuration configuration) + { + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + this.bytePosition = 0; + this.bitPosition = 0; + } + + /// + /// Writes a image compressed with CCITT T4 to the stream. + /// + /// The pixel data. + /// The image to write to the stream. This has to be a bi-color image. + /// A span for converting a pixel row to gray. + /// The stream to write to. + /// The number of bytes written to the stream. + public int CompressImage(Image image, Span pixelRowAsGray, Stream stream) + where TPixel : unmanaged, IPixel + { + // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. + int maxNeededBytes = image.Width * image.Height; + IMemoryOwner compressedDataBuffer = this.memoryAllocator.Allocate(maxNeededBytes, AllocationOptions.Clean); + Span compressedData = compressedDataBuffer.GetSpan(); + + this.bytePosition = 0; + this.bitPosition = 0; + + // An EOL code is expected at the start of the data. + this.WriteCode(12, 1, compressedData); + + for (int y = 0; y < image.Height; y++) + { + bool isWhiteRun = true; + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToL8(this.configuration, pixelRow, pixelRowAsGray); + int x = 0; + while (x < image.Width) + { + uint runLength = 0; + for (int i = x; i < pixelRow.Length; i++) + { + if (isWhiteRun && pixelRowAsGray[i].PackedValue != 255) + { + break; + } + + if (isWhiteRun && pixelRowAsGray[i].PackedValue == 255) + { + runLength++; + continue; + } + + if (!isWhiteRun && pixelRowAsGray[i].PackedValue != 0) + { + break; + } + + if (!isWhiteRun && pixelRowAsGray[i].PackedValue == 0) + { + runLength++; + } + } + + bool gotTermCode = this.GetTermCode(runLength, out var code, out var codeLength, isWhiteRun); + + this.WriteCode(codeLength, code, compressedData); + + x += (int)runLength; + + isWhiteRun = !isWhiteRun; + } + + // Write EOL + this.WriteCode(12, 1, compressedData); + } + + // Write the compressed data to the stream. + stream.Write(compressedData.Slice(0, this.bytePosition)); + + return this.bytePosition; + } + + private void WriteCode(uint codeLength, uint code, Span compressedData) + { + while (codeLength > 0) + { + var bitNumber = (int) codeLength; + var 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 bool GetTermCode(uint runLength, out uint code, out uint codeLength, bool isWhiteRun) + { + if (isWhiteRun) + { + return this.GetWhiteTermCode(runLength, out code, out codeLength); + } + + return this.GetBlackTermCode(runLength, out code, out codeLength); + } + + private bool GetWhiteTermCode(uint runLength, out uint code, out uint codeLength) + { + code = 0; + codeLength = 0; + + if (WhiteLen4TermCodes.ContainsKey(runLength)) + { + code = WhiteLen4TermCodes[runLength]; + codeLength = 4; + return true; + } + + if (WhiteLen5TermCodes.ContainsKey(runLength)) + { + code = WhiteLen5TermCodes[runLength]; + codeLength = 5; + return true; + } + + if (WhiteLen6TermCodes.ContainsKey(runLength)) + { + code = WhiteLen6TermCodes[runLength]; + codeLength = 6; + return true; + } + + if (WhiteLen7TermCodes.ContainsKey(runLength)) + { + code = WhiteLen7TermCodes[runLength]; + codeLength = 7; + return true; + } + + if (WhiteLen8TermCodes.ContainsKey(runLength)) + { + code = WhiteLen8TermCodes[runLength]; + codeLength = 8; + return true; + } + + return false; + } + + private bool GetBlackTermCode(uint runLength, out uint code, out uint codeLength) + { + code = 0; + codeLength = 0; + + if (BlackLen2TermCodes.ContainsKey(runLength)) + { + code = BlackLen2TermCodes[runLength]; + codeLength = 2; + return true; + } + + if (BlackLen3TermCodes.ContainsKey(runLength)) + { + code = BlackLen3TermCodes[runLength]; + codeLength = 3; + return true; + } + + if (BlackLen4TermCodes.ContainsKey(runLength)) + { + code = BlackLen4TermCodes[runLength]; + codeLength = 4; + return true; + } + + if (BlackLen5TermCodes.ContainsKey(runLength)) + { + code = BlackLen5TermCodes[runLength]; + codeLength = 5; + return true; + } + + if (BlackLen6TermCodes.ContainsKey(runLength)) + { + code = BlackLen6TermCodes[runLength]; + codeLength = 6; + return true; + } + + if (BlackLen7TermCodes.ContainsKey(runLength)) + { + code = BlackLen7TermCodes[runLength]; + codeLength = 7; + return true; + } + + if (BlackLen8TermCodes.ContainsKey(runLength)) + { + code = BlackLen8TermCodes[runLength]; + codeLength = 8; + return true; + } + + if (BlackLen9TermCodes.ContainsKey(runLength)) + { + code = BlackLen9TermCodes[runLength]; + codeLength = 9; + return true; + } + + if (BlackLen10TermCodes.ContainsKey(runLength)) + { + code = BlackLen10TermCodes[runLength]; + codeLength = 10; + return true; + } + + if (BlackLen11TermCodes.ContainsKey(runLength)) + { + code = BlackLen11TermCodes[runLength]; + codeLength = 11; + return true; + } + + if (BlackLen12TermCodes.ContainsKey(runLength)) + { + code = BlackLen12TermCodes[runLength]; + codeLength = 12; + return true; + } + + return false; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs index 6a064962b3..9220836036 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/T4TiffCompression.cs @@ -27,8 +27,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression public override void Decompress(Stream stream, int byteCount, Span buffer) { bool isWhiteZero = this.PhotometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; - int whiteValue = isWhiteZero ? 0 : 1; - int blackValue = isWhiteZero ? 1 : 0; + byte whiteValue = (byte)(isWhiteZero ? 0 : 1); + byte blackValue = (byte)(isWhiteZero ? 1 : 0); using var bitReader = new T4BitReader(stream, byteCount, this.Allocator); @@ -42,12 +42,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression { if (bitReader.IsWhiteRun) { - this.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, whiteValue); + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, whiteValue); bitsWritten += bitReader.RunLength; } else { - this.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, blackValue); + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, blackValue); bitsWritten += bitReader.RunLength; } } @@ -58,36 +58,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression uint pad = 8 - (bitsWritten % 8); if (pad != 8) { - this.WriteBits(buffer, (int)bitsWritten, pad, 0); + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0); bitsWritten += pad; } } } } - - protected 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++; - } - } - } - - protected 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 3a0e5e6da6..11f85faa10 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionFactory.cs @@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff case TiffCompressionType.T4: return new T4TiffCompression(allocator, photometricInterpretation, width); case TiffCompressionType.HuffmanRle: - return new TiffModifiedHuffmanCompression(allocator, photometricInterpretation, width); + return new ModifiedHuffmanTiffCompression(allocator, photometricInterpretation, width); default: throw TiffThrowHelper.NotSupportedCompression(nameof(compressionType)); } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs index 536cd2c2d3..30702641a0 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs @@ -16,6 +16,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Use zlib compression. /// - Deflate + Deflate, + + /// + /// Use CCITT T4 1D compression. Note: This is only valid for bi-level images. + /// + CcittGroup3Fax, } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 75d078ccd6..0d5bacc343 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -406,6 +406,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff return (ushort)TiffCompression.Deflate; } + if (this.CompressionType == TiffEncoderCompression.CcittGroup3Fax && this.Mode == TiffEncodingMode.BiColor) + { + return (ushort)TiffCompression.CcittGroup3Fax; + } + return (ushort)TiffCompression.None; } } diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs index 586eb0a55c..eaa71c953f 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs @@ -9,6 +9,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Compression; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -383,6 +384,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff return this.WriteBiColorDeflate(image, pixelRowAsGraySpan, outputRow); } + if (compression == TiffEncoderCompression.CcittGroup3Fax) + { + var bitWriter = new T4BitWriter(this.memoryAllocator, this.configuration); + return bitWriter.CompressImage(image, pixelRowAsGraySpan, this.output); + } + int bytesWritten = 0; for (int y = 0; y < image.Height; y++) {