Browse Source

Add support for encoding modified huffman RLE

pull/1457/head
Brian Popow 6 years ago
parent
commit
c976600520
  1. 1
      src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs
  2. 42
      src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs
  3. 8
      src/ImageSharp/Formats/Tiff/README.md
  4. 5
      src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
  5. 4
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  6. 5
      src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs
  7. 11
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  8. 17
      src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
  9. 10
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

1
src/ImageSharp/Formats/Tiff/Compression/T4BitReader.cs

@ -6,6 +6,7 @@ using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression namespace SixLabors.ImageSharp.Formats.Tiff.Compression

42
src/ImageSharp/Formats/Tiff/Compression/T4BitWriter.cs

@ -5,6 +5,7 @@ using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -183,17 +184,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
private byte bitPosition; private byte bitPosition;
/// <summary>
/// The modified huffman is basically the same as CCITT T4, but without EOL markers and padding at the end of the rows.
/// </summary>
private bool useModifiedHuffman;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="T4BitWriter" /> class. /// Initializes a new instance of the <see cref="T4BitWriter" /> class.
/// </summary> /// </summary>
/// <param name="memoryAllocator">The memory allocator.</param> /// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
public T4BitWriter(MemoryAllocator memoryAllocator, Configuration configuration) /// <param name="useModifiedHuffman">Indicates if the modified huffman RLE should be used.</param>
public T4BitWriter(MemoryAllocator memoryAllocator, Configuration configuration, bool useModifiedHuffman = false)
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.configuration = configuration; this.configuration = configuration;
this.bytePosition = 0; this.bytePosition = 0;
this.bitPosition = 0; this.bitPosition = 0;
this.useModifiedHuffman = useModifiedHuffman;
} }
/// <summary> /// <summary>
@ -215,9 +223,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
this.bytePosition = 0; this.bytePosition = 0;
this.bitPosition = 0; this.bitPosition = 0;
// An EOL code is expected at the start of the data. if (!this.useModifiedHuffman)
this.WriteCode(12, 1, compressedData); {
// 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++) for (int y = 0; y < image.Height; y++)
{ {
bool isWhiteRun = true; bool isWhiteRun = true;
@ -268,6 +280,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
code = this.GetTermCode(runLength, out codeLength, isWhiteRun); code = this.GetTermCode(runLength, out codeLength, isWhiteRun);
this.WriteCode(codeLength, code, compressedData); this.WriteCode(codeLength, code, compressedData);
x += (int)runLength; x += (int)runLength;
pixelsWritten += runLength;
} }
else else
{ {
@ -275,6 +288,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
code = this.GetMakeupCode(runLength, out codeLength, isWhiteRun); code = this.GetMakeupCode(runLength, out codeLength, isWhiteRun);
this.WriteCode(codeLength, code, compressedData); this.WriteCode(codeLength, code, compressedData);
x += (int)runLength; 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 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) if (x == image.Width)
@ -296,8 +310,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
isWhiteRun = !isWhiteRun; isWhiteRun = !isWhiteRun;
} }
// Write EOL. this.WriteEndOfLine(compressedData);
this.WriteCode(12, 1, compressedData);
} }
// Write the compressed data to the stream. // Write the compressed data to the stream.
@ -306,6 +319,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
return this.bytePosition; return this.bytePosition;
} }
private void WriteEndOfLine(Span<byte> 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<byte> compressedData) private void WriteCode(uint codeLength, uint code, Span<byte> compressedData)
{ {
while (codeLength > 0) while (codeLength > 0)

8
src/ImageSharp/Formats/Tiff/README.md

@ -41,9 +41,9 @@
| |Encoder|Decoder|Comments | | |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------| |---------------------------|:-----:|:-----:|--------------------------|
|None | Y | Y | | |None | Y | Y | |
|Ccitt1D | | Y | | |Ccitt1D | Y | Y | |
|PackBits | | Y | | |PackBits | | Y | |
|CcittGroup3Fax | | Y | | |CcittGroup3Fax | Y | Y | |
|CcittGroup4Fax | | | | |CcittGroup4Fax | | | |
|Lzw | | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | |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 | |Old Jpeg | | | We should not even try to support this |
@ -55,8 +55,8 @@
| |Encoder|Decoder|Comments | | |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------| |---------------------------|:-----:|:-----:|--------------------------|
|WhiteIsZero | | Y | General + 1/4/8-bit optimised implementations | |WhiteIsZero | Y | Y | General + 1/4/8-bit optimised implementations |
|BlackIsZero | | 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 (Chunky) | Y | Y | General + Rgb888 optimised implementation |
|Rgb (Planar) | | Y | General implementation only | |Rgb (Planar) | | Y | General implementation only |
|PaletteColor | Y | Y | General implementation only | |PaletteColor | Y | Y | General implementation only |

5
src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs

@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary> /// </summary>
public enum TiffBitsPerPixel public enum TiffBitsPerPixel
{ {
/// <summary>
/// 1 bits per pixel, bi-color image. Each pixel consists of 1 bit.
/// </summary>
Pixel1 = 1,
/// <summary> /// <summary>
/// 8 bits per pixel, grayscale image. Each pixel consists of 1 byte. /// 8 bits per pixel, grayscale image. Each pixel consists of 1 byte.
/// </summary> /// </summary>

4
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -159,6 +159,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{ {
this.tiffMetaData.BitsPerPixel = TiffBitsPerPixel.Pixel8; this.tiffMetaData.BitsPerPixel = TiffBitsPerPixel.Pixel8;
} }
else if (bitsPerPixel == 1)
{
this.tiffMetaData.BitsPerPixel = TiffBitsPerPixel.Pixel1;
}
} }
/// <inheritdoc/> /// <inheritdoc/>

5
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. /// Use CCITT T4 1D compression. Note: This is only valid for bi-level images.
/// </summary> /// </summary>
CcittGroup3Fax, CcittGroup3Fax,
/// <summary>
/// Use the modified Huffman RLE. Note: This is only valid for bi-level images.
/// </summary>
ModifiedHuffman,
} }
} }

