Skip to content

Commit 4eb4e54

Browse files
committed
Add support for decoding tiff's encoded with LeastSignificantBitFirst compression
1 parent 7da6c33 commit 4eb4e54

File tree

11 files changed

+80
-17
lines changed

11 files changed

+80
-17
lines changed

src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ internal class ModifiedHuffmanTiffCompression : T4TiffCompression
2222
/// Initializes a new instance of the <see cref="ModifiedHuffmanTiffCompression" /> class.
2323
/// </summary>
2424
/// <param name="allocator">The memory allocator.</param>
25+
/// <param name="fillOrder">The logical order of bits within a byte.</param>
2526
/// <param name="width">The image width.</param>
2627
/// <param name="bitsPerPixel">The number of bits per pixel.</param>
2728
/// <param name="photometricInterpretation">The photometric interpretation.</param>
28-
public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation)
29-
: base(allocator, width, bitsPerPixel, FaxCompressionOptions.None, photometricInterpretation)
29+
public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, TiffFillOrder fillOrder, int width, int bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation)
30+
: base(allocator, fillOrder, width, bitsPerPixel, FaxCompressionOptions.None, photometricInterpretation)
3031
{
3132
bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero;
3233
this.whiteValue = (byte)(isWhiteZero ? 0 : 1);
@@ -36,7 +37,7 @@ public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, int width, int
3637
/// <inheritdoc/>
3738
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
3839
{
39-
using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding: false, isModifiedHuffman: true);
40+
using var bitReader = new T4BitReader(stream, this.FillOrder, byteCount, this.Allocator, eolPadding: false, isModifiedHuffman: true);
4041

4142
buffer.Clear();
4243
uint bitsWritten = 0;

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
using System.Buffers;
66
using System.Collections.Generic;
77
using System.IO;
8-
8+
using System.Runtime.CompilerServices;
9+
using SixLabors.ImageSharp.Formats.Tiff.Constants;
910
using SixLabors.ImageSharp.Memory;
1011

1112
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
@@ -20,6 +21,11 @@ internal class T4BitReader : IDisposable
2021
/// </summary>
2122
private int bitsRead;
2223

24+
/// <summary>
25+
/// The logical order of bits within a byte.
26+
/// </summary>
27+
private readonly TiffFillOrder fillOrder;
28+
2329
/// <summary>
2430
/// Current value.
2531
/// </summary>
@@ -221,12 +227,14 @@ internal class T4BitReader : IDisposable
221227
/// Initializes a new instance of the <see cref="T4BitReader" /> class.
222228
/// </summary>
223229
/// <param name="input">The compressed input stream.</param>
230+
/// <param name="fillOrder">The logical order of bits within a byte.</param>
224231
/// <param name="bytesToRead">The number of bytes to read from the stream.</param>
225232
/// <param name="allocator">The memory allocator.</param>
226233
/// <param name="eolPadding">Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false.</param>
227234
/// <param name="isModifiedHuffman">Indicates, if its the modified huffman code variation. Defaults to false.</param>
228-
public T4BitReader(Stream input, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false, bool isModifiedHuffman = false)
235+
public T4BitReader(Stream input, TiffFillOrder fillOrder, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false, bool isModifiedHuffman = false)
229236
{
237+
this.fillOrder = fillOrder;
230238
this.Data = allocator.Allocate<byte>(bytesToRead);
231239
this.ReadImageDataFromStream(input, bytesToRead);
232240

@@ -375,7 +383,7 @@ public void ReadNextRun()
375383
break;
376384
}
377385

378-
var currBit = this.ReadValue(1);
386+
uint currBit = this.ReadValue(1);
379387
this.value = (this.value << 1) | currBit;
380388

381389
if (this.IsEndOfScanLine)
@@ -816,7 +824,7 @@ private uint GetBit()
816824

817825
Span<byte> dataSpan = this.Data.GetSpan();
818826
int shift = 8 - this.bitsRead - 1;
819-
var bit = (uint)((dataSpan[(int)this.position] & (1 << shift)) != 0 ? 1 : 0);
827+
uint bit = (uint)((dataSpan[(int)this.position] & (1 << shift)) != 0 ? 1 : 0);
820828
this.bitsRead++;
821829

822830
return bit;
@@ -837,6 +845,19 @@ private void ReadImageDataFromStream(Stream input, int bytesToRead)
837845
{
838846
Span<byte> dataSpan = this.Data.GetSpan();
839847
input.Read(dataSpan, 0, bytesToRead);
848+
849+
if (this.fillOrder == TiffFillOrder.LeastSignificantBitFirst)
850+
{
851+
for (int i = 0; i < dataSpan.Length; i++)
852+
{
853+
dataSpan[i] = ReverseBits(dataSpan[i]);
854+
}
855+
}
840856
}
857+
858+
// http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith64Bits
859+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
860+
private static byte ReverseBits(byte b) =>
861+
(byte)((((b * 0x80200802UL) & 0x0884422110UL) * 0x0101010101UL) >> 32);
841862
}
842863
}

