From 7ac6fa6a82c51a9cf6189251b8beab8ebdf5e9e1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 3 Dec 2020 19:11:22 +0100 Subject: [PATCH] Add support for encoding tiff's with lzw compression --- .../Formats/Tiff/ITiffEncoderOptions.cs | 2 +- src/ImageSharp/Formats/Tiff/TiffEncoder.cs | 2 +- .../Formats/Tiff/TiffEncoderCompression.cs | 5 ++ .../Formats/Tiff/TiffEncoderCore.cs | 10 +++ .../Formats/Tiff/Utils/TiffLzwEncoder.cs | 12 +-- .../Formats/Tiff/Utils/TiffWriter.cs | 89 ++++++++++++++++++- .../Compression/LzwTiffCompressionTests.cs | 9 +- .../Formats/Tiff/TiffEncoderTests.cs | 20 +++++ 8 files changed, 137 insertions(+), 12 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs index 0d3aa4bac2..78cf553d34 100644 --- a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs +++ b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff TiffEncodingMode Mode { get; } /// - /// 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. /// bool UseHorizontalPredictor { get; } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 2deb063b0c..0d1821704d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff public TiffEncodingMode Mode { get; set; } /// - /// 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. /// public bool UseHorizontalPredictor { get; set; } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs index 145849d4f5..f2e94d316e 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs @@ -18,6 +18,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// Deflate, + /// + /// Use lzw compression. + /// + Lzw, + /// /// Use PackBits to compression the image data. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 6bc3b7338b..c5283c74b5 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -433,6 +433,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff 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) { return (ushort)TiffCompression.PackBits; @@ -443,6 +448,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff 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) { return (ushort)TiffCompression.PackBits; diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffLzwEncoder.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffLzwEncoder.cs index d22f14ce5c..b0c20a0db7 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffLzwEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffLzwEncoder.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.IO; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils { @@ -66,9 +67,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils }; /// - /// The working pixel array + /// The working pixel array. /// - private readonly byte[] pixelArray; + private readonly IMemoryOwner pixelArray; /// /// The initial code size. @@ -200,11 +201,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// /// The array of indexed pixels. /// The color depth in bits. - public TiffLzwEncoder(byte[] indexedPixels, int colorDepth) + public TiffLzwEncoder(IMemoryOwner indexedPixels, int colorDepth) { this.pixelArray = indexedPixels; this.initialCodeSize = Math.Max(2, colorDepth); + // TODO: use memory allocator this.hashTable = ArrayPool.Shared.Rent(HashSize); this.codeTable = ArrayPool.Shared.Rent(HashSize); Array.Clear(this.hashTable, 0, HashSize); @@ -404,13 +406,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// private int NextPixel() { - if (this.currentPixel == this.pixelArray.Length) + if (this.currentPixel == this.pixelArray.Length()) { return Eof; } this.currentPixel++; - return this.pixelArray[this.currentPixel - 1] & 0xff; + return this.pixelArray.GetSpan()[this.currentPixel - 1] & 0xff; } /// diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs index 52edbdc41d..fce6ec4624 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Memory; @@ -149,6 +150,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils return this.WriteDeflateCompressedRgb(image, rowSpan, useHorizontalPredictor); } + if (compression == TiffEncoderCompression.Lzw) + { + return this.WriteLzwCompressedRgb(image, rowSpan, useHorizontalPredictor); + } + if (compression == TiffEncoderCompression.PackBits) { return this.WriteRgbPackBitsCompressed(image, rowSpan); @@ -205,6 +211,44 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils return bytesWritten; } + /// + /// Writes the image data as RGB compressed with lzw to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// A Span for a pixel row. + /// Indicates if horizontal prediction should be used. Should only be used with deflate compression. + /// The number of bytes written. + private int WriteLzwCompressedRgb(Image image, Span rowSpan, bool useHorizontalPredictor) + where TPixel : unmanaged, IPixel + { + int bytesWritten = 0; + using var memoryStream = new MemoryStream(); + + IMemoryOwner pixelData = this.memoryAllocator.Allocate(image.Width * image.Height * 3); + Span pixels = pixelData.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.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; + } + /// /// 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. @@ -425,7 +469,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// The image to write to the stream. /// The padding bytes for each row. /// The compression to use. - /// Indicates if horizontal prediction should be used. Should only be used with deflate compression. + /// Indicates if horizontal prediction should be used. Should only be used with deflate or lzw compression. /// The number of bytes written. public int WriteGray(Image image, int padding, TiffEncoderCompression compression, bool useHorizontalPredictor) where TPixel : unmanaged, IPixel @@ -438,6 +482,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils return this.WriteGrayDeflateCompressed(image, rowSpan, useHorizontalPredictor); } + if (compression == TiffEncoderCompression.Lzw) + { + return this.WriteGrayLzwCompressed(image, rowSpan, useHorizontalPredictor); + } + if (compression == TiffEncoderCompression.PackBits) { return this.WriteGrayPackBitsCompressed(image, rowSpan); @@ -460,7 +509,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// /// The image to write to the stream. /// A span of a row of pixels. - /// Indicates if horizontal prediction should be used. Should only be used with deflate compression. + /// Indicates if horizontal prediction should be used. /// The number of bytes written. private int WriteGrayDeflateCompressed(Image image, Span rowSpan, bool useHorizontalPredictor) where TPixel : unmanaged, IPixel @@ -492,6 +541,42 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils return bytesWritten; } + /// + /// Writes the image data as 8 bit gray with lzw compression to the stream. + /// + /// The image to write to the stream. + /// A span of a row of pixels. + /// Indicates if horizontal prediction should be used. + /// The number of bytes written. + private int WriteGrayLzwCompressed(Image image, Span rowSpan, bool useHorizontalPredictor) + where TPixel : unmanaged, IPixel + { + int bytesWritten = 0; + using var memoryStream = new MemoryStream(); + + IMemoryOwner pixelData = this.memoryAllocator.Allocate(image.Width * image.Height); + Span pixels = pixelData.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.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; + } + /// /// Applies a horizontal predictor to a gray pixel row. /// diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs index f6a31f43a3..775d55af69 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs @@ -1,11 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils; - +using SixLabors.ImageSharp.Memory; using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression @@ -29,9 +30,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression 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 data = Configuration.Default.MemoryAllocator.Allocate(inputData.Length); + inputData.AsSpan().CopyTo(data.GetSpan()); using (var encoder = new TiffLzwEncoder(data, 8)) { diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index ff00edb672..2f0d753884 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -61,6 +61,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffEncoder_EncodeRgb_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.Rgb, TiffEncoderCompression.Deflate, usePredictor: true); + [Theory] + [WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeRgb_WithLzwCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.Rgb, TiffEncoderCompression.Lzw); + + [Theory] + [WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeRgb_WithLzwCompressionAndPredictor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.Rgb, TiffEncoderCompression.Lzw, usePredictor: true); + [Theory] [WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works(TestImageProvider provider) @@ -81,6 +91,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffEncoder_EncodeGray_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray, TiffEncoderCompression.Deflate, usePredictor: true); + [Theory] + [WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeGray_WithLzwCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray, TiffEncoderCompression.Lzw); + + [Theory] + [WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeGray_WithLzwCompressionAndPredictor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray, TiffEncoderCompression.Lzw, usePredictor: true); + [Theory] [WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeGray_WithPackBitsCompression_Works(TestImageProvider provider)