11
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -98,6 +98,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{ {
this.Mode = TiffEncodingMode.Gray; this.Mode = TiffEncodingMode.Gray;
} }
else if (this.bitsPerPixel == TiffBitsPerPixel.Pixel1)
{
this.Mode = TiffEncodingMode.BiColor;
}
} }
this.SetPhotometricInterpretation(); this.SetPhotometricInterpretation();
@ -341,7 +345,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor; this.PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor;
break; break;
case TiffEncodingMode.BiColor: 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. // The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero.
this.PhotometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero; this.PhotometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero;
@ -431,6 +435,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
return (ushort)TiffCompression.CcittGroup3Fax; return (ushort)TiffCompression.CcittGroup3Fax;
} }
if (this.CompressionType == TiffEncoderCompression.ModifiedHuffman && this.Mode == TiffEncodingMode.BiColor)
{
return (ushort)TiffCompression.Ccitt1D;
}
return (ushort)TiffCompression.None; return (ushort)TiffCompression.None;
} }
} }

17
src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs

@ -376,18 +376,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff
Span<L8> pixelRowAsGraySpan = pixelRowAsGray.GetSpan(); Span<L8> pixelRowAsGraySpan = pixelRowAsGray.GetSpan();
// Convert image to black and white. // Convert image to black and white.
using Image<TPixel> imageClone = image.Clone(); // TODO: Should we allow to skip this by the user, if its known to be black and white already?
imageClone.Mutate(img => img.BinaryDither(default(ErrorDither))); using Image<TPixel> imageBlackWhite = image.Clone();
imageBlackWhite.Mutate(img => img.BinaryDither(default(ErrorDither)));
if (compression == TiffEncoderCompression.Deflate) if (compression == TiffEncoderCompression.Deflate)
{ {
return this.WriteBiColorDeflate(image, pixelRowAsGraySpan, outputRow); return this.WriteBiColorDeflate(imageBlackWhite, pixelRowAsGraySpan, outputRow);
} }
if (compression == TiffEncoderCompression.CcittGroup3Fax) if (compression == TiffEncoderCompression.CcittGroup3Fax)
{ {
var bitWriter = new T4BitWriter(this.memoryAllocator, this.configuration); 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; int bytesWritten = 0;
@ -395,7 +402,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{ {
int bitIndex = 0; int bitIndex = 0;
int byteIndex = 0; int byteIndex = 0;
Span<TPixel> pixelRow = imageClone.GetPixelRowSpan(y); Span<TPixel> pixelRow = imageBlackWhite.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8(this.configuration, pixelRow, pixelRowAsGraySpan); PixelOperations<TPixel>.Instance.ToL8(this.configuration, pixelRow, pixelRowAsGraySpan);
for (int x = 0; x < pixelRow.Length; x++) for (int x = 0; x < pixelRow.Length; x++)
{ {

10
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -21,6 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public static readonly TheoryData<string, TiffBitsPerPixel> TiffBitsPerPixelFiles = public static readonly TheoryData<string, TiffBitsPerPixel> TiffBitsPerPixelFiles =
new TheoryData<string, TiffBitsPerPixel> new TheoryData<string, TiffBitsPerPixel>
{ {
{ TestImages.Tiff.Calliphora_BiColor, TiffBitsPerPixel.Pixel1 },
{ TestImages.Tiff.GrayscaleUncompressed, TiffBitsPerPixel.Pixel8 }, { TestImages.Tiff.GrayscaleUncompressed, TiffBitsPerPixel.Pixel8 },
{ TestImages.Tiff.RgbUncompressed, TiffBitsPerPixel.Pixel24 }, { TestImages.Tiff.RgbUncompressed, TiffBitsPerPixel.Pixel24 },
}; };
@ -85,12 +86,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Theory] [Theory]
[WithFile(TestImages.Tiff.Calliphora_BiColor, PixelTypes.Rgba32)] [WithFile(TestImages.Tiff.Calliphora_BiColor, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeBiColor_WithDeflateCompression_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeBiColor_WithDeflateCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.BiColor, TiffEncoderCompression.Deflate); where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel1, TiffEncodingMode.BiColor, TiffEncoderCompression.Deflate);
[Theory] [Theory]
[WithFile(TestImages.Tiff.Calliphora_BiColor, PixelTypes.Rgba32)] [WithFile(TestImages.Tiff.Calliphora_BiColor, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.BiColor, TiffEncoderCompression.CcittGroup3Fax); where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel1, TiffEncodingMode.BiColor, TiffEncoderCompression.CcittGroup3Fax);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_BiColor, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel1, TiffEncodingMode.BiColor, TiffEncoderCompression.ModifiedHuffman);
private static void TestTiffEncoderCore<TPixel>( private static void TestTiffEncoderCore<TPixel>(
TestImageProvider<TPixel> provider, TestImageProvider<TPixel> provider,

Loading…
Cancel
Save