Browse Source

Add support for Deflate (and OldDeflate) compression

pull/1570/head
Andrew Wilkinson 9 years ago
parent
commit
d773963d74
  1. 60
      src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs
  2. 5
      src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs
  3. 10
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  4. 177
      src/ImageSharp/Formats/Tiff/Utils/SubStream.cs
  5. 10
      src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs
  6. 48
      tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs
  7. 8
      tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs
  8. 327
      tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Utils/SubStreamTests.cs

60
src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs

@ -0,0 +1,60 @@
// <copyright file="DeflateTiffCompression.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Tiff
{
using System;
using System.Buffers;
using System.IO;
using System.IO.Compression;
using System.Runtime.CompilerServices;
/// <summary>
/// Class to handle cases where TIFF image data is compressed using Deflate compression.
/// </summary>
/// <remarks>
/// Note that the 'OldDeflate' compression type is identical to the 'Deflate' compression type.
/// </remarks>
internal static class DeflateTiffCompression
{
/// <summary>
/// Decompresses image data into the supplied buffer.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to read image data from.</param>
/// <param name="byteCount">The number of bytes to read from the input stream.</param>
/// <param name="buffer">The output buffer for uncompressed data.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Decompress(Stream stream, int byteCount, byte[] buffer)
{
// Read the 'zlib' header information
int cmf = stream.ReadByte();
int flag = stream.ReadByte();
if ((cmf & 0x0f) != 8)
{
throw new Exception($"Bad compression method for ZLIB header: cmf={cmf}");
}
// If the 'fdict' flag is set then we should skip the next four bytes
bool fdict = (flag & 32) != 0;
if (fdict)
{
stream.ReadByte();
stream.ReadByte();
stream.ReadByte();
stream.ReadByte();
}
// The subsequent data is the Deflate compressed data (except for the last four bytes of checksum)
int headerLength = fdict ? 10 : 6;
SubStream subStream = new SubStream(stream, byteCount - headerLength);
using (DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress, true))
{
deflateStream.ReadFull(buffer);
}
}
}
}

5
src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs

@ -19,5 +19,10 @@ namespace ImageSharp.Formats.Tiff
/// Image data is compressed using PackBits compression.
/// </summary>
PackBits = 1,
/// <summary>
/// Image data is compressed using Deflate compression.
/// </summary>
Deflate = 2,
}
}

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

@ -268,6 +268,13 @@ namespace ImageSharp.Formats
break;
}
case TiffCompression.Deflate:
case TiffCompression.OldDeflate:
{
this.CompressionType = TiffCompressionType.Deflate;
break;
}
default:
{
throw new NotSupportedException("The specified TIFF compression format is not supported.");
@ -482,6 +489,9 @@ namespace ImageSharp.Formats
case TiffCompressionType.PackBits:
PackBitsTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer);
break;
case TiffCompressionType.Deflate:
DeflateTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer);
break;
default:
throw new InvalidOperationException();
}

177
src/ImageSharp/Formats/Tiff/Utils/SubStream.cs

