From 56ac3f8f29db2953aedd9fe657db046ff929ff85 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 3 Dec 2020 13:13:54 +0100 Subject: [PATCH] Add support for encoding tiff with deflate and horizontal predictor --- .../Formats/Tiff/ITiffEncoderOptions.cs | 5 ++ .../Formats/Tiff/TiffDecoderCore.cs | 11 +++- src/ImageSharp/Formats/Tiff/TiffEncoder.cs | 5 ++ .../Formats/Tiff/TiffEncoderCore.cs | 20 +++++- .../Formats/Tiff/Utils/TiffWriter.cs | 65 ++++++++++++++++--- .../Formats/Tiff/TiffEncoderTests.cs | 13 +++- 6 files changed, 106 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs index b24d7ff3d..0d3aa4bac 100644 --- a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs +++ b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs @@ -20,6 +20,11 @@ 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. + /// + bool UseHorizontalPredictor { get; } + /// /// Gets the quantizer for creating a color palette image. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index e19630f26..381162093 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -263,13 +263,20 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff if (tiffFormatMetaData.Predictor == TiffPredictor.Horizontal) { - this.UndoHorizontalPredictor(width, height, frame); + this.UndoHorizontalPredictor(frame, width, height); } return frame; } - private void UndoHorizontalPredictor(int width, int height, ImageFrame frame) + /// + /// This will reverse the horizontal prediction operation. + /// + /// The pixel format. + /// The image frame. + /// The width of the image. + /// The height of the image. + private void UndoHorizontalPredictor(ImageFrame frame, int width, int height) where TPixel : unmanaged, IPixel { using System.Buffers.IMemoryOwner rowRgbBuffer = this.memoryAllocator.Allocate(width); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 84e9fb979..2deb063b0 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -25,6 +25,11 @@ 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. + /// + public bool UseHorizontalPredictor { get; set; } + /// /// Gets or sets the quantizer for color images with a palette. /// Defaults to OctreeQuantizer. diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index c6fec546f..6bc3b7338 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -48,6 +48,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff /// private readonly IQuantizer quantizer; + /// + /// Indicating whether to use horizontal prediction. This can improve the compression ratio with deflate compression. + /// + private bool useHorizontalPredictor; + /// /// Initializes a new instance of the class. /// @@ -59,6 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff this.CompressionType = options.Compression; this.Mode = options.Mode; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; + this.useHorizontalPredictor = options.UseHorizontalPredictor; } /// @@ -162,13 +168,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff imageDataBytes = writer.WritePalettedRgb(image, this.quantizer, this.padding, this.CompressionType, out colorMap); break; case TiffEncodingMode.Gray: - imageDataBytes = writer.WriteGray(image, this.padding, this.CompressionType); + imageDataBytes = writer.WriteGray(image, this.padding, this.CompressionType, this.useHorizontalPredictor); break; case TiffEncodingMode.BiColor: imageDataBytes = writer.WriteBiColor(image, this.CompressionType); break; default: - imageDataBytes = writer.WriteRgb(image, this.padding, this.CompressionType); + imageDataBytes = writer.WriteRgb(image, this.padding, this.CompressionType, this.useHorizontalPredictor); break; } @@ -337,6 +343,16 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff ifdEntries.Add(yResolution); ifdEntries.Add(resolutionUnit); ifdEntries.Add(software); + + if (this.useHorizontalPredictor) + { + if (this.Mode == TiffEncodingMode.Rgb || this.Mode == TiffEncodingMode.Gray) + { + var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; + + ifdEntries.Add(predictor); + } + } } private void SetPhotometricInterpretation() diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs index 774273b07..52edbdc41 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs @@ -137,15 +137,16 @@ 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. /// The number of bytes written. - public int WriteRgb(Image image, int padding, TiffEncoderCompression compression) + public int WriteRgb(Image image, int padding, TiffEncoderCompression compression, bool useHorizontalPredictor) where TPixel : unmanaged, IPixel { using IManagedByteBuffer row = this.AllocateRow(image.Width, 3, padding); Span rowSpan = row.GetSpan(); if (compression == TiffEncoderCompression.Deflate) { - return this.WriteDeflateCompressedRgb(image, rowSpan); + return this.WriteDeflateCompressedRgb(image, rowSpan, useHorizontalPredictor); } if (compression == TiffEncoderCompression.PackBits) @@ -172,8 +173,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// 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 WriteDeflateCompressedRgb(Image image, Span rowSpan) + private int WriteDeflateCompressedRgb(Image image, Span rowSpan, bool useHorizontalPredictor) where TPixel : unmanaged, IPixel { int bytesWritten = 0; @@ -186,6 +188,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils { Span pixelRow = image.GetPixelRowSpan(y); PixelOperations.Instance.ToRgb24Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); + + if (useHorizontalPredictor) + { + this.ApplyHorizontalPredictionRgb(rowSpan); + } + deflateStream.Write(rowSpan); } @@ -197,6 +205,27 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils 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. + /// In such images, if we replace the pixel values by differences between consecutive pixels, many of the differences should be 0, plus + /// or minus 1, and so on.This reduces the apparent information content and allows LZW to encode the data more compactly. + /// + /// The rgb pixel row. + private void ApplyHorizontalPredictionRgb(Span rowSpan) + { + Span rowRgb = MemoryMarshal.Cast(rowSpan); + + for (int x = rowRgb.Length - 1; x >= 1; x--) + { + byte r = (byte)(rowRgb[x].R - rowRgb[x - 1].R); + byte g = (byte)(rowRgb[x].G - rowRgb[x - 1].G); + byte b = (byte)(rowRgb[x].B - rowRgb[x - 1].B); + var rgb = new Rgb24(r, g, b); + rowRgb[x].FromRgb24(rgb); + } + } + /// /// Writes the image data as RGB with packed bits compression to the stream. /// @@ -208,7 +237,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils where TPixel : unmanaged, IPixel { // Worst case is that the actual compressed data is larger then the input data. In this case we need 1 additional byte per 127 bytes. - int additionalBytes = ((image.Width * 3) / 127) + 1; + int additionalBytes = (image.Width * 3 / 127) + 1; using IManagedByteBuffer compressedRow = this.memoryAllocator.AllocateManagedByteBuffer((image.Width * 3) + additionalBytes, AllocationOptions.Clean); Span compressedRowSpan = compressedRow.GetSpan(); int bytesWritten = 0; @@ -360,7 +389,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils where TPixel : unmanaged, IPixel { // Worst case is that the actual compressed data is larger then the input data. In this case we need 1 additional byte per 127 bytes. - int additionalBytes = (image.Width * 3 / 127) + 1; + int additionalBytes = ((image.Width * 3) / 127) + 1; using IManagedByteBuffer compressedRow = this.memoryAllocator.AllocateManagedByteBuffer((image.Width * 3) + additionalBytes, AllocationOptions.Clean); using IManagedByteBuffer pixelRowWithPadding = this.memoryAllocator.AllocateManagedByteBuffer((image.Width * 3) + padding, AllocationOptions.Clean); Span compressedRowSpan = compressedRow.GetSpan(); @@ -396,8 +425,9 @@ 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. /// The number of bytes written. - public int WriteGray(Image image, int padding, TiffEncoderCompression compression) + public int WriteGray(Image image, int padding, TiffEncoderCompression compression, bool useHorizontalPredictor) where TPixel : unmanaged, IPixel { using IManagedByteBuffer row = this.AllocateRow(image.Width, 1, padding); @@ -405,7 +435,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils if (compression == TiffEncoderCompression.Deflate) { - return this.WriteGrayDeflateCompressed(image, rowSpan); + return this.WriteGrayDeflateCompressed(image, rowSpan, useHorizontalPredictor); } if (compression == TiffEncoderCompression.PackBits) @@ -430,8 +460,9 @@ 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. /// The number of bytes written. - private int WriteGrayDeflateCompressed(Image image, Span rowSpan) + private int WriteGrayDeflateCompressed(Image image, Span rowSpan, bool useHorizontalPredictor) where TPixel : unmanaged, IPixel { int bytesWritten = 0; @@ -444,6 +475,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils { Span pixelRow = image.GetPixelRowSpan(y); PixelOperations.Instance.ToL8Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); + + if (useHorizontalPredictor) + { + this.ApplyHorizontalPredictionGray(rowSpan); + } + deflateStream.Write(rowSpan); } @@ -455,6 +492,18 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils return bytesWritten; } + /// + /// Applies a horizontal predictor to a gray pixel row. + /// + /// The gray pixel row. + private void ApplyHorizontalPredictionGray(Span rowSpan) + { + for (int x = rowSpan.Length - 1; x >= 1; x--) + { + rowSpan[x] -= rowSpan[x - 1]; + } + } + /// /// Writes the image data as 8 bit gray to the stream. /// diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 7f255d395..ff00edb67 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -56,6 +56,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffEncoder_EncodeRgb_WithDeflateCompression_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.Rgb, TiffEncoderCompression.Deflate); + [Theory] + [WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + 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_WithPackBitsCompression_Works(TestImageProvider provider) @@ -71,6 +76,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffEncoder_EncodeGray_WithDeflateCompression_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray, TiffEncoderCompression.Deflate); + [Theory] + [WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + 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_WithPackBitsCompression_Works(TestImageProvider provider) @@ -160,12 +170,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff TiffBitsPerPixel bitsPerPixel, TiffEncodingMode mode, TiffEncoderCompression compression = TiffEncoderCompression.None, + bool usePredictor = false, bool useExactComparer = true, float compareTolerance = 0.01f) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - var encoder = new TiffEncoder { Mode = mode, Compression = compression }; + var encoder = new TiffEncoder { Mode = mode, Compression = compression, UseHorizontalPredictor = usePredictor }; // Does DebugSave & load reference CompareToReferenceInput(): image.VerifyEncoder(provider, "tiff", bitsPerPixel, encoder, useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), referenceDecoder: ReferenceDecoder);