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);