src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,27 @@ internal class T4TiffCompression : TiffBaseDecompressor
2424
/// Initializes a new instance of the <see cref="T4TiffCompression" /> class.
2525
/// </summary>
2626
/// <param name="allocator">The memory allocator.</param>
27+
/// <param name="fillOrder">The logical order of bits within a byte.</param>
2728
/// <param name="width">The image width.</param>
2829
/// <param name="bitsPerPixel">The number of bits per pixel.</param>
2930
/// <param name="faxOptions">Fax compression options.</param>
3031
/// <param name="photometricInterpretation">The photometric interpretation.</param>
31-
public T4TiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation)
32+
public T4TiffCompression(MemoryAllocator allocator, TiffFillOrder fillOrder, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation)
3233
: base(allocator, width, bitsPerPixel)
3334
{
3435
this.faxCompressionOptions = faxOptions;
36+
this.FillOrder = fillOrder;
3537

3638
bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero;
3739
this.whiteValue = (byte)(isWhiteZero ? 0 : 1);
3840
this.blackValue = (byte)(isWhiteZero ? 1 : 0);
3941
}
4042

43+
/// <summary>
44+
/// Gets the logical order of bits within a byte.
45+
/// </summary>
46+
protected TiffFillOrder FillOrder { get; }
47+
4148
/// <inheritdoc/>
4249
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
4350
{
@@ -46,8 +53,8 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, Spa
4653
TiffThrowHelper.ThrowNotSupported("TIFF CCITT 2D compression is not yet supported");
4754
}
4855

49-
var eolPadding = this.faxCompressionOptions.HasFlag(FaxCompressionOptions.EolPadding);
50-
using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding);
56+
bool eolPadding = this.faxCompressionOptions.HasFlag(FaxCompressionOptions.EolPadding);
57+
using var bitReader = new T4BitReader(stream, this.FillOrder, byteCount, this.Allocator, eolPadding);
5158

5259
buffer.Clear();
5360
uint bitsWritten = 0;

src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public static TiffBaseDecompressor Create(
1616
int width,
1717
int bitsPerPixel,
1818
TiffPredictor predictor,
19-
FaxCompressionOptions faxOptions)
19+
FaxCompressionOptions faxOptions,
20+
TiffFillOrder fillOrder)
2021
{
2122
switch (method)
2223
{
@@ -40,11 +41,11 @@ public static TiffBaseDecompressor Create(
4041

4142
case TiffDecoderCompressionType.T4:
4243
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
43-
return new T4TiffCompression(allocator, width, bitsPerPixel, faxOptions, photometricInterpretation);
44+
return new T4TiffCompression(allocator, fillOrder, width, bitsPerPixel, faxOptions, photometricInterpretation);
4445

4546
case TiffDecoderCompressionType.HuffmanRle:
4647
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
47-
return new ModifiedHuffmanTiffCompression(allocator, width, bitsPerPixel, photometricInterpretation);
48+
return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation);
4849

4950
default:
5051
throw TiffThrowHelper.NotSupportedDecompressor(nameof(method));

src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ public TiffDecoderCore(Configuration configuration, ITiffDecoderOptions options)
8585
/// </summary>
8686
public FaxCompressionOptions FaxCompressionOptions { get; set; }
8787

88+
/// <summary>
89+
/// Gets or sets the the logical order of bits within a byte.
90+
/// </summary>
91+
public TiffFillOrder FillOrder { get; set; }
92+
8893
/// <summary>
8994
/// Gets or sets the planar configuration type to use when decoding the image.
9095
/// </summary>
@@ -264,7 +269,15 @@ private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStr
264269
stripBuffers[stripIndex] = this.memoryAllocator.Allocate<byte>(uncompressedStripSize);
265270
}
266271

267-
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions);
272+
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(
273+
this.CompressionType,
274+
this.memoryAllocator,
275+
this.PhotometricInterpretation,
276+
frame.Width,
277+
bitsPerPixel,
278+
this.Predictor,
279+
this.FaxCompressionOptions,
280+
this.FillOrder);
268281

