From 4e64aabc1dcc52c5b63c8e6677de95affcbd1f65 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 30 Jan 2021 18:11:01 +0300 Subject: [PATCH] Split TiffWriter to the specialized classes --- .../Formats/Tiff/Utils/TiffWriter.cs | 732 +----------------- .../Formats/Tiff/Writers/TiffBiColorWriter.cs | 206 +++++ .../Formats/Tiff/Writers/TiffGrayWriter.cs | 169 ++++ .../Formats/Tiff/Writers/TiffPaletteWriter.cs | 240 ++++++ .../Formats/Tiff/Writers/TiffRgbWriter.cs | 172 ++++ 5 files changed, 811 insertions(+), 708 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter.cs create mode 100644 src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter.cs create mode 100644 src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter.cs create mode 100644 src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter.cs diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs index ebfd119f4..190f7af08 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs @@ -2,19 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.IO; -using System.Runtime.InteropServices; - -using SixLabors.ImageSharp.Compression.Zlib; -using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; -using SixLabors.ImageSharp.Formats.Tiff.Compression; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Dithering; -using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils { @@ -23,25 +12,19 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// internal class TiffWriter : IDisposable { - private readonly Stream output; - - private readonly MemoryAllocator memoryAllocator; - - private readonly Configuration configuration; - - private readonly byte[] paddingBytes = new byte[4]; + private static readonly byte[] PaddingBytes = new byte[4]; /// /// Initializes a new instance of the class. /// /// The output stream. - /// The memory allocator. + /// The memory allocator. /// The configuration. - public TiffWriter(Stream output, MemoryAllocator memoryMemoryAllocator, Configuration configuration) + public TiffWriter(Stream output, MemoryAllocator memoryAllocator, Configuration configuration) { - this.output = output; - this.memoryAllocator = memoryMemoryAllocator; - this.configuration = configuration; + this.Output = output; + this.MemoryAllocator = memoryAllocator; + this.Configuration = configuration; } /// @@ -52,7 +35,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// /// Gets the current position within the stream. /// - public long Position => this.output.Position; + public long Position => this.Output.Position; + + protected Stream Output { get; } + + protected MemoryAllocator MemoryAllocator { get; } + + protected Configuration Configuration { get; } /// /// Writes an empty four bytes to the stream, returning the offset to be written later. @@ -60,7 +49,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// The offset to be written later public long PlaceMarker() { - long offset = this.output.Position; + long offset = this.Output.Position; this.Write(0u); return offset; } @@ -69,13 +58,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// Writes an array of bytes to the current stream. /// /// The bytes to write. - public void Write(byte[] value) => this.output.Write(value, 0, value.Length); + public void Write(byte[] value) => this.Output.Write(value, 0, value.Length); /// /// Writes a byte to the current stream. /// /// The byte to write. - public void Write(byte value) => this.output.Write(new[] { value }, 0, 1); + public void Write(byte value) => this.Output.Write(new byte[] { value }, 0, 1); /// /// Writes a two-byte unsigned integer to the current stream. @@ -84,7 +73,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils public void Write(ushort value) { byte[] bytes = BitConverter.GetBytes(value); - this.output.Write(bytes, 0, 2); + this.Output.Write(bytes, 0, 2); } /// @@ -94,7 +83,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils public void Write(uint value) { byte[] bytes = BitConverter.GetBytes(value); - this.output.Write(bytes, 0, 4); + this.Output.Write(bytes, 0, 4); } /// @@ -103,11 +92,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// The bytes to write. public void WritePadded(byte[] value) { - this.output.Write(value, 0, value.Length); + this.Output.Write(value, 0, value.Length); if (value.Length < 4) { - this.output.Write(this.paddingBytes, 0, 4 - value.Length); + this.Output.Write(PaddingBytes, 0, 4 - value.Length); } } @@ -118,688 +107,15 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils /// The four-byte unsigned integer to write. public void WriteMarker(long offset, uint value) { - long currentOffset = this.output.Position; - this.output.Seek(offset, SeekOrigin.Begin); + long currentOffset = this.Output.Position; + this.Output.Seek(offset, SeekOrigin.Begin); this.Write(value); - this.output.Seek(currentOffset, SeekOrigin.Begin); - } - - /// - /// Writes the image data as RGB to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// The compression to use. - /// The compression level for deflate compression. - /// Indicates if horizontal prediction should be used. Should only be used with deflate compression. - /// The number of bytes written. - public int WriteRgb(Image image, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor) - where TPixel : unmanaged, IPixel - { - using IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(image.Width * 3); - Span rowSpan = row.GetSpan(); - if (compression == TiffEncoderCompression.Deflate) - { - return this.WriteDeflateCompressedRgb(image, rowSpan, compressionLevel, useHorizontalPredictor); - } - - if (compression == TiffEncoderCompression.Lzw) - { - return this.WriteLzwCompressedRgb(image, rowSpan, useHorizontalPredictor); - } - - if (compression == TiffEncoderCompression.PackBits) - { - return this.WriteRgbPackBitsCompressed(image, rowSpan); - } - - // No compression. - int bytesWritten = 0; - for (int y = 0; y < image.Height; y++) - { - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToRgb24Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); - this.output.Write(rowSpan); - bytesWritten += rowSpan.Length; - } - - 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 compression level for deflate compression. - /// 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, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor) - where TPixel : unmanaged, IPixel - { - int bytesWritten = 0; - using var memoryStream = new MemoryStream(); - using var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, compressionLevel); - - for (int y = 0; y < image.Height; y++) - { - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToRgb24Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); - - if (useHorizontalPredictor) - { - HorizontalPredictor.ApplyHorizontalPrediction24Bit(rowSpan); - } - - deflateStream.Write(rowSpan); - } - - deflateStream.Flush(); - - byte[] buffer = memoryStream.ToArray(); - this.output.Write(buffer); - bytesWritten += buffer.Length; - return bytesWritten; - } - - /// - /// Writes the image data as RGB compressed with lzw to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// A Span for a pixel row. - /// 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 - { - int bytesWritten = 0; - using var memoryStream = new MemoryStream(); - - IMemoryOwner pixelData = this.memoryAllocator.Allocate(image.Width * image.Height * 3); - Span pixels = pixelData.GetSpan(); - for (int y = 0; y < image.Height; y++) - { - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToRgb24Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); - - if (useHorizontalPredictor) - { - HorizontalPredictor.ApplyHorizontalPrediction24Bit(rowSpan); - } - - rowSpan.CopyTo(pixels.Slice(y * image.Width * 3)); - } - - using var lzwEncoder = new TiffLzwEncoder(this.memoryAllocator, pixelData); - lzwEncoder.Encode(memoryStream); - - byte[] buffer = memoryStream.ToArray(); - this.output.Write(buffer); - bytesWritten += buffer.Length; - return bytesWritten; - } - - /// - /// Writes the image data as RGB with packed bits compression 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 WriteRgbPackBitsCompressed(Image image, Span rowSpan) - 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; - using IManagedByteBuffer compressedRow = this.memoryAllocator.AllocateManagedByteBuffer((image.Width * 3) + additionalBytes, AllocationOptions.Clean); - Span compressedRowSpan = compressedRow.GetSpan(); - int bytesWritten = 0; - - for (int y = 0; y < image.Height; y++) - { - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToRgb24Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); - int size = PackBitsWriter.PackBits(rowSpan, compressedRowSpan); - this.output.Write(compressedRow.Slice(0, size)); - bytesWritten += size; - compressedRowSpan.Clear(); - } - - return bytesWritten; - } - - /// - /// Writes the image data as indices into a color map to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// The quantizer to use. - /// The compression to use. - /// The compression level for deflate compression. - /// Indicates if horizontal prediction should be used. Should only be used in combination with deflate or LZW compression. - /// The entries collector. - /// The number of bytes written. - public int WritePalettedRgb(Image image, IQuantizer quantizer, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor, TiffEncoderEntriesCollector entriesCollector) - where TPixel : unmanaged, IPixel - { - int colorsPerChannel = 256; - int colorPaletteSize = colorsPerChannel * 3; - int colorPaletteBytes = colorPaletteSize * 2; - using IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(image.Width); - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration); - using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image.Frames.RootFrame, image.Bounds()); - using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(colorPaletteBytes); - Span colorPalette = colorPaletteBuffer.GetSpan(); - - ReadOnlySpan quantizedColors = quantized.Palette.Span; - int quantizedColorBytes = quantizedColors.Length * 3 * 2; - - // In the ColorMap, black is represented by 0,0,0 and white is represented by 65535, 65535, 65535. - 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 per channel. - var diffToMaxColors = colorsPerChannel - 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[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; - } - - var colorMap = new ExifShortArray(ExifTagValue.ColorMap) - { - Value = palette - }; - - entriesCollector.Add(colorMap); - - if (compression == TiffEncoderCompression.Deflate) - { - return this.WriteDeflateCompressedPalettedRgb(image, quantized, compressionLevel, useHorizontalPredictor); - } - - if (compression == TiffEncoderCompression.Lzw) - { - return this.WriteLzwCompressedPalettedRgb(image, quantized, useHorizontalPredictor); - } - - if (compression == TiffEncoderCompression.PackBits) - { - return this.WritePackBitsCompressedPalettedRgb(image, quantized); - } - - // No compression. - int bytesWritten = 0; - for (int y = 0; y < image.Height; y++) - { - ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y); - this.output.Write(pixelSpan); - bytesWritten += pixelSpan.Length; - } - - return bytesWritten; - } - - /// - /// Writes the image data as indices into a color map compressed with deflate compression to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// The quantized frame. - /// The compression level for deflate compression. - /// Indicates if horizontal prediction should be used. - /// The number of bytes written. - public int WriteDeflateCompressedPalettedRgb(Image image, IndexedImageFrame quantized, DeflateCompressionLevel compressionLevel, 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, compressionLevel); - - int bytesWritten = 0; - for (int y = 0; y < image.Height; y++) - { - 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); - } - } - - 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 compressed with lzw compression to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// The quantized frame. - /// Indicates if horizontal prediction should be used. - /// The number of bytes written. - public int WriteLzwCompressedPalettedRgb(Image image, IndexedImageFrame quantized, bool useHorizontalPredictor) - where TPixel : unmanaged, IPixel - { - IMemoryOwner pixelData = this.memoryAllocator.Allocate(image.Width * image.Height); - using IManagedByteBuffer tmpBuffer = this.memoryAllocator.AllocateManagedByteBuffer(image.Width); - using var memoryStream = new MemoryStream(); - - int bytesWritten = 0; - Span pixels = pixelData.GetSpan(); - for (int y = 0; y < image.Height; y++) - { - ReadOnlySpan indexedPixelRow = quantized.GetPixelRowSpan(y); - - if (useHorizontalPredictor) - { - // We need a writable Span here. - Span pixelRowCopy = tmpBuffer.GetSpan(); - indexedPixelRow.CopyTo(pixelRowCopy); - HorizontalPredictor.ApplyHorizontalPrediction8Bit(pixelRowCopy); - pixelRowCopy.CopyTo(pixels.Slice(y * image.Width)); - } - else - { - indexedPixelRow.CopyTo(pixels.Slice(y * image.Width)); - } - } - - using var lzwEncoder = new TiffLzwEncoder(this.memoryAllocator, pixelData); - lzwEncoder.Encode(memoryStream); - - byte[] buffer = memoryStream.ToArray(); - this.output.Write(buffer); - bytesWritten += buffer.Length; - - return bytesWritten; - } - - /// - /// Writes the image data as indices into a color map compressed with deflate compression to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// The quantized frame. - /// The number of bytes written. - public int WritePackBitsCompressedPalettedRgb(Image image, IndexedImageFrame quantized) - 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; - using IManagedByteBuffer compressedRow = this.memoryAllocator.AllocateManagedByteBuffer((image.Width * 3) + additionalBytes, AllocationOptions.Clean); - Span compressedRowSpan = compressedRow.GetSpan(); - - int bytesWritten = 0; - for (int y = 0; y < image.Height; y++) - { - ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y); - - int size = PackBitsWriter.PackBits(pixelSpan, compressedRowSpan); - this.output.Write(compressedRowSpan.Slice(0, size)); - bytesWritten += size; - } - - return bytesWritten; - } - - /// - /// Writes the image data as 8 bit gray to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// The compression to use. - /// The compression level for deflate compression. - /// Indicates if horizontal prediction should be used. Should only be used with deflate or lzw compression. - /// The number of bytes written. - public int WriteGray(Image image, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor) - where TPixel : unmanaged, IPixel - { - using IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(image.Width); - Span rowSpan = row.GetSpan(); - - if (compression == TiffEncoderCompression.Deflate) - { - return this.WriteGrayDeflateCompressed(image, rowSpan, compressionLevel, useHorizontalPredictor); - } - - if (compression == TiffEncoderCompression.Lzw) - { - return this.WriteGrayLzwCompressed(image, rowSpan, useHorizontalPredictor); - } - - if (compression == TiffEncoderCompression.PackBits) - { - return this.WriteGrayPackBitsCompressed(image, rowSpan); - } - - int bytesWritten = 0; - for (int y = 0; y < image.Height; y++) - { - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToL8Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); - this.output.Write(rowSpan); - bytesWritten += rowSpan.Length; - } - - return bytesWritten; - } - - /// - /// Writes the image data as 8 bit gray with deflate compression to the stream. - /// - /// The image to write to the stream. - /// A span of a row of pixels. - /// The compression level for deflate compression. - /// Indicates if horizontal prediction should be used. - /// The number of bytes written. - private int WriteGrayDeflateCompressed(Image image, Span rowSpan, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor) - where TPixel : unmanaged, IPixel - { - int bytesWritten = 0; - using var memoryStream = new MemoryStream(); - using var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, compressionLevel); - - for (int y = 0; y < image.Height; y++) - { - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToL8Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); - - if (useHorizontalPredictor) - { - HorizontalPredictor.ApplyHorizontalPrediction8Bit(rowSpan); - } - - deflateStream.Write(rowSpan); - } - - deflateStream.Flush(); - - byte[] buffer = memoryStream.ToArray(); - this.output.Write(buffer); - bytesWritten += buffer.Length; - return bytesWritten; - } - - /// - /// Writes the image data as 8 bit gray with lzw compression to the stream. - /// - /// The image to write to the stream. - /// A span of a row of pixels. - /// Indicates if horizontal prediction should be used. - /// The number of bytes written. - private int WriteGrayLzwCompressed(Image image, Span rowSpan, bool useHorizontalPredictor) - where TPixel : unmanaged, IPixel - { - int bytesWritten = 0; - using var memoryStream = new MemoryStream(); - - IMemoryOwner pixelData = this.memoryAllocator.Allocate(image.Width * image.Height); - Span pixels = pixelData.GetSpan(); - for (int y = 0; y < image.Height; y++) - { - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToL8Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); - if (useHorizontalPredictor) - { - HorizontalPredictor.ApplyHorizontalPrediction8Bit(rowSpan); - } - - rowSpan.CopyTo(pixels.Slice(y * image.Width)); - } - - using var lzwEncoder = new TiffLzwEncoder(this.memoryAllocator, pixelData); - lzwEncoder.Encode(memoryStream); - - byte[] buffer = memoryStream.ToArray(); - this.output.Write(buffer); - bytesWritten += buffer.Length; - return bytesWritten; - } - - /// - /// Writes the image data as 8 bit gray to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// A span of a row of pixels. - /// The number of bytes written. - private int WriteGrayPackBitsCompressed(Image image, Span rowSpan) - 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 / 127) + 1; - using IManagedByteBuffer compressedRow = this.memoryAllocator.AllocateManagedByteBuffer(image.Width + additionalBytes, AllocationOptions.Clean); - Span compressedRowSpan = compressedRow.GetSpan(); - - int bytesWritten = 0; - for (int y = 0; y < image.Height; y++) - { - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToL8Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); - int size = PackBitsWriter.PackBits(rowSpan, compressedRowSpan); - this.output.Write(compressedRow.Slice(0, size)); - bytesWritten += size; - compressedRowSpan.Clear(); - } - - return bytesWritten; - } - - /// - /// Writes the image data as 1 bit black and white to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// The compression to use. - /// The compression level for deflate compression. - /// The number of bytes written. - public int WriteBiColor(Image image, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel) - where TPixel : unmanaged, IPixel - { - int padding = image.Width % 8 == 0 ? 0 : 1; - int bytesPerRow = (image.Width / 8) + padding; - using IMemoryOwner pixelRowAsGray = this.memoryAllocator.Allocate(image.Width); - using IManagedByteBuffer row = this.memoryAllocator.AllocateManagedByteBuffer(bytesPerRow, AllocationOptions.Clean); - Span outputRow = row.GetSpan(); - Span pixelRowAsGraySpan = pixelRowAsGray.GetSpan(); - - // Convert image to black and white. - // TODO: Should we allow to skip this by the user, if its known to be black and white already? - using Image imageBlackWhite = image.Clone(); - imageBlackWhite.Mutate(img => img.BinaryDither(default(ErrorDither))); - - if (compression == TiffEncoderCompression.Deflate) - { - return this.WriteBiColorDeflate(imageBlackWhite, pixelRowAsGraySpan, outputRow, compressionLevel); - } - - if (compression == TiffEncoderCompression.PackBits) - { - return this.WriteBiColorPackBits(imageBlackWhite, pixelRowAsGraySpan, outputRow); - } - - if (compression == TiffEncoderCompression.CcittGroup3Fax) - { - var bitWriter = new T4BitWriter(this.memoryAllocator, this.configuration); - return bitWriter.CompressImage(imageBlackWhite, pixelRowAsGraySpan, this.output); - } - - if (compression == TiffEncoderCompression.ModifiedHuffman) - { - var bitWriter = new T4BitWriter(this.memoryAllocator, this.configuration, useModifiedHuffman: true); - return bitWriter.CompressImage(imageBlackWhite, pixelRowAsGraySpan, this.output); - } - - // Write image uncompressed. - int bytesWritten = 0; - for (int y = 0; y < image.Height; y++) - { - int bitIndex = 0; - int byteIndex = 0; - Span pixelRow = imageBlackWhite.GetPixelRowSpan(y); - PixelOperations.Instance.ToL8(this.configuration, pixelRow, pixelRowAsGraySpan); - for (int x = 0; x < pixelRow.Length; x++) - { - int shift = 7 - bitIndex; - if (pixelRowAsGraySpan[x].PackedValue == 255) - { - outputRow[byteIndex] |= (byte)(1 << shift); - } - - bitIndex++; - if (bitIndex == 8) - { - byteIndex++; - bitIndex = 0; - } - } - - this.output.Write(row); - bytesWritten += row.Length(); - - row.Clear(); - } - - return bytesWritten; - } - - /// - /// Writes the image data as 1 bit black and white with deflate compression to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// A span for converting a pixel row to gray. - /// A span which will be used to store the output pixels. - /// The compression level for deflate compression. - /// The number of bytes written. - public int WriteBiColorDeflate(Image image, Span pixelRowAsGraySpan, Span outputRow, DeflateCompressionLevel compressionLevel) - where TPixel : unmanaged, IPixel - { - using var memoryStream = new MemoryStream(); - using var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, compressionLevel); - - int bytesWritten = 0; - for (int y = 0; y < image.Height; y++) - { - int bitIndex = 0; - int byteIndex = 0; - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToL8(this.configuration, pixelRow, pixelRowAsGraySpan); - for (int x = 0; x < pixelRow.Length; x++) - { - int shift = 7 - bitIndex; - if (pixelRowAsGraySpan[x].PackedValue == 255) - { - outputRow[byteIndex] |= (byte)(1 << shift); - } - - bitIndex++; - if (bitIndex == 8) - { - byteIndex++; - bitIndex = 0; - } - } - - deflateStream.Write(outputRow); - - outputRow.Clear(); - } - - deflateStream.Flush(); - byte[] buffer = memoryStream.ToArray(); - this.output.Write(buffer); - bytesWritten += buffer.Length; - - return bytesWritten; - } - - /// - /// Writes the image data as 1 bit black and white with pack bits compression to the stream. - /// - /// The pixel data. - /// The image to write to the stream. - /// A span for converting a pixel row to gray. - /// A span which will be used to store the output pixels. - /// The number of bytes written. - public int WriteBiColorPackBits(Image image, Span pixelRowAsGraySpan, Span outputRow) - 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 bits. - int additionalBytes = (image.Width / 127) + 2; - int compressedRowBytes = (image.Width / 8) + additionalBytes; - using IManagedByteBuffer compressedRow = this.memoryAllocator.AllocateManagedByteBuffer(compressedRowBytes, AllocationOptions.Clean); - Span compressedRowSpan = compressedRow.GetSpan(); - - int bytesWritten = 0; - for (int y = 0; y < image.Height; y++) - { - int bitIndex = 0; - int byteIndex = 0; - Span pixelRow = image.GetPixelRowSpan(y); - PixelOperations.Instance.ToL8(this.configuration, pixelRow, pixelRowAsGraySpan); - for (int x = 0; x < pixelRow.Length; x++) - { - int shift = 7 - bitIndex; - if (pixelRowAsGraySpan[x].PackedValue == 255) - { - outputRow[byteIndex] |= (byte)(1 << shift); - } - - bitIndex++; - if (bitIndex == 8) - { - byteIndex++; - bitIndex = 0; - } - } - - var size = PackBitsWriter.PackBits(outputRow, compressedRowSpan); - this.output.Write(compressedRowSpan.Slice(0, size)); - bytesWritten += size; - - outputRow.Clear(); - } - - return bytesWritten; + this.Output.Seek(currentOffset, SeekOrigin.Begin); } /// /// Disposes instance, ensuring any unwritten data is flushed. /// - public void Dispose() => this.output.Flush(); + public void Dispose() => this.Output.Flush(); } } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter.cs new file mode 100644 index 000000000..60df87da3 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter.cs @@ -0,0 +1,206 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Dithering; + +namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils +{ + /// + /// Utility class for writing TIFF data to a . + /// + internal class TiffBiColorWriter : TiffWriter + { + public TiffBiColorWriter(Stream output, MemoryAllocator memoryAllocator, Configuration configuration) + : base(output, memoryAllocator, configuration) + { + } + + /// + /// Writes the image data as 1 bit black and white to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// The compression to use. + /// The compression level for deflate compression. + /// The number of bytes written. + public int WriteBiColor(Image image, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel) + where TPixel : unmanaged, IPixel + { + int padding = image.Width % 8 == 0 ? 0 : 1; + int bytesPerRow = (image.Width / 8) + padding; + using IMemoryOwner pixelRowAsGray = this.MemoryAllocator.Allocate(image.Width); + using IManagedByteBuffer row = this.MemoryAllocator.AllocateManagedByteBuffer(bytesPerRow, AllocationOptions.Clean); + Span outputRow = row.GetSpan(); + Span pixelRowAsGraySpan = pixelRowAsGray.GetSpan(); + + // Convert image to black and white. + // TODO: Should we allow to skip this by the user, if its known to be black and white already? + using Image imageBlackWhite = image.Clone(); + imageBlackWhite.Mutate(img => img.BinaryDither(default(ErrorDither))); + + if (compression == TiffEncoderCompression.Deflate) + { + return this.WriteBiColorDeflate(imageBlackWhite, pixelRowAsGraySpan, outputRow, compressionLevel); + } + + if (compression == TiffEncoderCompression.PackBits) + { + return this.WriteBiColorPackBits(imageBlackWhite, pixelRowAsGraySpan, outputRow); + } + + if (compression == TiffEncoderCompression.CcittGroup3Fax) + { + var bitWriter = new T4BitWriter(this.MemoryAllocator, this.Configuration); + return bitWriter.CompressImage(imageBlackWhite, pixelRowAsGraySpan, this.Output); + } + + if (compression == TiffEncoderCompression.ModifiedHuffman) + { + var bitWriter = new T4BitWriter(this.MemoryAllocator, this.Configuration, useModifiedHuffman: true); + return bitWriter.CompressImage(imageBlackWhite, pixelRowAsGraySpan, this.Output); + } + + // Write image uncompressed. + int bytesWritten = 0; + for (int y = 0; y < image.Height; y++) + { + int bitIndex = 0; + int byteIndex = 0; + Span pixelRow = imageBlackWhite.GetPixelRowSpan(y); + PixelOperations.Instance.ToL8(this.Configuration, pixelRow, pixelRowAsGraySpan); + for (int x = 0; x < pixelRow.Length; x++) + { + int shift = 7 - bitIndex; + if (pixelRowAsGraySpan[x].PackedValue == 255) + { + outputRow[byteIndex] |= (byte)(1 << shift); + } + + bitIndex++; + if (bitIndex == 8) + { + byteIndex++; + bitIndex = 0; + } + } + + this.Output.Write(row); + bytesWritten += row.Length(); + + row.Clear(); + } + + return bytesWritten; + } + + /// + /// Writes the image data as 1 bit black and white with deflate compression to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// A span for converting a pixel row to gray. + /// A span which will be used to store the output pixels. + /// The compression level for deflate compression. + /// The number of bytes written. + public int WriteBiColorDeflate(Image image, Span pixelRowAsGraySpan, Span outputRow, DeflateCompressionLevel compressionLevel) + where TPixel : unmanaged, IPixel + { + using var memoryStream = new MemoryStream(); + using var deflateStream = new ZlibDeflateStream(this.MemoryAllocator, memoryStream, compressionLevel); + + int bytesWritten = 0; + for (int y = 0; y < image.Height; y++) + { + int bitIndex = 0; + int byteIndex = 0; + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToL8(this.Configuration, pixelRow, pixelRowAsGraySpan); + for (int x = 0; x < pixelRow.Length; x++) + { + int shift = 7 - bitIndex; + if (pixelRowAsGraySpan[x].PackedValue == 255) + { + outputRow[byteIndex] |= (byte)(1 << shift); + } + + bitIndex++; + if (bitIndex == 8) + { + byteIndex++; + bitIndex = 0; + } + } + + deflateStream.Write(outputRow); + + outputRow.Clear(); + } + + deflateStream.Flush(); + byte[] buffer = memoryStream.ToArray(); + this.Output.Write(buffer); + bytesWritten += buffer.Length; + + return bytesWritten; + } + + /// + /// Writes the image data as 1 bit black and white with pack bits compression to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// A span for converting a pixel row to gray. + /// A span which will be used to store the output pixels. + /// The number of bytes written. + public int WriteBiColorPackBits(Image image, Span pixelRowAsGraySpan, Span outputRow) + 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 bits. + int additionalBytes = (image.Width / 127) + 2; + int compressedRowBytes = (image.Width / 8) + additionalBytes; + using IManagedByteBuffer compressedRow = this.MemoryAllocator.AllocateManagedByteBuffer(compressedRowBytes, AllocationOptions.Clean); + Span compressedRowSpan = compressedRow.GetSpan(); + + int bytesWritten = 0; + for (int y = 0; y < image.Height; y++) + { + int bitIndex = 0; + int byteIndex = 0; + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToL8(this.Configuration, pixelRow, pixelRowAsGraySpan); + for (int x = 0; x < pixelRow.Length; x++) + { + int shift = 7 - bitIndex; + if (pixelRowAsGraySpan[x].PackedValue == 255) + { + outputRow[byteIndex] |= (byte)(1 << shift); + } + + bitIndex++; + if (bitIndex == 8) + { + byteIndex++; + bitIndex = 0; + } + } + + var size = PackBitsWriter.PackBits(outputRow, compressedRowSpan); + this.Output.Write(compressedRowSpan.Slice(0, size)); + bytesWritten += size; + + outputRow.Clear(); + } + + return bytesWritten; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter.cs new file mode 100644 index 000000000..a611b8832 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils +{ + /// + /// Utility class for writing TIFF data to a . + /// + internal class TiffGrayWriter : TiffWriter + { + public TiffGrayWriter(Stream output, MemoryAllocator memoryMemoryAllocator, Configuration configuration) + : base(output, memoryMemoryAllocator, configuration) + { + } + + /// + /// Writes the image data as 8 bit gray to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// The compression to use. + /// The compression level for deflate compression. + /// Indicates if horizontal prediction should be used. Should only be used with deflate or lzw compression. + /// The number of bytes written. + public int WriteGray(Image image, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor) + where TPixel : unmanaged, IPixel + { + using IManagedByteBuffer row = this.MemoryAllocator.AllocateManagedByteBuffer(image.Width); + Span rowSpan = row.GetSpan(); + + if (compression == TiffEncoderCompression.Deflate) + { + return this.WriteGrayDeflateCompressed(image, rowSpan, compressionLevel, useHorizontalPredictor); + } + + if (compression == TiffEncoderCompression.Lzw) + { + return this.WriteGrayLzwCompressed(image, rowSpan, useHorizontalPredictor); + } + + if (compression == TiffEncoderCompression.PackBits) + { + return this.WriteGrayPackBitsCompressed(image, rowSpan); + } + + int bytesWritten = 0; + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelRow, rowSpan, pixelRow.Length); + this.Output.Write(rowSpan); + bytesWritten += rowSpan.Length; + } + + return bytesWritten; + } + + /// + /// Writes the image data as 8 bit gray with deflate compression to the stream. + /// + /// The image to write to the stream. + /// A span of a row of pixels. + /// The compression level for deflate compression. + /// Indicates if horizontal prediction should be used. + /// The number of bytes written. + private int WriteGrayDeflateCompressed(Image image, Span rowSpan, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor) + where TPixel : unmanaged, IPixel + { + int bytesWritten = 0; + using var memoryStream = new MemoryStream(); + using var deflateStream = new ZlibDeflateStream(this.MemoryAllocator, memoryStream, compressionLevel); + + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelRow, rowSpan, pixelRow.Length); + + if (useHorizontalPredictor) + { + HorizontalPredictor.ApplyHorizontalPrediction8Bit(rowSpan); + } + + deflateStream.Write(rowSpan); + } + + deflateStream.Flush(); + + byte[] buffer = memoryStream.ToArray(); + this.Output.Write(buffer); + bytesWritten += buffer.Length; + return bytesWritten; + } + + /// + /// Writes the image data as 8 bit gray with lzw compression to the stream. + /// + /// The image to write to the stream. + /// A span of a row of pixels. + /// Indicates if horizontal prediction should be used. + /// The number of bytes written. + private int WriteGrayLzwCompressed(Image image, Span rowSpan, bool useHorizontalPredictor) + where TPixel : unmanaged, IPixel + { + int bytesWritten = 0; + using var memoryStream = new MemoryStream(); + + IMemoryOwner pixelData = this.MemoryAllocator.Allocate(image.Width * image.Height); + Span pixels = pixelData.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelRow, rowSpan, pixelRow.Length); + if (useHorizontalPredictor) + { + HorizontalPredictor.ApplyHorizontalPrediction8Bit(rowSpan); + } + + rowSpan.CopyTo(pixels.Slice(y * image.Width)); + } + + using var lzwEncoder = new TiffLzwEncoder(this.MemoryAllocator, pixelData); + lzwEncoder.Encode(memoryStream); + + byte[] buffer = memoryStream.ToArray(); + this.Output.Write(buffer); + bytesWritten += buffer.Length; + return bytesWritten; + } + + /// + /// Writes the image data as 8 bit gray to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// A span of a row of pixels. + /// The number of bytes written. + private int WriteGrayPackBitsCompressed(Image image, Span rowSpan) + 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 / 127) + 1; + using IManagedByteBuffer compressedRow = this.MemoryAllocator.AllocateManagedByteBuffer(image.Width + additionalBytes, AllocationOptions.Clean); + Span compressedRowSpan = compressedRow.GetSpan(); + + int bytesWritten = 0; + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelRow, rowSpan, pixelRow.Length); + int size = PackBitsWriter.PackBits(rowSpan, compressedRowSpan); + this.Output.Write(compressedRow.Slice(0, size)); + bytesWritten += size; + compressedRowSpan.Clear(); + } + + return bytesWritten; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter.cs new file mode 100644 index 000000000..bef5fe1bc --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter.cs @@ -0,0 +1,240 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.InteropServices; + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils +{ + /// + /// Utility class for writing TIFF data to a . + /// + internal class TiffPaletteWriter : TiffWriter + { + /// + /// Initializes a new instance of the class. + /// + /// The output stream. + /// The memory allocator. + /// The configuration. + public TiffPaletteWriter(Stream output, MemoryAllocator memoryMemoryAllocator, Configuration configuration) + : base(output, memoryMemoryAllocator, configuration) + { + } + + /// + /// Writes the image data as indices into a color map to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// The quantizer to use. + /// The compression to use. + /// The compression level for deflate compression. + /// Indicates if horizontal prediction should be used. Should only be used in combination with deflate or LZW compression. + /// The entries collector. + /// The number of bytes written. + public int WritePalettedRgb(Image image, IQuantizer quantizer, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor, TiffEncoderEntriesCollector entriesCollector) + where TPixel : unmanaged, IPixel + { + int colorsPerChannel = 256; + int colorPaletteSize = colorsPerChannel * 3; + int colorPaletteBytes = colorPaletteSize * 2; + using IManagedByteBuffer row = this.MemoryAllocator.AllocateManagedByteBuffer(image.Width); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.Configuration); + using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image.Frames.RootFrame, image.Bounds()); + using IMemoryOwner colorPaletteBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(colorPaletteBytes); + Span colorPalette = colorPaletteBuffer.GetSpan(); + + ReadOnlySpan quantizedColors = quantized.Palette.Span; + int quantizedColorBytes = quantizedColors.Length * 3 * 2; + + // In the ColorMap, black is represented by 0,0,0 and white is represented by 65535, 65535, 65535. + 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 per channel. + var diffToMaxColors = colorsPerChannel - 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[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; + } + + var colorMap = new ExifShortArray(ExifTagValue.ColorMap) + { + Value = palette + }; + + entriesCollector.Add(colorMap); + + if (compression == TiffEncoderCompression.Deflate) + { + return this.WriteDeflateCompressedPalettedRgb(image, quantized, compressionLevel, useHorizontalPredictor); + } + + if (compression == TiffEncoderCompression.Lzw) + { + return this.WriteLzwCompressedPalettedRgb(image, quantized, useHorizontalPredictor); + } + + if (compression == TiffEncoderCompression.PackBits) + { + return this.WritePackBitsCompressedPalettedRgb(image, quantized); + } + + // No compression. + int bytesWritten = 0; + for (int y = 0; y < image.Height; y++) + { + ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y); + this.Output.Write(pixelSpan); + bytesWritten += pixelSpan.Length; + } + + return bytesWritten; + } + + /// + /// Writes the image data as indices into a color map compressed with deflate compression to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// The quantized frame. + /// The compression level for deflate compression. + /// Indicates if horizontal prediction should be used. + /// The number of bytes written. + private int WriteDeflateCompressedPalettedRgb(Image image, IndexedImageFrame quantized, DeflateCompressionLevel compressionLevel, 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, compressionLevel); + + int bytesWritten = 0; + for (int y = 0; y < image.Height; y++) + { + 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); + } + } + + 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 compressed with lzw compression to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// The quantized frame. + /// Indicates if horizontal prediction should be used. + /// The number of bytes written. + private int WriteLzwCompressedPalettedRgb(Image image, IndexedImageFrame quantized, bool useHorizontalPredictor) + where TPixel : unmanaged, IPixel + { + IMemoryOwner pixelData = this.MemoryAllocator.Allocate(image.Width * image.Height); + using IManagedByteBuffer tmpBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(image.Width); + using var memoryStream = new MemoryStream(); + + int bytesWritten = 0; + Span pixels = pixelData.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + ReadOnlySpan indexedPixelRow = quantized.GetPixelRowSpan(y); + + if (useHorizontalPredictor) + { + // We need a writable Span here. + Span pixelRowCopy = tmpBuffer.GetSpan(); + indexedPixelRow.CopyTo(pixelRowCopy); + HorizontalPredictor.ApplyHorizontalPrediction8Bit(pixelRowCopy); + pixelRowCopy.CopyTo(pixels.Slice(y * image.Width)); + } + else + { + indexedPixelRow.CopyTo(pixels.Slice(y * image.Width)); + } + } + + using var lzwEncoder = new TiffLzwEncoder(this.MemoryAllocator, pixelData); + lzwEncoder.Encode(memoryStream); + + byte[] buffer = memoryStream.ToArray(); + this.Output.Write(buffer); + bytesWritten += buffer.Length; + + return bytesWritten; + } + + /// + /// Writes the image data as indices into a color map compressed with deflate compression to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// The quantized frame. + /// The number of bytes written. + private int WritePackBitsCompressedPalettedRgb(Image image, IndexedImageFrame quantized) + 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; + using IManagedByteBuffer compressedRow = this.MemoryAllocator.AllocateManagedByteBuffer((image.Width * 3) + additionalBytes, AllocationOptions.Clean); + Span compressedRowSpan = compressedRow.GetSpan(); + + int bytesWritten = 0; + for (int y = 0; y < image.Height; y++) + { + ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y); + + int size = PackBitsWriter.PackBits(pixelSpan, compressedRowSpan); + this.Output.Write(compressedRowSpan.Slice(0, size)); + bytesWritten += size; + } + + return bytesWritten; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter.cs new file mode 100644 index 000000000..b72e923b4 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter.cs @@ -0,0 +1,172 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Utils +{ + /// + /// Utility class for writing TIFF data to a . + /// + internal class TiffRgbWriter : TiffWriter + { + public TiffRgbWriter(Stream output, MemoryAllocator memoryMemoryAllocator, Configuration configuration) + : base(output, memoryMemoryAllocator, configuration) + { + } + + /// + /// Writes the image data as RGB to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// The compression to use. + /// The compression level for deflate compression. + /// Indicates if horizontal prediction should be used. Should only be used with deflate compression. + /// The number of bytes written. + public int WriteRgb(Image image, TiffEncoderCompression compression, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor) + where TPixel : unmanaged, IPixel + { + using IManagedByteBuffer row = this.MemoryAllocator.AllocateManagedByteBuffer(image.Width * 3); + Span rowSpan = row.GetSpan(); + if (compression == TiffEncoderCompression.Deflate) + { + return this.WriteDeflateCompressedRgb(image, rowSpan, compressionLevel, useHorizontalPredictor); + } + + if (compression == TiffEncoderCompression.Lzw) + { + return this.WriteLzwCompressedRgb(image, rowSpan, useHorizontalPredictor); + } + + if (compression == TiffEncoderCompression.PackBits) + { + return this.WriteRgbPackBitsCompressed(image, rowSpan); + } + + // No compression. + int bytesWritten = 0; + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixelRow, rowSpan, pixelRow.Length); + this.Output.Write(rowSpan); + bytesWritten += rowSpan.Length; + } + + 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 compression level for deflate compression. + /// 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, DeflateCompressionLevel compressionLevel, bool useHorizontalPredictor) + where TPixel : unmanaged, IPixel + { + int bytesWritten = 0; + using var memoryStream = new MemoryStream(); + using var deflateStream = new ZlibDeflateStream(this.MemoryAllocator, memoryStream, compressionLevel); + + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixelRow, rowSpan, pixelRow.Length); + + if (useHorizontalPredictor) + { + HorizontalPredictor.ApplyHorizontalPrediction24Bit(rowSpan); + } + + deflateStream.Write(rowSpan); + } + + deflateStream.Flush(); + + byte[] buffer = memoryStream.ToArray(); + this.Output.Write(buffer); + bytesWritten += buffer.Length; + return bytesWritten; + } + + /// + /// Writes the image data as RGB compressed with lzw to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// A Span for a pixel row. + /// 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 + { + int bytesWritten = 0; + using var memoryStream = new MemoryStream(); + + IMemoryOwner pixelData = this.MemoryAllocator.Allocate(image.Width * image.Height * 3); + Span pixels = pixelData.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixelRow, rowSpan, pixelRow.Length); + + if (useHorizontalPredictor) + { + HorizontalPredictor.ApplyHorizontalPrediction24Bit(rowSpan); + } + + rowSpan.CopyTo(pixels.Slice(y * image.Width * 3)); + } + + using var lzwEncoder = new TiffLzwEncoder(this.MemoryAllocator, pixelData); + lzwEncoder.Encode(memoryStream); + + byte[] buffer = memoryStream.ToArray(); + this.Output.Write(buffer); + bytesWritten += buffer.Length; + return bytesWritten; + } + + /// + /// Writes the image data as RGB with packed bits compression 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 WriteRgbPackBitsCompressed(Image image, Span rowSpan) + 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; + using IManagedByteBuffer compressedRow = this.MemoryAllocator.AllocateManagedByteBuffer((image.Width * 3) + additionalBytes, AllocationOptions.Clean); + Span compressedRowSpan = compressedRow.GetSpan(); + int bytesWritten = 0; + + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixelRow, rowSpan, pixelRow.Length); + int size = PackBitsWriter.PackBits(rowSpan, compressedRowSpan); + this.Output.Write(compressedRow.Slice(0, size)); + bytesWritten += size; + compressedRowSpan.Clear(); + } + + return bytesWritten; + } + } +}