Browse Source

Reworked lzw encoder with a tree based approach based on a java implementation

pull/1570/head
Brian Popow 6 years ago
parent
commit
4bf3d16789
  1. 2
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  2. 571
      src/ImageSharp/Formats/Tiff/Utils/TiffLzwEncoder.cs
  3. 6
      src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
  4. 2
      tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs
  5. 18
      tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs

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

@ -297,7 +297,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
var rowsPerStrip = new ExifLong(ExifTagValue.RowsPerStrip) var rowsPerStrip = new ExifLong(ExifTagValue.RowsPerStrip)
{ {
// TODO: all rows in one strip for the start // All rows in one strip.
Value = (uint)image.Height Value = (uint)image.Height
}; };

571
src/ImageSharp/Formats/Tiff/Utils/TiffLzwEncoder.cs

@ -9,28 +9,46 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
{ {
/*
This implementation is a port of a java tiff encoder by Harald Kuhr: https://github.com/haraldk/TwelveMonkeys
Original licence:
BSD 3-Clause License
* Copyright (c) 2015, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
** Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/// <summary> /// <summary>
/// Encodes and compresses the image data using dynamic Lempel-Ziv compression. /// Encodes and compresses the image data using dynamic Lempel-Ziv compression.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. K Weiner 12/00
/// <para>
/// GIFCOMPR.C - GIF Image compression routines
/// </para>
/// <para>
/// Lempel-Ziv compression based on 'compress'. GIF modifications by
/// David Rowley (mgardi@watdcsu.waterloo.edu)
/// </para>
/// GIF Image compression - modified 'compress'
/// <para>
/// Based on: compress.c - File compression ala IEEE Computer, June 1984.
/// By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
/// Jim McKie (decvax!mcvax!jim)
/// Steve Davies (decvax!vax135!petsd!peora!srd)
/// Ken Turkowski (decvax!decwrl!turtlevax!ken)
/// James A. Woods (decvax!ihnp4!ames!jaw)
/// Joe Orost (decvax!vax135!petsd!joe)
/// </para>
/// <para> /// <para>
/// This code is based on the <see cref="LzwEncoder"/> used for GIF encoding. There is potential /// This code is based on the <see cref="LzwEncoder"/> used for GIF encoding. There is potential
/// for a shared implementation. Differences between the GIF and TIFF implementations of the LZW /// for a shared implementation. Differences between the GIF and TIFF implementations of the LZW
@ -42,173 +60,54 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
/// </remarks> /// </remarks>
internal sealed class TiffLzwEncoder : IDisposable internal sealed class TiffLzwEncoder : IDisposable
{ {
/// <summary> // Clear: Re-initialize tables.
/// The end-of-file marker private static readonly int ClearCode = 256;
/// </summary>
private const int Eof = -1;
/// <summary>
/// The maximum number of bits.
/// </summary>
private const int Bits = 12;
/// <summary>
/// 80% occupancy
/// </summary>
private const int HashSize = 5003;
/// <summary>
/// Mask used when shifting pixel values
/// </summary>
private static readonly int[] Masks =
{
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF,
0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF
};
/// <summary>
/// The working pixel array.
/// </summary>
private readonly IMemoryOwner<byte> pixelArray;
/// <summary> // End of Information.
/// The initial code size. private static readonly int EoiCode = 257;
/// </summary>
private readonly int initialCodeSize;
/// <summary> private static readonly int MinBits = 9;
/// The hash table. private static readonly int MaxBits = 12;
/// </summary>
private readonly IMemoryOwner<int> hashTable;
/// <summary> private static readonly int TableSize = 1 << MaxBits;
/// The code table.
/// </summary>
private readonly IMemoryOwner<int> codeTable;
/// <summary> private readonly IMemoryOwner<byte> data;
/// Define the storage for the packet accumulator.
/// </summary>
private readonly byte[] accumulators = new byte[256];
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
/// <summary> // A child is made up of a parent (or prefix) code plus a suffix byte
/// The current pixel // and siblings are strings with a common parent(or prefix) and different suffix bytes.
/// </summary> private readonly IMemoryOwner<int> children;
private int currentPixel;
/// <summary> private readonly IMemoryOwner<int> siblings;
/// Number of bits/code
/// </summary>
private int bitCount;
/// <summary> private readonly IMemoryOwner<int> suffixes;
/// User settable max # bits/code
/// </summary>
private int maxbits = Bits;
/// <summary> // Initial setup
/// maximum code, given bitCount private int parent;
/// </summary> private int bitsPerCode;
private int maxcode; private int nextValidCode;
private int maxCode;
/// <summary> // Buffer for partial codes
/// should NEVER generate this code private int bits;
/// </summary> private int bitPos;
private int maxmaxcode = 1 << Bits; private int bufferPosition;
/// <summary>
/// For dynamic table sizing
/// </summary>
private int hsize = HashSize;
/// <summary>
/// First unused entry
/// </summary>
private int freeEntry;
/// <summary>
/// Block compression parameters -- after all codes are used up,
/// and compression rate changes, start over.
/// </summary>
private bool clearFlag;
/// <summary>
/// Algorithm: use open addressing double hashing (no chaining) on the
/// prefix code / next character combination. We do a variant of Knuth's
/// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
/// secondary probe. Here, the modular division first probe is gives way
/// to a faster exclusive-or manipulation. Also do block compression with
/// an adaptive reset, whereby the code table is cleared when the compression
/// ratio decreases, but after the table fills. The variable-length output
/// codes are re-sized at this point, and a special CLEAR code is generated
/// for the decompressor. Late addition: construct the table according to
/// file size for noticeable speed improvement on small files. Please direct
/// questions about this implementation to ames!jaw.
/// </summary>
private int globalInitialBits;
/// <summary>
/// The clear code.
/// </summary>
private int clearCode;
/// <summary>
/// The end-of-file code.
/// </summary>
private int eofCode;
/// <summary>
/// Output the given code.
/// Inputs:
/// code: A bitCount-bit integer. If == -1, then EOF. This assumes
/// that bitCount =&lt; wordsize - 1.
/// Outputs:
/// Outputs code to the file.
/// Assumptions:
/// Chars are 8 bits long.
/// Algorithm:
/// Maintain a BITS character long buffer (so that 8 codes will
/// fit in it exactly). Use the VAX insv instruction to insert each
/// code in turn. When the buffer fills up empty it and start over.
/// </summary>
private int currentAccumulator;
/// <summary>
/// The current bits.
/// </summary>
private int currentBits;
/// <summary>
/// Number of characters so far in this 'packet'
/// </summary>
private int accumulatorCount;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TiffLzwEncoder"/> class. /// Initializes a new instance of the <see cref="TiffLzwEncoder"/> class.
/// </summary> /// </summary>
/// <param name="indexedPixels">The array of indexed pixels.</param> /// <param name="data">The data to compress.</param>
/// <param name="colorDepth">The color depth in bits.</param>
/// <param name="memoryAllocator">The memory allocator.</param> /// <param name="memoryAllocator">The memory allocator.</param>
public TiffLzwEncoder(MemoryAllocator memoryAllocator, IMemoryOwner<byte> indexedPixels, int colorDepth) public TiffLzwEncoder(MemoryAllocator memoryAllocator, IMemoryOwner<byte> data)
{ {
this.pixelArray = indexedPixels; this.data = data;
this.initialCodeSize = Math.Max(2, colorDepth); this.children = memoryAllocator.Allocate<int>(TableSize, AllocationOptions.Clean);
this.siblings = memoryAllocator.Allocate<int>(TableSize, AllocationOptions.Clean);
this.hashTable = memoryAllocator.Allocate<int>(HashSize, AllocationOptions.Clean); this.suffixes = memoryAllocator.Allocate<int>(TableSize, AllocationOptions.Clean);
this.codeTable = memoryAllocator.Allocate<int>(HashSize, AllocationOptions.Clean);
this.parent = -1;
this.bitsPerCode = MinBits;
this.nextValidCode = EoiCode + 1;
this.maxCode = (1 << this.bitsPerCode) - 1;
} }
/// <summary> /// <summary>
@ -217,282 +116,158 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
public void Encode(Stream stream) public void Encode(Stream stream)
{ {
this.currentPixel = 0; Span<int> childrenSpan = this.children.GetSpan();
Span<int> suffixesSpan = this.suffixes.GetSpan();
// Compress and write the pixel data Span<int> siblingsSpan = this.siblings.GetSpan();
this.Compress(this.initialCodeSize + 1, stream); int length = this.data.Length();
}
/// <inheritdoc />
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
this.Dispose(true);
}
/// <summary>
/// Gets the maximum code value
/// </summary>
/// <param name="bitCount">The number of bits</param>
/// <returns>See <see cref="int"/></returns>
private static int GetMaxcode(int bitCount)
{
return (1 << bitCount) - 1;
}
/// <summary>
/// Add a character to the end of the current packet, and if it is 254 characters,
/// flush the packet to disk.
/// </summary>
/// <param name="c">The character to add.</param>
/// <param name="stream">The stream to write to.</param>
private void AddCharacter(byte c, Stream stream)
{
this.accumulators[this.accumulatorCount++] = c;
if (this.accumulatorCount >= 254)
{
this.FlushPacket(stream);
}
}
/// <summary> if (length == 0)
/// Table clear for block compress
/// </summary>
/// <param name="stream">The output stream.</param>
private void ClearBlock(Stream stream)
{
this.ResetCodeTable(this.hsize);
this.freeEntry = this.clearCode + 2;
this.clearFlag = true;
this.Output(this.clearCode, stream);
}
/// <summary>
/// Reset the code table.
/// </summary>
/// <param name="size">The hash size.</param>
private void ResetCodeTable(int size)
{
Span<int> hashTableSpan = this.hashTable.GetSpan();
for (int i = 0; i < size; ++i)
{ {
hashTableSpan[i] = -1; return;
} }
}
/// <summary>
/// Compress the packets to the stream.
/// </summary>
/// <param name="intialBits">The initial bits.</param>
/// <param name="stream">The stream to write to.</param>
private void Compress(int intialBits, Stream stream)
{
int fcode;
int c;
int ent;
int hsizeReg;
int hshift;
// Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = intialBits;
// Set up the necessary values
this.clearFlag = false;
this.bitCount = this.globalInitialBits;
this.maxcode = GetMaxcode(this.bitCount);
this.clearCode = 1 << (intialBits - 1); if (this.parent == -1)
this.eofCode = this.clearCode + 1;
this.freeEntry = this.clearCode + 2;
this.accumulatorCount = 0; // clear packet
ent = this.NextPixel();
hshift = 0;
for (fcode = this.hsize; fcode < 65536; fcode *= 2)
{ {
++hshift; // Init stream.
this.WriteCode(stream, ClearCode);
this.parent = this.ReadNextByte() & 0xff;
} }
hshift = 8 - hshift; // set hash code range bound while (this.bufferPosition < this.data.Length())
hsizeReg = this.hsize;
this.ResetCodeTable(hsizeReg); // clear hash table
this.Output(this.clearCode, stream);
Span<int> hashTableSpan = this.hashTable.GetSpan();
Span<int> codeTableSpan = this.codeTable.GetSpan();
while ((c = this.NextPixel()) != Eof)
{ {
fcode = (c << this.maxbits) + ent; int value = this.ReadNextByte() & 0xff;
int i = (c << hshift) ^ ent /* = 0 */; int child = childrenSpan[this.parent];
if (hashTableSpan[i] == fcode) if (child > 0)
{ {
ent = codeTableSpan[i]; if (suffixesSpan[child] == value)
continue;
}
// Non-empty slot
if (hashTableSpan[i] >= 0)
{
int disp = hsizeReg - i;
if (i == 0)
{ {
disp = 1; this.parent = child;
} }
else
do
{ {
if ((i -= disp) < 0) int sibling = child;
{
i += hsizeReg;
}
if (hashTableSpan[i] == fcode) while (true)
{ {
ent = codeTableSpan[i]; if (siblingsSpan[sibling] > 0)
break; {
sibling = siblingsSpan[sibling];
if (suffixesSpan[sibling] == value)
{
this.parent = sibling;
break;
}
}
else
{
siblingsSpan[sibling] = (short)this.nextValidCode;
suffixesSpan[this.nextValidCode] = (short)value;
this.WriteCode(stream, this.parent);
this.parent = value;
this.nextValidCode++;
this.IncreaseCodeSizeOrResetIfNeeded(stream);
break;
}
} }
} }
while (hashTableSpan[i] >= 0);
if (hashTableSpan[i] == fcode)
{
continue;
}
}
this.Output(ent, stream);
ent = c;
if (this.freeEntry < this.maxmaxcode)
{
codeTableSpan[i] = this.freeEntry++; // code -> hashtable
hashTableSpan[i] = fcode;
} }
else else
{ {
this.ClearBlock(stream); childrenSpan[this.parent] = (short)this.nextValidCode;
suffixesSpan[this.nextValidCode] = (short)value;
this.WriteCode(stream, this.parent);
this.parent = value;
this.nextValidCode++;
this.IncreaseCodeSizeOrResetIfNeeded(stream);
} }
} }
// Put out the final code. // Write EOI when we are done.
this.Output(ent, stream); this.WriteCode(stream, this.parent);
this.WriteCode(stream, EoiCode);
this.Output(this.eofCode, stream);
}
/// <summary> // Flush partial codes by writing 0 pad.
/// Flush the packet to disk, and reset the accumulator. if (this.bitPos > 0)
/// </summary>
/// <param name="outStream">The output stream.</param>
private void FlushPacket(Stream outStream)
{
if (this.accumulatorCount > 0)
{ {
outStream.Write(this.accumulators, 0, this.accumulatorCount); this.WriteCode(stream, 0);
this.accumulatorCount = 0;
} }
} }
/// <summary> /// <inheritdoc />
/// Return the next pixel from the image public void Dispose()
/// </summary>
/// <returns>
/// The <see cref="int"/>
/// </returns>
private int NextPixel()
{ {
if (this.currentPixel == this.pixelArray.Length()) this.children.Dispose();
{ this.siblings.Dispose();
return Eof; this.suffixes.Dispose();
}
this.currentPixel++;
return this.pixelArray.GetSpan()[this.currentPixel - 1] & 0xff;
} }
/// <summary> private byte ReadNextByte()
/// Output the current code to the stream.
/// </summary>
/// <param name="code">The code.</param>
/// <param name="outs">The stream to write to.</param>
private void Output(int code, Stream outs)
{ {
this.currentAccumulator &= Masks[this.currentBits]; Span<byte> dataSpan = this.data.GetSpan();
var nextByte = dataSpan[this.bufferPosition];
if (this.currentBits > 0) this.bufferPosition++;
{ return nextByte;
this.currentAccumulator |= code << this.currentBits; }
}
else
{
this.currentAccumulator = code;
}
this.currentBits += this.bitCount;
while (this.currentBits >= 8)
{
this.AddCharacter((byte)(this.currentAccumulator & 0xff), outs);
this.currentAccumulator >>= 8;
this.currentBits -= 8;
}
// If the next entry is going to be too big for the code size, private void IncreaseCodeSizeOrResetIfNeeded(Stream stream)
// then increase it, if possible. {
if (this.freeEntry > this.maxcode || this.clearFlag) if (this.nextValidCode > this.maxCode)
{ {
if (this.clearFlag) if (this.bitsPerCode == MaxBits)
{ {
this.maxcode = GetMaxcode(this.bitCount = this.globalInitialBits); // Reset stream by writing Clear code.
this.clearFlag = false; this.WriteCode(stream, ClearCode);
// Reset tables.
this.ResetTables();
} }
else else
{ {
++this.bitCount; // Increase code size.
this.maxcode = this.bitCount == this.maxbits this.bitsPerCode++;
? this.maxmaxcode this.maxCode = MaxValue(this.bitsPerCode);
: GetMaxcode(this.bitCount);
} }
} }
}
if (code == this.eofCode) private void WriteCode(Stream stream, int code)
{ {
// At EOF, write the rest of the buffer. this.bits = (this.bits << this.bitsPerCode) | (code & this.maxCode);
while (this.currentBits > 0) this.bitPos += this.bitsPerCode;
{
this.AddCharacter((byte)(this.currentAccumulator & 0xff), outs);
this.currentAccumulator >>= 8;
this.currentBits -= 8;
}
this.FlushPacket(outs); while (this.bitPos >= 8)
{
int b = (this.bits >> (this.bitPos - 8)) & 0xff;
stream.WriteByte((byte)b);
this.bitPos -= 8;
} }
this.bits &= BitmaskFor(this.bitPos);
} }
/// <summary> private void ResetTables()
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">If true, the object gets disposed.</param>
private void Dispose(bool disposing)
{ {
if (this.isDisposed) this.children.GetSpan().Fill(0);
{ this.siblings.GetSpan().Fill(0);
return;
}
if (disposing) this.bitsPerCode = MinBits;
{ this.maxCode = MaxValue(this.bitsPerCode);
this.hashTable.Dispose(); this.nextValidCode = EoiCode + 1;
this.codeTable.Dispose(); }
}
this.isDisposed = true; private static int MaxValue(int codeLen)
{
return (1 << codeLen) - 1;
}
private static int BitmaskFor(int bits)
{
return MaxValue(bits);
} }
} }
} }

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

