Browse Source

Add support for encoding tiff's with lzw compression

pull/1570/head
Brian Popow 6 years ago
parent
commit
7ac6fa6a82
  1. 2
      src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs
  2. 2
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  3. 5
      src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs
  4. 10
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  5. 12
      src/ImageSharp/Formats/Tiff/Utils/TiffLzwEncoder.cs
  6. 89
      src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
  7. 9
      tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs
  8. 20
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

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

@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
TiffEncodingMode Mode { get; } TiffEncodingMode Mode { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether to use horizontal prediction. This can improve the compression ratio with deflate compression. /// Gets a value indicating whether to use horizontal prediction. This can improve the compression ratio with deflate or lzw compression.
/// </summary> /// </summary>
bool UseHorizontalPredictor { get; } bool UseHorizontalPredictor { get; }

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

@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public TiffEncodingMode Mode { get; set; } public TiffEncodingMode Mode { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to use horizontal prediction. This can improve the compression ratio with deflate compression. /// Gets or sets a value indicating whether to use horizontal prediction. This can improve the compression ratio with deflate or lzw compression.
/// </summary> /// </summary>
public bool UseHorizontalPredictor { get; set; } public bool UseHorizontalPredictor { get; set; }

5
src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs

@ -18,6 +18,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary> /// </summary>
Deflate, Deflate,
/// <summary>
/// Use lzw compression.
/// </summary>
Lzw,
/// <summary> /// <summary>
/// Use PackBits to compression the image data. /// Use PackBits to compression the image data.
/// </summary> /// </summary>

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

@ -433,6 +433,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
return (ushort)TiffCompression.Deflate; return (ushort)TiffCompression.Deflate;
} }
if (this.CompressionType == TiffEncoderCompression.Lzw && this.Mode == TiffEncodingMode.Rgb)
{
return (ushort)TiffCompression.Lzw;
}
if (this.CompressionType == TiffEncoderCompression.PackBits && this.Mode == TiffEncodingMode.Rgb) if (this.CompressionType == TiffEncoderCompression.PackBits && this.Mode == TiffEncodingMode.Rgb)
{ {
return (ushort)TiffCompression.PackBits; return (ushort)TiffCompression.PackBits;
@ -443,6 +448,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
return (ushort)TiffCompression.Deflate; return (ushort)TiffCompression.Deflate;
} }
if (this.CompressionType == TiffEncoderCompression.Lzw && this.Mode == TiffEncodingMode.Gray)
{
return (ushort)TiffCompression.Lzw;
}
if (this.CompressionType == TiffEncoderCompression.PackBits && this.Mode == TiffEncodingMode.Gray) if (this.CompressionType == TiffEncoderCompression.PackBits && this.Mode == TiffEncodingMode.Gray)
{ {
return (ushort)TiffCompression.PackBits; return (ushort)TiffCompression.PackBits;

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

@ -5,6 +5,7 @@ using System;
using System.Buffers; using System.Buffers;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
{ {
@ -66,9 +67,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
}; };
/// <summary> /// <summary>
/// The working pixel array /// The working pixel array.
/// </summary> /// </summary>
private readonly byte[] pixelArray; private readonly IMemoryOwner<byte> pixelArray;
/// <summary> /// <summary>
/// The initial code size. /// The initial code size.
@ -200,11 +201,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
/// </summary> /// </summary>
/// <param name="indexedPixels">The array of indexed pixels.</param> /// <param name="indexedPixels">The array of indexed pixels.</param>
/// <param name="colorDepth">The color depth in bits.</param> /// <param name="colorDepth">The color depth in bits.</param>
public TiffLzwEncoder(byte[] indexedPixels, int colorDepth) public TiffLzwEncoder(IMemoryOwner<byte> indexedPixels, int colorDepth)
{ {
this.pixelArray = indexedPixels; this.pixelArray = indexedPixels;
this.initialCodeSize = Math.Max(2, colorDepth); this.initialCodeSize = Math.Max(2, colorDepth);
// TODO: use memory allocator
this.hashTable = ArrayPool<int>.Shared.Rent(HashSize); this.hashTable = ArrayPool<int>.Shared.Rent(HashSize);
this.codeTable = ArrayPool<int>.Shared.Rent(HashSize); this.codeTable = ArrayPool<int>.Shared.Rent(HashSize);
Array.Clear(this.hashTable, 0, HashSize); Array.Clear(this.hashTable, 0, HashSize);
@ -404,13 +406,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
/// </returns> /// </returns>
private int NextPixel() private int NextPixel()
{ {
if (this.currentPixel == this.pixelArray.Length) if (this.currentPixel == this.pixelArray.Length())
{ {
return Eof; return Eof;
} }
this.currentPixel++; this.currentPixel++;
return this.pixelArray[this.currentPixel - 1] & 0xff; return this.pixelArray.GetSpan()[this.currentPixel - 1] & 0xff;
} }
/// <summary> /// <summary>

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

@ -7,6 +7,7 @@ using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Formats.Png.Zlib;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -149,6 +150,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
return this.WriteDeflateCompressedRgb(image, rowSpan, useHorizontalPredictor); return this.WriteDeflateCompressedRgb(image, rowSpan, useHorizontalPredictor);
} }
if (compression == TiffEncoderCompression.Lzw)
{
return this.WriteLzwCompressedRgb(image, rowSpan, useHorizontalPredictor);
}
if (compression == TiffEncoderCompression.PackBits) if (compression == TiffEncoderCompression.PackBits)
{ {
return this.WriteRgbPackBitsCompressed(image, rowSpan); return this.WriteRgbPackBitsCompressed(image, rowSpan);
@ -205,6 +211,44 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
return bytesWritten; return bytesWritten;
} }
/// <summary>
/// Writes the image data as RGB compressed with lzw to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel data.</typeparam>
/// <param name="image">The image to write to the stream.</param>
/// <param name="rowSpan">A Span for a pixel row.</param>
/// <param name="useHorizontalPredictor">Indicates if horizontal prediction should be used. Should only be used with deflate compression.</param>
/// <returns>The number of bytes written.</returns>
private int WriteLzwCompressedRgb<TPixel>(Image<TPixel> image, Span<byte> rowSpan, bool useHorizontalPredictor)
where TPixel : unmanaged, IPixel<TPixel>
{
int bytesWritten = 0;
using var memoryStream = new MemoryStream();
IMemoryOwner<byte> pixelData = this.memoryAllocator.Allocate<byte>(image.Width * image.Height * 3);
Span<byte> pixels = pixelData.GetSpan();
for (int y = 0; y < image.Height; y++)
{
Span<TPixel> pixelRow = image.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgb24Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length);
if (useHorizontalPredictor)
{
this.ApplyHorizontalPredictionRgb(rowSpan);
}
rowSpan.CopyTo(pixels.Slice(y * image.Width * 3));
}
using var lzwEncoder = new TiffLzwEncoder(pixelData, 8);
lzwEncoder.Encode(memoryStream);
byte[] buffer = memoryStream.ToArray();
this.output.Write(buffer);
bytesWritten += buffer.Length;
return bytesWritten;
}
/// <summary> /// <summary>
/// Applies a horizontal predictor to the rgb row. /// Applies a horizontal predictor to the rgb row.
/// Make use of the fact that many continuous-tone images rarely vary much in pixel value from one pixel to the next. /// Make use of the fact that many continuous-tone images rarely vary much in pixel value from one pixel to the next.
@ -425,7 +469,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
/// <param name="image">The image to write to the stream.</param> /// <param name="image">The image to write to the stream.</param>
/// <param name="padding">The padding bytes for each row.</param> /// <param name="padding">The padding bytes for each row.</param>
/// <param name="compression">The compression to use.</param> /// <param name="compression">The compression to use.</param>
/// <param name="useHorizontalPredictor">Indicates if horizontal prediction should be used. Should only be used with deflate compression.</param> /// <param name="useHorizontalPredictor">Indicates if horizontal prediction should be used. Should only be used with deflate or lzw compression.</param>
/// <returns>The number of bytes written.</returns> /// <returns>The number of bytes written.</returns>
public int WriteGray<TPixel>(Image<TPixel> image, int padding, TiffEncoderCompression compression, bool useHorizontalPredictor) public int WriteGray<TPixel>(Image<TPixel> image, int padding, TiffEncoderCompression compression, bool useHorizontalPredictor)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -438,6 +482,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
return this.WriteGrayDeflateCompressed(image, rowSpan, useHorizontalPredictor); return this.WriteGrayDeflateCompressed(image, rowSpan, useHorizontalPredictor);
} }
if (compression == TiffEncoderCompression.Lzw)
{
return this.WriteGrayLzwCompressed(image, rowSpan, useHorizontalPredictor);
}
if (compression == TiffEncoderCompression.PackBits) if (compression == TiffEncoderCompression.PackBits)
{ {
return this.WriteGrayPackBitsCompressed(image, rowSpan); return this.WriteGrayPackBitsCompressed(image, rowSpan);
@ -460,7 +509,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
/// </summary> /// </summary>
/// <param name="image">The image to write to the stream.</param> /// <param name="image">The image to write to the stream.</param>
/// <param name="rowSpan">A span of a row of pixels.</param> /// <param name="rowSpan">A span of a row of pixels.</param>
/// <param name="useHorizontalPredictor">Indicates if horizontal prediction should be used. Should only be used with deflate compression.</param> /// <param name="useHorizontalPredictor">Indicates if horizontal prediction should be used.</param>
/// <returns>The number of bytes written.</returns> /// <returns>The number of bytes written.</returns>
private int WriteGrayDeflateCompressed<TPixel>(Image<TPixel> image, Span<byte> rowSpan, bool useHorizontalPredictor) private int WriteGrayDeflateCompressed<TPixel>(Image<TPixel> image, Span<byte> rowSpan, bool useHorizontalPredictor)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -492,6 +541,42 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
return bytesWritten; return bytesWritten;
} }
/// <summary>
/// Writes the image data as 8 bit gray with lzw compression to the stream.
/// </summary>
/// <param name="image">The image to write to the stream.</param>
/// <param name="rowSpan">A span of a row of pixels.</param>
/// <param name="useHorizontalPredictor">Indicates if horizontal prediction should be used.</param>
/// <returns>The number of bytes written.</returns>
private int WriteGrayLzwCompressed<TPixel>(Image<TPixel> image, Span<byte> rowSpan, bool useHorizontalPredictor)
where TPixel : unmanaged, IPixel<TPixel>
{
int bytesWritten = 0;
using var memoryStream = new MemoryStream();
IMemoryOwner<byte> pixelData = this.memoryAllocator.Allocate<byte>(image.Width * image.Height);
Span<byte> pixels = pixelData.GetSpan();
for (int y = 0; y < image.Height; y++)
{
Span<TPixel> pixelRow = image.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length);
if (useHorizontalPredictor)
{
this.ApplyHorizontalPredictionGray(rowSpan);
}
rowSpan.CopyTo(pixels.Slice(y * image.Width));
}
using var lzwEncoder = new TiffLzwEncoder(pixelData, 8);
lzwEncoder.Encode(memoryStream);
byte[] buffer = memoryStream.ToArray();
this.output.Write(buffer);
bytesWritten += buffer.Length;
return bytesWritten;
}
/// <summary> /// <summary>
/// Applies a horizontal predictor to a gray pixel row. /// Applies a horizontal predictor to a gray pixel row.
/// </summary> /// </summary>

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

