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;
+ }
+ }
+}