Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ internal class ModifiedHuffmanTiffCompression : T4TiffCompression
/// Initializes a new instance of the <see cref="ModifiedHuffmanTiffCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="fillOrder">The logical order of bits within a byte.</param>
/// <param name="width">The image width.</param>
/// <param name="bitsPerPixel">The number of bits per pixel.</param>
/// <param name="photometricInterpretation">The photometric interpretation.</param>
public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation)
: base(allocator, width, bitsPerPixel, FaxCompressionOptions.None, photometricInterpretation)
public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, TiffFillOrder fillOrder, int width, int bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation)
: base(allocator, fillOrder, width, bitsPerPixel, FaxCompressionOptions.None, photometricInterpretation)
{
bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero;
this.whiteValue = (byte)(isWhiteZero ? 0 : 1);
Expand All @@ -36,7 +37,7 @@ public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, int width, int
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
{
using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding: false, isModifiedHuffman: true);
using var bitReader = new T4BitReader(stream, this.FillOrder, byteCount, this.Allocator, eolPadding: false, isModifiedHuffman: true);

buffer.Clear();
uint bitsWritten = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
using System.Buffers;
using System.Collections.Generic;
using System.IO;

using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;

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

/// <summary>
/// The logical order of bits within a byte.
/// </summary>
private readonly TiffFillOrder fillOrder;

/// <summary>
/// Current value.
/// </summary>
Expand Down Expand Up @@ -221,12 +227,14 @@ internal class T4BitReader : IDisposable
/// Initializes a new instance of the <see cref="T4BitReader" /> class.
/// </summary>
/// <param name="input">The compressed input stream.</param>
/// <param name="fillOrder">The logical order of bits within a byte.</param>
/// <param name="bytesToRead">The number of bytes to read from the stream.</param>
/// <param name="allocator">The memory allocator.</param>
/// <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>
/// <param name="isModifiedHuffman">Indicates, if its the modified huffman code variation. Defaults to false.</param>
public T4BitReader(Stream input, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false, bool isModifiedHuffman = false)
public T4BitReader(Stream input, TiffFillOrder fillOrder, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false, bool isModifiedHuffman = false)
{
this.fillOrder = fillOrder;
this.Data = allocator.Allocate<byte>(bytesToRead);
this.ReadImageDataFromStream(input, bytesToRead);

Expand Down Expand Up @@ -375,7 +383,7 @@ public void ReadNextRun()
break;
}

var currBit = this.ReadValue(1);
uint currBit = this.ReadValue(1);
this.value = (this.value << 1) | currBit;

if (this.IsEndOfScanLine)
Expand Down Expand Up @@ -816,7 +824,7 @@ private uint GetBit()

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

return bit;
Expand All @@ -837,6 +845,19 @@ private void ReadImageDataFromStream(Stream input, int bytesToRead)
{
Span<byte> dataSpan = this.Data.GetSpan();
input.Read(dataSpan, 0, bytesToRead);

if (this.fillOrder == TiffFillOrder.LeastSignificantBitFirst)
{
for (int i = 0; i < dataSpan.Length; i++)
{
dataSpan[i] = ReverseBits(dataSpan[i]);
}
}
}

// http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith64Bits
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte ReverseBits(byte b) =>
(byte)((((b * 0x80200802UL) & 0x0884422110UL) * 0x0101010101UL) >> 32);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,33 @@ internal class T4TiffCompression : TiffBaseDecompressor

private readonly byte blackValue;

private readonly int width;

/// <summary>
/// Initializes a new instance of the <see cref="T4TiffCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="fillOrder">The logical order of bits within a byte.</param>
/// <param name="width">The image width.</param>
/// <param name="bitsPerPixel">The number of bits per pixel.</param>
/// <param name="faxOptions">Fax compression options.</param>
/// <param name="photometricInterpretation">The photometric interpretation.</param>
public T4TiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation)
public T4TiffCompression(MemoryAllocator allocator, TiffFillOrder fillOrder, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation)
: base(allocator, width, bitsPerPixel)
{
this.faxCompressionOptions = faxOptions;

this.FillOrder = fillOrder;
this.width = width;
bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero;
this.whiteValue = (byte)(isWhiteZero ? 0 : 1);
this.blackValue = (byte)(isWhiteZero ? 1 : 0);
}

/// <summary>
/// Gets the logical order of bits within a byte.
/// </summary>
protected TiffFillOrder FillOrder { get; }

/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
{
Expand All @@ -46,27 +55,22 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, Spa
TiffThrowHelper.ThrowNotSupported("TIFF CCITT 2D compression is not yet supported");
}

var eolPadding = this.faxCompressionOptions.HasFlag(FaxCompressionOptions.EolPadding);
using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding);
bool eolPadding = this.faxCompressionOptions.HasFlag(FaxCompressionOptions.EolPadding);
using var bitReader = new T4BitReader(stream, this.FillOrder, byteCount, this.Allocator, eolPadding);

buffer.Clear();
uint bitsWritten = 0;
uint pixelWritten = 0;
while (bitReader.HasMoreData)
{
bitReader.ReadNextRun();

if (bitReader.RunLength > 0)
{
if (bitReader.IsWhiteRun)
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue);
bitsWritten += bitReader.RunLength;
}
else
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue);
bitsWritten += bitReader.RunLength;
}
this.WritePixelRun(buffer, bitReader, bitsWritten);