@ -0,0 +1,177 @@
// <copyright file="SubStream.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Tiff
{
using System;
using System.IO;
/// <summary>
/// Utility class to encapsulate a sub-portion of another <see cref="Stream"/>.
/// </summary>
/// <remarks>
/// Note that disposing of the <see cref="SubStream"/> does not dispose the underlying
/// <see cref="Stream"/>.
/// </remarks>
internal class SubStream : Stream
{
private Stream innerStream;
private long offset;
private long endOffset;
private long length;
/// <summary>
/// Initializes a new instance of the <see cref="Stream"/> class.
/// </summary>
/// <param name="innerStream">The underlying <see cref="Stream"/> to wrap.</param>
/// <param name="length">The length of the sub-stream.</param>
/// <remarks>
/// Note that calling the sub-stream with start from the current offset of the
/// underlying <see cref="Stream"/>
/// </remarks>
public SubStream(Stream innerStream, long length)
{
this.innerStream = innerStream;
this.offset = this.innerStream.Position;
this.endOffset = this.offset + length;
this.length = length;
}
/// <summary>
/// Initializes a new instance of the <see cref="Stream"/> class.
/// </summary>
/// <param name="innerStream">The underlying <see cref="Stream"/> to wrap.</param>
/// <param name="offset">The offset of the sub-stream within the underlying <see cref="Stream"/>.</param>
/// <param name="length">The length of the sub-stream.</param>
/// <remarks>
/// Note that calling the constructor will immediately move the underlying
/// <see cref="Stream"/> to the specified offset.
/// </remarks>
public SubStream(Stream innerStream, long offset, long length)
{
this.innerStream = innerStream;
this.offset = offset;
this.endOffset = offset + length;
this.length = length;
innerStream.Seek(offset, SeekOrigin.Begin);
}
/// <inheritdoc/>
public override bool CanRead
{
get
{
return true;
}
}
/// <inheritdoc/>
public override bool CanWrite
{
get
{
return false;
}
}
/// <inheritdoc/>
public override bool CanSeek
{
get
{
return this.innerStream.CanSeek;
}
}
/// <inheritdoc/>
public override long Length
{
get
{
return this.length;
}
}
/// <inheritdoc/>
public override long Position
{
get
{
return this.innerStream.Position - this.offset;
}
set
{
this.Seek(value, SeekOrigin.Begin);
}
}
/// <inheritdoc/>
public override void Flush()
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
long bytesRemaining = this.endOffset - this.innerStream.Position;
if (bytesRemaining < count)
{
count = (int)bytesRemaining;
}
return this.innerStream.Read(buffer, offset, count);
}
/// <inheritdoc/>
public override int ReadByte()
{
if (this.innerStream.Position < this.endOffset)
{
return this.innerStream.ReadByte();
}
else
{
return -1;
}
}
/// <inheritdoc/>
public override void Write(byte[] array, int offset, int count)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void WriteByte(byte value)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Current:
return this.innerStream.Seek(offset, SeekOrigin.Current) - this.offset;
case SeekOrigin.Begin:
return this.innerStream.Seek(this.offset + offset, SeekOrigin.Begin) - this.offset;
case SeekOrigin.End:
return this.innerStream.Seek(this.endOffset - offset, SeekOrigin.Begin) - this.offset;
default:
throw new ArgumentException("Invalid seek origin.");
}
}
/// <inheritdoc/>
public override void SetLength(long value)
{
throw new NotSupportedException();
}
}
}

10
src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs

@ -34,5 +34,15 @@ namespace ImageSharp.Formats.Tiff
count -= bytesRead;
}
}
/// <summary>
/// Reads all bytes from the input stream into a buffer until the end of stream or the buffer is full.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">A buffer to store the retrieved data.</param>
public static void ReadFull(this Stream stream, byte[] buffer)
{
ReadFull(stream, buffer, buffer.Length);
}
}
}

48
tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs

@ -0,0 +1,48 @@
// <copyright file="DeflateTiffCompressionTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests
{
using System.IO;
using Xunit;
using ImageSharp.Formats;
using ImageSharp.Formats.Tiff;
public class DeflateTiffCompressionTests
{
[Theory]
[InlineData(new byte[] { })]
[InlineData(new byte[] { 42 })] // One byte
[InlineData(new byte[] { 42, 16, 128, 53, 96, 218, 7, 64, 3, 4, 97 })] // Random bytes
[InlineData(new byte[] { 1, 2, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 3, 4 })] // Repeated bytes
[InlineData(new byte[] { 1, 2, 42, 53, 42, 53, 42, 53, 42, 53, 42, 53, 3, 4 })] // Repeated sequence
public void Decompress_ReadsData(byte[] data)
{
using (Stream stream = CreateCompressedStream(data))
{
byte[] buffer = new byte[data.Length];
DeflateTiffCompression.Decompress(stream, data.Length, buffer);
Assert.Equal(data, buffer);
}
}
private static Stream CreateCompressedStream(byte[] data)
{
Stream compressedStream = new MemoryStream();
using (Stream uncompressedStream = new MemoryStream(data),
deflateStream = new ZlibDeflateStream(compressedStream, 6))
{
uncompressedStream.CopyTo(deflateStream);
}
compressedStream.Seek(0, SeekOrigin.Begin);
return compressedStream;
}
}
}

8
tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs

