diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index c5283c74b5..d64ac2e7d2 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -165,7 +165,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff switch (this.Mode) { case TiffEncodingMode.ColorPalette: - imageDataBytes = writer.WritePalettedRgb(image, this.quantizer, this.padding, this.CompressionType, out colorMap); + imageDataBytes = writer.WritePalettedRgb(image, this.quantizer, this.padding, this.CompressionType, this.useHorizontalPredictor, out colorMap); break; case TiffEncodingMode.Gray: imageDataBytes = writer.WriteGray(image, this.padding, this.CompressionType, this.useHorizontalPredictor); @@ -346,7 +346,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff if (this.useHorizontalPredictor) { - if (this.Mode == TiffEncodingMode.Rgb || this.Mode == TiffEncodingMode.Gray) + if (this.Mode == TiffEncodingMode.Rgb || this.Mode == TiffEncodingMode.Gray || this.Mode == TiffEncodingMode.ColorPalette) { var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs index d427e9c668..53f763a025 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs @@ -217,7 +217,7 @@ 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. + /// Indicates if horizontal prediction should be used. /// The number of bytes written. private int WriteLzwCompressedRgb(Image image, Span rowSpan, bool useHorizontalPredictor) where TPixel : unmanaged, IPixel @@ -286,9 +286,10 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// The quantizer to use. /// The padding bytes for each row. /// The compression to use. + /// Indicates if horizontal prediction should be used. Should only be used in combination with deflate or LZW compression. /// The color map. /// The number of bytes written. - public int WritePalettedRgb(Image image, IQuantizer quantizer, int padding, TiffEncoderCompression compression, out IExifValue colorMap) + public int WritePalettedRgb(Image image, IQuantizer quantizer, int padding, TiffEncoderCompression compression, bool useHorizontalPredictor, out IExifValue colorMap) where TPixel : unmanaged, IPixel { int colorsPerChannel = 256; @@ -340,7 +341,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils if (compression == TiffEncoderCompression.Deflate) { - return this.WriteDeflateCompressedPalettedRgb(image, quantized, padding); + return this.WriteDeflateCompressedPalettedRgb(image, quantized, padding, useHorizontalPredictor); } if (compression == TiffEncoderCompression.PackBits) @@ -373,18 +374,31 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// The image to write to the stream. /// The quantized frame. /// The padding bytes for each row. + /// Indicates if horizontal prediction should be used. /// The number of bytes written. - public int WriteDeflateCompressedPalettedRgb(Image image, IndexedImageFrame quantized, int padding) + public int WriteDeflateCompressedPalettedRgb(Image image, IndexedImageFrame quantized, int padding, bool useHorizontalPredictor) where TPixel : unmanaged, IPixel { + using IManagedByteBuffer tmpBuffer = this.memoryAllocator.AllocateManagedByteBuffer(image.Width); using var memoryStream = new MemoryStream(); using var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, PngCompressionLevel.Level6); // TODO: make compression level configurable int bytesWritten = 0; for (int y = 0; y < image.Height; y++) { - ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y); - deflateStream.Write(pixelSpan); + ReadOnlySpan pixelRow = quantized.GetPixelRowSpan(y); + if (useHorizontalPredictor) + { + // We need a writable Span here. + Span pixelRowCopy = tmpBuffer.GetSpan(); + pixelRow.CopyTo(pixelRowCopy); + HorizontalPredictor.ApplyHorizontalPrediction8Bit(pixelRowCopy); + deflateStream.Write(pixelRowCopy); + } + else + { + deflateStream.Write(pixelRow); + } for (int i = 0; i < padding; i++) { @@ -423,7 +437,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils { ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y); - int size = 0; + int size; if (padding != 0) { pixelSpan.CopyTo(pixelRowWithPaddingSpan); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 2f0d753884..aa5a84d83f 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -143,6 +143,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff TiffTestUtils.CompareWithReferenceDecoder(encodedImagePath, encodedImage); } + [Theory] + [WithFile(TestImages.Tiff.Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeColorPalette_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + using var memStream = new MemoryStream(); + var encoder = new TiffEncoder { Mode = TiffEncodingMode.ColorPalette, Compression = TiffEncoderCompression.Deflate, UseHorizontalPredictor = true}; + + image.Save(memStream, encoder); + memStream.Position = 0; + + using var encodedImage = (Image)Image.Load(memStream); + var encodedImagePath = provider.Utility.SaveTestOutputFile(encodedImage, "tiff", encoder); + TiffTestUtils.CompareWithReferenceDecoder(encodedImagePath, encodedImage); + } + [Theory] [WithFile(TestImages.Tiff.Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeColorPalette_WithPackBitsCompression_Works(TestImageProvider provider)