@ -238,7 +238,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
rowSpan.CopyTo(pixels.Slice(y * image.Width * 3)); rowSpan.CopyTo(pixels.Slice(y * image.Width * 3));
} }
using var lzwEncoder = new TiffLzwEncoder(this.memoryAllocator, pixelData, 8); using var lzwEncoder = new TiffLzwEncoder(this.memoryAllocator, pixelData);
lzwEncoder.Encode(memoryStream); lzwEncoder.Encode(memoryStream);
byte[] buffer = memoryStream.ToArray(); byte[] buffer = memoryStream.ToArray();
@ -441,7 +441,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
} }
} }
using var lzwEncoder = new TiffLzwEncoder(this.memoryAllocator, pixelData, 8); using var lzwEncoder = new TiffLzwEncoder(this.memoryAllocator, pixelData);
lzwEncoder.Encode(memoryStream); lzwEncoder.Encode(memoryStream);
byte[] buffer = memoryStream.ToArray(); byte[] buffer = memoryStream.ToArray();
@ -584,7 +584,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
rowSpan.CopyTo(pixels.Slice(y * image.Width)); rowSpan.CopyTo(pixels.Slice(y * image.Width));
} }
using var lzwEncoder = new TiffLzwEncoder(this.memoryAllocator, pixelData, 8); using var lzwEncoder = new TiffLzwEncoder(this.memoryAllocator, pixelData);
lzwEncoder.Encode(memoryStream); lzwEncoder.Encode(memoryStream);
byte[] buffer = memoryStream.ToArray(); byte[] buffer = memoryStream.ToArray();

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

