Skip to content

Commit 25dadda

Browse files
authored
Merge pull request #1725 from SixLabors/bp/tiffLeastSignificantBitFirst
Add support for decoding tiff's encoded with LeastSignificantBitFirst
2 parents 6a388c8 + 9fbb3f4 commit 25dadda

18 files changed

+125
-47
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: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,33 @@ internal class T4TiffCompression : TiffBaseDecompressor
2020

2121
private readonly byte blackValue;
2222

23+
private readonly int width;
24+
2325
/// <summary>
2426
/// Initializes a new instance of the <see cref="T4TiffCompression" /> class.
2527
/// </summary>
2628
/// <param name="allocator">The memory allocator.</param>
29+
/// <param name="fillOrder">The logical order of bits within a byte.</param>
2730
/// <param name="width">The image width.</param>
2831
/// <param name="bitsPerPixel">The number of bits per pixel.</param>
2932
/// <param name="faxOptions">Fax compression options.</param>
3033
/// <param name="photometricInterpretation">The photometric interpretation.</param>
31-
public T4TiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation)
34+
public T4TiffCompression(MemoryAllocator allocator, TiffFillOrder fillOrder, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation)
3235
: base(allocator, width, bitsPerPixel)
3336
{
3437
this.faxCompressionOptions = faxOptions;
35-
38+
this.FillOrder = fillOrder;
39+
this.width = width;
3640
bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero;
3741
this.whiteValue = (byte)(isWhiteZero ? 0 : 1);
3842
this.blackValue = (byte)(isWhiteZero ? 1 : 0);
3943
}
4044

45+
/// <summary>
46+
/// Gets the logical order of bits within a byte.
47+
/// </summary>
48+
protected TiffFillOrder FillOrder { get; }
49+
4150
/// <inheritdoc/>
4251
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
4352
{
@@ -46,27 +55,22 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, Spa
4655
TiffThrowHelper.ThrowNotSupported("TIFF CCITT 2D compression is not yet supported");
4756
}
4857

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

5261
buffer.Clear();
5362
uint bitsWritten = 0;
63+
uint pixelWritten = 0;
5464
while (bitReader.HasMoreData)
5565
{
5666
bitReader.ReadNextRun();
5767

5868
if (bitReader.RunLength > 0)
5969
{
60-
if (bitReader.IsWhiteRun)
61-
{
62-
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue);
63-
bitsWritten += bitReader.RunLength;
64-
}
65-
else
66-
{
67-
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue);
68-
bitsWritten += bitReader.RunLength;
69-
}
70+
this.WritePixelRun(buffer, bitReader, bitsWritten);
71+
72+
bitsWritten += bitReader.RunLength;
73+
pixelWritten += bitReader.RunLength;
7074
}
7175

7276
if (bitReader.IsEndOfScanLine)
@@ -78,8 +82,29 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, Spa
7882
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0);
7983
bitsWritten += pad;
8084
}
85+
86+
pixelWritten = 0;
8187
}
8288
}
89+
90+
// Edge case for when we are at the last byte, but there are still some unwritten pixels left.
91+
if (pixelWritten > 0 && pixelWritten < this.width)
92+
{
93+
bitReader.ReadNextRun();
94+
this.WritePixelRun(buffer, bitReader, bitsWritten);
95+
}
96+
}
97+
98+
private void WritePixelRun(Span<byte> buffer, T4BitReader bitReader, uint bitsWritten)
99+
{
100+
if (bitReader.IsWhiteRun)
101+
{
102+
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue);
103+
}
104+
else
105+
{
106+
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue);
107+
}
83108
}
84109

85110
/// <inheritdoc/>

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/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
## Implementation Status
2727

28-
- The Decoder and Encoder currently only supports a single frame per image.
28+
- The Decoder currently only supports a single frame per image.
2929
- Some compression formats are not yet supported. See the list below.
3030

3131
### Deviations from the TIFF spec (to be fixed)
@@ -81,7 +81,7 @@
8181
|Thresholding | | | |
8282
|CellWidth | | | |
8383
|CellLength | | | |
84-
|FillOrder | | - | Ignore. In practice is very uncommon, and is not recommended. |
84+
|FillOrder | | Y | |
8585
|ImageDescription | Y | Y | |
8686
|Make | Y | Y | |
8787
|Model | Y | Y | |

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.Configuration, 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ public void TiffDecoder_CanDecode_HuffmanCompressed<TPixel>(TestImageProvider<TP
279279
public void TiffDecoder_CanDecode_Fax3Compressed<TPixel>(TestImageProvider<TPixel> provider)
280280
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
281281

282+
[Theory]
283+
[WithFile(CcittFax3LowerOrderBitsFirst, PixelTypes.Rgba32)]
284+
public void TiffDecoder_CanDecode_Compressed_LowerOrderBitsFirst<TPixel>(TestImageProvider<TPixel> provider)
285+
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
286+
282287
[Theory]
283288
[WithFile(Calliphora_RgbPackbits, PixelTypes.Rgba32)]
284289
[WithFile(RgbPackbits, PixelTypes.Rgba32)]

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,11 @@ public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPix
117117
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)]
118118
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldDeflate, TiffBitsPerPixel.Bit24, TiffCompression.None)]
119119
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldJpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)]
120-
public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffBitsPerPixel expectedBitsPerPixel, TiffCompression expectedCompression)
120+
public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(
121+
TiffPhotometricInterpretation? photometricInterpretation,
122+
TiffCompression compression,
123+
TiffBitsPerPixel expectedBitsPerPixel,
124+
TiffCompression expectedCompression)
121125
{
122126
// arrange
123127
var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression };

tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
1313
{
14+
[Collection("RunSerial")]
1415
public class ArrayPoolMemoryAllocatorTests
1516
{
1617
private const int MaxPooledBufferSizeInBytes = 2048;
@@ -56,19 +57,14 @@ public void WhenPassedOnly_MaxPooledBufferSizeInBytes_SmallerThresholdValueIsAut
5657

5758
[Fact]
5859
public void When_PoolSelectorThresholdInBytes_IsGreaterThan_MaxPooledBufferSizeInBytes_ExceptionIsThrown()
59-
{
60-
Assert.ThrowsAny<Exception>(() => new ArrayPoolMemoryAllocator(100, 200));
61-
}
60+
=> Assert.ThrowsAny<Exception>(() => new ArrayPoolMemoryAllocator(100, 200));
6261
}
6362

6463
[Theory]
6564
[InlineData(32)]
6665
[InlineData(512)]
6766
[InlineData(MaxPooledBufferSizeInBytes - 1)]
68-
public void SmallBuffersArePooled_OfByte(int size)
69-
{
70-
Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer<byte>(size));
71-
}
67+
public void SmallBuffersArePooled_OfByte(int size) => Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer<byte>(size));
7268

7369
[Theory]
7470
[InlineData(128 * 1024 * 1024)]

0 commit comments

Comments
 (0)