@ -125,6 +125,10 @@ namespace ImageSharp.Tests
[InlineData(true, TiffCompression.None, TiffCompressionType.None)]
[InlineData(false, TiffCompression.PackBits, TiffCompressionType.PackBits)]
[InlineData(true, TiffCompression.PackBits, TiffCompressionType.PackBits)]
[InlineData(false, TiffCompression.Deflate, TiffCompressionType.Deflate)]
[InlineData(true, TiffCompression.Deflate, TiffCompressionType.Deflate)]
[InlineData(false, TiffCompression.OldDeflate, TiffCompressionType.Deflate)]
[InlineData(true, TiffCompression.OldDeflate, TiffCompressionType.Deflate)]
public void ReadImageFormat_DeterminesCorrectCompressionImplementation(bool isLittleEndian, ushort compression, int compressionType)
{
Stream stream = CreateTiffGenIfd()
@ -142,23 +146,19 @@ namespace ImageSharp.Tests
[InlineData(false, TiffCompression.Ccitt1D)]
[InlineData(false, TiffCompression.CcittGroup3Fax)]
[InlineData(false, TiffCompression.CcittGroup4Fax)]
[InlineData(false, TiffCompression.Deflate)]
[InlineData(false, TiffCompression.ItuTRecT43)]
[InlineData(false, TiffCompression.ItuTRecT82)]
[InlineData(false, TiffCompression.Jpeg)]
[InlineData(false, TiffCompression.Lzw)]
[InlineData(false, TiffCompression.OldDeflate)]
[InlineData(false, TiffCompression.OldJpeg)]
[InlineData(false, 999)]
[InlineData(true, TiffCompression.Ccitt1D)]
[InlineData(true, TiffCompression.CcittGroup3Fax)]
[InlineData(true, TiffCompression.CcittGroup4Fax)]
[InlineData(true, TiffCompression.Deflate)]
[InlineData(true, TiffCompression.ItuTRecT43)]
[InlineData(true, TiffCompression.ItuTRecT82)]
[InlineData(true, TiffCompression.Jpeg)]
[InlineData(true, TiffCompression.Lzw)]
[InlineData(true, TiffCompression.OldDeflate)]
[InlineData(true, TiffCompression.OldJpeg)]
[InlineData(true, 999)]
public void ReadImageFormat_ThrowsExceptionForUnsupportedCompression(bool isLittleEndian, ushort compression)

327
tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Utils/SubStreamTests.cs

@ -0,0 +1,327 @@
// <copyright file="SubStreamTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests
{
using System;
using System.IO;
using Xunit;
using ImageSharp.Formats.Tiff;
public class SubStreamTests
{
[Fact]
public void Constructor_PositionsStreamCorrectly_WithSpecifiedOffset()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
innerStream.Position = 2;
SubStream stream = new SubStream(innerStream, 4, 6);
Assert.Equal(0, stream.Position);
Assert.Equal(6, stream.Length);
Assert.Equal(4, innerStream.Position);
}
[Fact]
public void Constructor_PositionsStreamCorrectly_WithCurrentOffset()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
innerStream.Position = 2;
SubStream stream = new SubStream(innerStream, 6);
Assert.Equal(0, stream.Position);
Assert.Equal(6, stream.Length);
Assert.Equal(2, innerStream.Position);
}
[Fact]
public void CanRead_ReturnsTrue()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
Assert.True(stream.CanRead);
}
[Fact]
public void CanWrite_ReturnsFalse()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
Assert.False(stream.CanWrite);
}
[Fact]
public void CanSeek_ReturnsTrue()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
Assert.True(stream.CanSeek);
}
[Fact]
public void Length_ReturnsTheConstrainedLength()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
Assert.Equal(6, stream.Length);
}
[Fact]
public void Position_ReturnsZeroBeforeReading()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
Assert.Equal(0, stream.Position);
Assert.Equal(2, innerStream.Position);
}
[Fact]
public void Position_ReturnsPositionAfterReading()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Read(new byte[2], 0, 2);
Assert.Equal(2, stream.Position);
Assert.Equal(4, innerStream.Position);
}
[Fact]
public void Position_ReturnsPositionAfterReadingTwice()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Read(new byte[2], 0, 2);
stream.Read(new byte[2], 0, 2);
Assert.Equal(4, stream.Position);
Assert.Equal(6, innerStream.Position);
}
[Fact]
public void Position_SettingPropertySeeksToNewPosition()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 3;
Assert.Equal(3, stream.Position);
Assert.Equal(5, innerStream.Position);
}
[Fact]
public void Flush_ThrowsNotSupportedException()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
Assert.Throws<NotSupportedException>(() => stream.Flush());
}
[Fact]
public void Read_Reads_FromStartOfSubStream()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
byte[] buffer = new byte[3];
var result = stream.Read(buffer, 0, 3);
Assert.Equal(new byte[] { 3, 4, 5 }, buffer);
Assert.Equal(3, result);
}
[Theory]
[InlineData(2, SeekOrigin.Begin)]
[InlineData(1, SeekOrigin.Current)]
[InlineData(4, SeekOrigin.End)]
public void Read_Reads_FromMiddleOfSubStream(long offset, SeekOrigin origin)
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 1;
stream.Seek(offset, origin);
byte[] buffer = new byte[3];
var result = stream.Read(buffer, 0, 3);
Assert.Equal(new byte[] { 5, 6, 7 }, buffer);
Assert.Equal(3, result);
}
[Theory]
[InlineData(3, SeekOrigin.Begin)]
[InlineData(2, SeekOrigin.Current)]
[InlineData(3, SeekOrigin.End)]
public void Read_Reads_FromEndOfSubStream(long offset, SeekOrigin origin)
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 1;
stream.Seek(offset, origin);
byte[] buffer = new byte[3];
var result = stream.Read(buffer, 0, 3);
Assert.Equal(new byte[] { 6, 7, 8 }, buffer);
Assert.Equal(3, result);
}
[Theory]
[InlineData(4, SeekOrigin.Begin)]
[InlineData(3, SeekOrigin.Current)]
[InlineData(2, SeekOrigin.End)]
public void Read_Reads_FromBeyondEndOfSubStream(long offset, SeekOrigin origin)
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 1;
stream.Seek(offset, origin);
byte[] buffer = new byte[3];
var result = stream.Read(buffer, 0, 3);
Assert.Equal(new byte[] { 7, 8, 0 }, buffer);
Assert.Equal(2, result);
}
[Fact]
public void ReadByte_Reads_FromStartOfSubStream()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
var result = stream.ReadByte();
Assert.Equal(3, result);
}
[Fact]
public void ReadByte_Reads_FromMiddleOfSubStream()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 3;
var result = stream.ReadByte();
Assert.Equal(6, result);
}
[Fact]
public void ReadByte_Reads_FromEndOfSubStream()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 5;
var result = stream.ReadByte();
Assert.Equal(8, result);
}
[Fact]
public void ReadByte_Reads_FromBeyondEndOfSubStream()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 5;
stream.ReadByte();
var result = stream.ReadByte();
Assert.Equal(-1, result);
}
[Fact]
public void Write_ThrowsNotSupportedException()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
Assert.Throws<NotSupportedException>(() => stream.Write(new byte[] { 1, 2 }, 0, 2));
}
[Fact]
public void WriteByte_ThrowsNotSupportedException()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
Assert.Throws<NotSupportedException>(() => stream.WriteByte(42));
}
[Fact]
public void Seek_MovesToNewPosition_FromBegin()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 1;
long result = stream.Seek(2, SeekOrigin.Begin);
Assert.Equal(2, result);
Assert.Equal(2, stream.Position);
Assert.Equal(4, innerStream.Position);
}
[Fact]
public void Seek_MovesToNewPosition_FromCurrent()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 1;
long result = stream.Seek(2, SeekOrigin.Current);
Assert.Equal(3, result);
Assert.Equal(3, stream.Position);
Assert.Equal(5, innerStream.Position);
}
[Fact]
public void Seek_MovesToNewPosition_FromEnd()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
stream.Position = 1;
long result = stream.Seek(2, SeekOrigin.End);
Assert.Equal(4, result);
Assert.Equal(4, stream.Position);
Assert.Equal(6, innerStream.Position);
}
[Fact]
public void Seek_ThrowsException_WithInvalidOrigin()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
var e = Assert.Throws<ArgumentException>(() => stream.Seek(2, (SeekOrigin)99));
Assert.Equal("Invalid seek origin.", e.Message);
}
public void SetLength_ThrowsNotSupportedException()
{
Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
SubStream stream = new SubStream(innerStream, 2, 6);
Assert.Throws<NotSupportedException>(() => stream.SetLength(5));
}
}
}
Loading…
Cancel
Save