@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression
[InlineData(new byte[] { 42, 16, 128, 53, 96, 218, 7, 64, 3, 4, 97 })] // Random bytes [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, 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 [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) public void Compress_Decompress_Roundtrip_Works(byte[] data)
{ {
using (Stream stream = CreateCompressedStream(data)) using (Stream stream = CreateCompressedStream(data))
{ {

18
tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs

@ -15,13 +15,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression
[Trait("Format", "Tiff")] [Trait("Format", "Tiff")]
public class LzwTiffCompressionTests public class LzwTiffCompressionTests
{ {
[Theory]
[InlineData(new byte[] { 1, 2, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 3, 4 }, new byte[] { 128, 0, 64, 66, 168, 36, 22, 12, 3, 2, 64, 64, 0, 0 })] // Repeated bytes
public void Compress_Works(byte[] inputData, byte[] expectedCompressedData)
{
var compressedData = new byte[expectedCompressedData.Length];
Stream streamData = CreateCompressedStream(inputData);
streamData.Read(compressedData, 0, expectedCompressedData.Length);
Assert.Equal(expectedCompressedData, compressedData);
}
[Theory] [Theory]
[InlineData(new byte[] { })] [InlineData(new byte[] { })]
[InlineData(new byte[] { 42 })] // One 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[] { 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, 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 [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)
public void Compress_Decompress_Roundtrip_Works(byte[] data)
{ {
using Stream stream = CreateCompressedStream(data); using Stream stream = CreateCompressedStream(data);
var buffer = new byte[data.Length]; var buffer = new byte[data.Length];
@ -37,12 +50,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression
using System.Buffers.IMemoryOwner<byte> data = Configuration.Default.MemoryAllocator.Allocate<byte>(inputData.Length); using System.Buffers.IMemoryOwner<byte> data = Configuration.Default.MemoryAllocator.Allocate<byte>(inputData.Length);
inputData.AsSpan().CopyTo(data.GetSpan()); inputData.AsSpan().CopyTo(data.GetSpan());
using (var encoder = new TiffLzwEncoder(Configuration.Default.MemoryAllocator, data, 8)) using (var encoder = new TiffLzwEncoder(Configuration.Default.MemoryAllocator, data))
{ {
encoder.Encode(compressedStream); encoder.Encode(compressedStream);
} }
compressedStream.Seek(0, SeekOrigin.Begin); compressedStream.Seek(0, SeekOrigin.Begin);
return compressedStream; return compressedStream;
} }
} }

Loading…
Cancel
Save