diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 0343d0a46..2bacf7c51 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -48,7 +48,7 @@ |Lzw | | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | |Old Jpeg | | | We should not even try to support this | |Jpeg (Technote 2) | | | | -|Deflate (Technote 2) | | Y | | +|Deflate (Technote 2) | (Y) | Y | Based on PNG Deflate. Deflate encoding only for RGB now, should we allow this for the gray and palette too? | |Old Deflate (Technote 2) | | Y | | ### Photometric Interpretation Formats @@ -59,7 +59,7 @@ |BlackIsZero | | Y | General + 1/4/8-bit optimised implementations | |Rgb (Chunky) | | Y | General + Rgb888 optimised implementation | |Rgb (Planar) | Y | Y | General implementation only | -|PaletteColor | | Y | General implementation only | +|PaletteColor | Y | Y | General implementation only | |TransparencyMask | | | | |Separated (TIFF Extension) | | | | |YCbCr (TIFF Extension) | | | | diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 99b299d04..6dee09932 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -153,10 +153,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff switch (this.Mode) { case TiffEncodingMode.ColorPalette: - imageDataBytes = writer.WritePalettedRgbImageData(image, this.quantizer, this.padding, out colorMap); + imageDataBytes = writer.WritePalettedRgb(image, this.quantizer, this.padding, out colorMap); break; case TiffEncodingMode.Gray: - imageDataBytes = writer.WriteGrayImageData(image, this.padding); + imageDataBytes = writer.WriteGray(image, this.padding); break; default: imageDataBytes = writer.WriteRgbImageData(image, this.padding, this.CompressionType); @@ -350,9 +350,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff { switch (this.PhotometricInterpretation) { - case TiffPhotometricInterpretation.PaletteColor: case TiffPhotometricInterpretation.Rgb: return 3; + case TiffPhotometricInterpretation.PaletteColor: case TiffPhotometricInterpretation.BlackIsZero: return 1; default: diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs index 8ef57f4b2..c95378356 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs @@ -143,30 +143,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff { using IManagedByteBuffer row = this.AllocateRow(image.Width, 3, padding); Span rowSpan = row.GetSpan(); - int bytesWritten = 0; if (compression == TiffEncoderCompression.Deflate) { - using var memoryStream = new MemoryStream(); - - // TODO: move zlib compression from png to a common place? - using var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, PngCompressionLevel.Level6); // TODO: make compression level configurable - - for (int y = 0; y < image.Height; y++) - { - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToRgb24Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); - deflateStream.Write(rowSpan); - } - - deflateStream.Flush(); - - byte[] buffer = memoryStream.ToArray(); - this.output.Write(buffer); - bytesWritten += buffer.Length; - return bytesWritten; + return this.WriteDeflateCompressedRgb(image, rowSpan); } // No compression. + int bytesWritten = 0; for (int y = 0; y < image.Height; y++) { Span pixelRow = image.GetPixelRowSpan(y); @@ -178,6 +161,37 @@ namespace SixLabors.ImageSharp.Formats.Tiff return bytesWritten; } + /// + /// Writes the image data as RGB compressed with zlib to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// A Span for a pixel row. + /// The number of bytes written. + private int WriteDeflateCompressedRgb(Image image, Span rowSpan) + where TPixel : unmanaged, IPixel + { + int bytesWritten = 0; + using var memoryStream = new MemoryStream(); + + // TODO: move zlib compression from png to a common place? + using var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, PngCompressionLevel.Level6); // TODO: make compression level configurable + + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToRgb24Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); + deflateStream.Write(rowSpan); + } + + deflateStream.Flush(); + + byte[] buffer = memoryStream.ToArray(); + this.output.Write(buffer); + bytesWritten += buffer.Length; + return bytesWritten; + } + /// /// Writes the image data as indices into a color map to the stream. /// @@ -187,13 +201,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The padding bytes for each row. /// The color map. /// The number of bytes written. - public int WritePalettedRgbImageData(Image image, IQuantizer quantizer, int padding, out IExifValue colorMap) + public int WritePalettedRgb(Image image, IQuantizer quantizer, int padding, out IExifValue colorMap) where TPixel : unmanaged, IPixel { + int colorPaletteSize = 256 * 3 * 2; using IManagedByteBuffer row = this.AllocateRow(image.Width, 1, padding); using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration); using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image.Frames.RootFrame, image.Bounds()); - using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(256 * 2 * 3); + using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(colorPaletteSize); Span colorPalette = colorPaletteBuffer.GetSpan(); ReadOnlySpan quantizedColors = quantized.Palette.Span; @@ -203,20 +218,27 @@ namespace SixLabors.ImageSharp.Formats.Tiff Span quantizedColorRgb48 = MemoryMarshal.Cast(colorPalette.Slice(0, quantizedColorBytes)); PixelOperations.Instance.ToRgb48(this.configuration, quantizedColors, quantizedColorRgb48); + // It can happen that the quantized colors are less than the expected 256. + var diffToMaxColors = 256 - quantizedColors.Length; + // In a TIFF ColorMap, all the Red values come first, followed by the Green values, // then the Blue values. Convert the quantized palette to this format. - var palette = new ushort[quantizedColorBytes]; + var palette = new ushort[colorPaletteSize]; int paletteIdx = 0; for (int i = 0; i < quantizedColors.Length; i++) { palette[paletteIdx++] = quantizedColorRgb48[i].R; } + paletteIdx += diffToMaxColors; + for (int i = 0; i < quantizedColors.Length; i++) { palette[paletteIdx++] = quantizedColorRgb48[i].G; } + paletteIdx += diffToMaxColors; + for (int i = 0; i < quantizedColors.Length; i++) { palette[paletteIdx++] = quantizedColorRgb48[i].B; @@ -251,7 +273,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The image to write to the stream. /// The padding bytes for each row. /// The number of bytes written. - public int WriteGrayImageData(Image image, int padding) + public int WriteGray(Image image, int padding) where TPixel : unmanaged, IPixel { using IManagedByteBuffer row = this.AllocateRow(image.Width, 1, padding); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index cef8cecc7..9d24132a4 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel8, TiffEncodingMode.Gray); [Theory] - [WithFile(TestImages.Tiff.RgbUncompressed, PixelTypes.Rgba32)] + [WithFile(TestImages.Tiff.Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeColorPalette_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Pixel24, TiffEncodingMode.ColorPalette);