Browse Source

Add support for encoding tiff with deflate and horizontal predictor

pull/1570/head
Brian Popow 5 years ago
parent
commit
56ac3f8f29
  1. 5
      src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs
  2. 11
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  3. 5
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  4. 20
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  5. 65
      src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
  6. 13
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

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

@ -20,6 +20,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
TiffEncodingMode Mode { get; }
/// <summary>
/// Gets a value indicating whether to use horizontal prediction. This can improve the compression ratio with deflate compression.
/// </summary>
bool UseHorizontalPredictor { get; }
/// <summary>
/// Gets the quantizer for creating a color palette image.
/// </summary>

11
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<TPixel>(int width, int height, ImageFrame<TPixel> frame)
/// <summary>
/// This will reverse the horizontal prediction operation.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
private void UndoHorizontalPredictor<TPixel>(ImageFrame<TPixel> frame, int width, int height)
where TPixel : unmanaged, IPixel<TPixel>
{
using System.Buffers.IMemoryOwner<Rgb24> rowRgbBuffer = this.memoryAllocator.Allocate<Rgb24>(width);

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

@ -25,6 +25,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public TiffEncodingMode Mode { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use horizontal prediction. This can improve the compression ratio with deflate compression.
/// </summary>
public bool UseHorizontalPredictor { get; set; }
/// <summary>
/// Gets or sets the quantizer for color images with a palette.
/// Defaults to OctreeQuantizer.

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

@ -48,6 +48,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
private readonly IQuantizer quantizer;
/// <summary>
/// Indicating whether to use horizontal prediction. This can improve the compression ratio with deflate compression.
/// </summary>
private bool useHorizontalPredictor;
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoderCore"/> class.
/// </summary>
@ -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;
}
/// <summary>
@ -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()

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

@ -137,15 +137,16 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
/// <param name="image">The image to write to the stream.</param>
/// <param name="padding">The padding bytes for each row.</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>
/// <returns>The number of bytes written.</returns>
public int WriteRgb<TPixel>(Image<TPixel> image, int padding, TiffEncoderCompression compression)
public int WriteRgb<TPixel>(Image<TPixel> image, int padding, TiffEncoderCompression compression, bool useHorizontalPredictor)
where TPixel : unmanaged, IPixel<TPixel>
{
using IManagedByteBuffer row = this.AllocateRow(image.Width, 3, padding);
Span<byte> 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
/// <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 WriteDeflateCompressedRgb<TPixel>(Image<TPixel> image, Span<byte> rowSpan)
private int WriteDeflateCompressedRgb<TPixel>(Image<TPixel> image, Span<byte> rowSpan, bool useHorizontalPredictor)
where TPixel : unmanaged, IPixel<TPixel>
{
int bytesWritten = 0;
@ -186,6 +188,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
{
Span<TPixel> pixelRow = image.GetPixelRowSpan(y);
PixelOperations<TPixel>.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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="rowSpan">The rgb pixel row.</param>
private void ApplyHorizontalPredictionRgb(Span<byte> rowSpan)
{
Span<Rgb24> rowRgb = MemoryMarshal.Cast<byte, Rgb24>(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);
}
}
/// <summary>
/// Writes the image data as RGB with packed bits compression to the stream.
/// </summary>
@ -208,7 +237,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<byte> compressedRowSpan = compressedRow.GetSpan();
int bytesWritten = 0;
@ -360,7 +389,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<byte> compressedRowSpan = compressedRow.GetSpan();
@ -396,8 +425,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
/// <param name="image">The image to write to the stream.</param>
/// <param name="padding">The padding bytes for each row.</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>
/// <returns>The number of bytes written.</returns>
public int WriteGray<TPixel>(Image<TPixel> image, int padding, TiffEncoderCompression compression)
public int WriteGray<TPixel>(Image<TPixel> image, int padding, TiffEncoderCompression compression, bool useHorizontalPredictor)
where TPixel : unmanaged, IPixel<TPixel>
{
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
/// </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. Should only be used with deflate compression.</param>
/// <returns>The number of bytes written.</returns>
private int WriteGrayDeflateCompressed<TPixel>(Image<TPixel> image, Span<byte> rowSpan)
private int WriteGrayDeflateCompressed<TPixel>(Image<TPixel> image, Span<byte> rowSpan, bool useHorizontalPredictor)
where TPixel : unmanaged, IPixel<TPixel>
{
int bytesWritten = 0;
@ -444,6 +475,12 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils
{
Span<TPixel> pixelRow = image.GetPixelRowSpan(y);
PixelOperations<TPixel>.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;
}
/// <summary>
/// Applies a horizontal predictor to a gray pixel row.
/// </summary>
/// <param name="rowSpan">The gray pixel row.</param>
private void ApplyHorizontalPredictionGray(Span<byte> rowSpan)
{
for (int x = rowSpan.Length - 1; x >= 1; x--)
{
rowSpan[x] -= rowSpan[x - 1];
}
}
/// <summary>
/// Writes the image data as 8 bit gray to the stream.
/// </summary>

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

@ -56,6 +56,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffEncoder_EncodeRgb_WithDeflateCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.Rgb, TiffEncoderCompression.Deflate);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)]
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);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_RgbUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
@ -71,6 +76,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffEncoder_EncodeGray_WithDeflateCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray, TiffEncoderCompression.Deflate);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
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);
[Theory]
[WithFile(TestImages.Tiff.Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeGray_WithPackBitsCompression_Works<TPixel>(TestImageProvider<TPixel> 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<TPixel>
{
using Image<TPixel> 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);

Loading…
Cancel
Save