@ -1,11 +1,12 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using Xunit; using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression
@ -29,9 +30,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression
Assert.Equal(data, buffer); Assert.Equal(data, buffer);
} }
private static Stream CreateCompressedStream(byte[] data) private static Stream CreateCompressedStream(byte[] inputData)
{ {
Stream compressedStream = new MemoryStream(); using Stream compressedStream = new MemoryStream();
using System.Buffers.IMemoryOwner<byte> data = Configuration.Default.MemoryAllocator.Allocate<byte>(inputData.Length);
inputData.AsSpan().CopyTo(data.GetSpan());
using (var encoder = new TiffLzwEncoder(data, 8)) using (var encoder = new TiffLzwEncoder(data, 8))
{ {

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

@ -61,6 +61,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffEncoder_EncodeRgb_WithDeflateCompressionAndPredictor_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeRgb_WithDeflateCompressionAndPredictor_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.Rgb, TiffEncoderCompression.Deflate, usePredictor: true); where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.Rgb, TiffEncoderCompression.Deflate, usePredictor: true);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeRgb_WithLzwCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.Rgb, TiffEncoderCompression.Lzw);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeRgb_WithLzwCompressionAndPredictor_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.Rgb, TiffEncoderCompression.Lzw, usePredictor: true);
[Theory] [Theory]
[WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)] [WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
@ -81,6 +91,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffEncoder_EncodeGray_WithDeflateCompressionAndPredictor_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeGray_WithDeflateCompressionAndPredictor_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray, TiffEncoderCompression.Deflate, usePredictor: true); where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray, TiffEncoderCompression.Deflate, usePredictor: true);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeGray_WithLzwCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray, TiffEncoderCompression.Lzw);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeGray_WithLzwCompressionAndPredictor_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray, TiffEncoderCompression.Lzw, usePredictor: true);
[Theory] [Theory]
[WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] [WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeGray_WithPackBitsCompression_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeGray_WithPackBitsCompression_Works<TPixel>(TestImageProvider<TPixel> provider)

Loading…
Cancel
Save