269282
TiffBasePlanarColorDecoder<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder);
270283

@@ -314,7 +327,8 @@ private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStr
314327
frame.Width,
315328
bitsPerPixel,
316329
this.Predictor,
317-
this.FaxCompressionOptions);
330+
this.FaxCompressionOptions,
331+
this.FillOrder);
318332

319333
TiffBaseColorDecoder<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.Create(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder);
320334

src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exif
3535
}
3636

3737
TiffFillOrder fillOrder = (TiffFillOrder?)exifProfile.GetValue(ExifTag.FillOrder)?.Value ?? TiffFillOrder.MostSignificantBitFirst;
38-
if (fillOrder != TiffFillOrder.MostSignificantBitFirst)
38+
if (fillOrder == TiffFillOrder.LeastSignificantBitFirst && frameMetadata.BitsPerPixel != TiffBitsPerPixel.Bit1)
3939
{
40-
TiffThrowHelper.ThrowNotSupported("The lower-order bits of the byte FillOrder is not supported.");
40+
TiffThrowHelper.ThrowNotSupported("The lower-order bits of the byte FillOrder is only supported in combination with 1bit per pixel bicolor tiff's.");
4141
}
4242

4343
if (frameMetadata.Predictor == TiffPredictor.FloatingPoint)
@@ -69,6 +69,7 @@ public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exif
6969
options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb;
7070
options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24;
7171
options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0);
72+
options.FillOrder = fillOrder;
7273

7374
options.ParseColorType(exifProfile);
7475
options.ParseCompression(frameMetadata.Compression, exifProfile);

tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ public void TiffDecoder_CanDecode_HuffmanCompressed<TPixel>(TestImageProvider<TP
221221
public void TiffDecoder_CanDecode_Fax3Compressed<TPixel>(TestImageProvider<TPixel> provider)
222222
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
223223

224+
[Theory]
225+
[WithFile(CcittFax3LowerOrderBitsFirst01, PixelTypes.Rgba32)]
226+
[WithFile(CcittFax3LowerOrderBitsFirst02, PixelTypes.Rgba32)]
227+
public void TiffDecoder_CanDecode_Compressed_LowerOrderBitsFirst<TPixel>(TestImageProvider<TPixel> provider)
228+
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
229+
224230
[Theory]
225231
[WithFile(Calliphora_RgbPackbits, PixelTypes.Rgba32)]
226232
[WithFile(RgbPackbits, PixelTypes.Rgba32)]

tests/ImageSharp.Tests/TestImages.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,9 @@ public static class Tiff
539539
public const string CcittFax3AllMakeupCodes = "Tiff/ccitt_fax3_all_makeup_codes.tiff";
540540
public const string HuffmanRleAllTermCodes = "Tiff/huffman_rle_all_terminating_codes.tiff";
541541
public const string HuffmanRleAllMakeupCodes = "Tiff/huffman_rle_all_makeup_codes.tiff";
542+
public const string CcittFax3LowerOrderBitsFirst01 = "Tiff/f8179f8f5e566349cf3583a1ff3ea95c.tiff";
543+
public const string CcittFax3LowerOrderBitsFirst02 = "Tiff/g3test.tiff";
544+
public const string HuffmanRleLowerOrderBitsFirst = "Tiff/basi3p02_huffman_rle_lowerOrderBitsFirst.tiff";
542545

543546
// Test case for an issue, that the last bits in a row got ignored.
544547
public const string HuffmanRle_basi3p02 = "Tiff/basi3p02_huffman_rle.tiff";
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:5ac3e56a93996464a579ae19cf5f8d9531e2f08db36879aaba176731c24951a5
3+
size 352
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:cf75c4b679d2449e239f228cdee6a25adc7d7b16dde3fb9061a07b2fb0699db1
3+
size 735412

0 commit comments

Comments
 (0)