bitsWritten += bitReader.RunLength;
pixelWritten += bitReader.RunLength;
}

if (bitReader.IsEndOfScanLine)
Expand All @@ -78,8 +82,29 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, Spa
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0);
bitsWritten += pad;
}

pixelWritten = 0;
}
}

// Edge case for when we are at the last byte, but there are still some unwritten pixels left.
if (pixelWritten > 0 && pixelWritten < this.width)
{
bitReader.ReadNextRun();
this.WritePixelRun(buffer, bitReader, bitsWritten);
}
}

private void WritePixelRun(Span<byte> buffer, T4BitReader bitReader, uint bitsWritten)
{
if (bitReader.IsWhiteRun)
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue);
}
else
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue);
}
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public static TiffBaseDecompressor Create(
int width,
int bitsPerPixel,
TiffPredictor predictor,
FaxCompressionOptions faxOptions)
FaxCompressionOptions faxOptions,
TiffFillOrder fillOrder)
{
switch (method)
{
Expand All @@ -40,11 +41,11 @@ public static TiffBaseDecompressor Create(

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

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

default:
throw TiffThrowHelper.NotSupportedDecompressor(nameof(method));
Expand Down
4 changes: 2 additions & 2 deletions src/ImageSharp/Formats/Tiff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

## Implementation Status

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

### Deviations from the TIFF spec (to be fixed)
Expand Down Expand Up @@ -81,7 +81,7 @@
|Thresholding | | | |
|CellWidth | | | |
|CellLength | | | |
|FillOrder | | - | Ignore. In practice is very uncommon, and is not recommended. |
|FillOrder | | Y | |
|ImageDescription | Y | Y | |
|Make | Y | Y | |
|Model | Y | Y | |
Expand Down
18 changes: 16 additions & 2 deletions src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public TiffDecoderCore(Configuration configuration, ITiffDecoderOptions options)
/// </summary>
public FaxCompressionOptions FaxCompressionOptions { get; set; }

/// <summary>
/// Gets or sets the the logical order of bits within a byte.
/// </summary>
public TiffFillOrder FillOrder { get; set; }

/// <summary>
/// Gets or sets the planar configuration type to use when decoding the image.
/// </summary>
Expand Down Expand Up @@ -264,7 +269,15 @@ private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStr
stripBuffers[stripIndex] = this.memoryAllocator.Allocate<byte>(uncompressedStripSize);
}

using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions);
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(
this.CompressionType,
this.memoryAllocator,
this.PhotometricInterpretation,
frame.Width,
bitsPerPixel,
this.Predictor,
this.FaxCompressionOptions,
this.FillOrder);

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

Expand Down Expand Up @@ -314,7 +327,8 @@ private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStr
frame.Width,
bitsPerPixel,
this.Predictor,
this.FaxCompressionOptions);
this.FaxCompressionOptions,
this.FillOrder);

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

Expand Down
5 changes: 3 additions & 2 deletions src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exif
}

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

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

options.ParseColorType(exifProfile);
options.ParseCompression(frameMetadata.Compression, exifProfile);
Expand Down
5 changes: 5 additions & 0 deletions tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,11 @@ public void TiffDecoder_CanDecode_HuffmanCompressed<TPixel>(TestImageProvider<TP
public void TiffDecoder_CanDecode_Fax3Compressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);

[Theory]
[WithFile(CcittFax3LowerOrderBitsFirst, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_Compressed_LowerOrderBitsFirst<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);

[Theory]
[WithFile(Calliphora_RgbPackbits, PixelTypes.Rgba32)]
[WithFile(RgbPackbits, PixelTypes.Rgba32)]
Expand Down
6 changes: 5 additions & 1 deletion tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,11 @@ public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPix
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldDeflate, TiffBitsPerPixel.Bit24, TiffCompression.None)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldJpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)]
public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffBitsPerPixel expectedBitsPerPixel, TiffCompression expectedCompression)
public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(
TiffPhotometricInterpretation? photometricInterpretation,
TiffCompression compression,
TiffBitsPerPixel expectedBitsPerPixel,
TiffCompression expectedCompression)
{
// arrange
var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
[Collection("RunSerial")]
public class ArrayPoolMemoryAllocatorTests
{
private const int MaxPooledBufferSizeInBytes = 2048;
Expand Down Expand Up @@ -56,19 +57,14 @@ public void WhenPassedOnly_MaxPooledBufferSizeInBytes_SmallerThresholdValueIsAut

[Fact]
public void When_PoolSelectorThresholdInBytes_IsGreaterThan_MaxPooledBufferSizeInBytes_ExceptionIsThrown()
{
Assert.ThrowsAny<Exception>(() => new ArrayPoolMemoryAllocator(100, 200));
}
=> Assert.ThrowsAny<Exception>(() => new ArrayPoolMemoryAllocator(100, 200));
}

[Theory]
[InlineData(32)]
[InlineData(512)]
[InlineData(MaxPooledBufferSizeInBytes - 1)]
public void SmallBuffersArePooled_OfByte(int size)
{
Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer<byte>(size));
}
public void SmallBuffersArePooled_OfByte(int size) => Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer<byte>(size));

[Theory]
[InlineData(128 * 1024 * 1024)]
Expand Down
Loading