diff --git a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs
index dcb8a5c44..5b849e131 100644
--- a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs
+++ b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs
@@ -1,16 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
+
namespace SixLabors.ImageSharp.Formats.Tiff
{
///
/// Encapsulates the options for the .
///
- public interface ITiffEncoderOptions
+ internal interface ITiffEncoderOptions
{
///
/// Gets the number of bits per pixel.
///
TiffBitsPerPixel? BitsPerPixel { get; }
+
+ ///
+ /// Gets the compression type to use.
+ ///
+ TiffEncoderCompression Compression { get; }
+
+ ///
+ /// Gets a value indicating whether to use a color palette.
+ ///
+ bool UseColorPalette { get; }
+
+ ///
+ /// Gets the quantizer for creating a color palette image.
+ ///
+ IQuantizer Quantizer { get; }
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs
index a83e0606c..409d16a68 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff
{
@@ -24,6 +25,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
public TiffEncoderCompression Compression { get; set; } = TiffEncoderCompression.None;
+ ///
+ /// Gets or sets a value indicating whether to use a color palette.
+ ///
+ public bool UseColorPalette { get; set; }
+
+ ///
+ /// Gets or sets the quantizer for color images with a palette.
+ /// Defaults to OctreeQuantizer.
+ ///
+ public IQuantizer Quantizer { get; set; }
+
///
public void Encode(Image image, Stream stream)
where TPixel : unmanaged, IPixel
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index ffccce520..f2aec7a61 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
@@ -11,6 +11,8 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff
{
@@ -39,6 +41,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
private TiffBitsPerPixel? bitsPerPixel;
+ ///
+ /// The quantizer for creating color palette image.
+ ///
+ private readonly IQuantizer quantizer;
+
///
/// Initializes a new instance of the class.
///
@@ -47,17 +54,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff
public TiffEncoderCore(ITiffEncoderOptions options, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
+ this.CompressionType = options.Compression;
+ this.UseColorMap = options.UseColorPalette;
+ this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
}
///
- /// Gets the photometric interpretation implementation to use when encoding the image.
+ /// Gets or sets the photometric interpretation implementation to use when encoding the image.
///
private TiffPhotometricInterpretation PhotometricInterpretation { get; set; }
///
- /// Gets or sets the compression implementation to use when encoding the image.
+ /// Gets the compression implementation to use when encoding the image.
+ ///
+ private TiffEncoderCompression CompressionType { get; }
+
+ ///
+ /// Gets a value indicating whether to use a colormap.
///
- public TiffCompressionType CompressionType { get; set; }
+ private bool UseColorMap { get; }
///
/// Encodes the image to the specified stream from the .
@@ -76,7 +91,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
ImageMetadata metadata = image.Metadata;
TiffMetadata tiffMetadata = metadata.GetTiffMetadata();
this.bitsPerPixel ??= tiffMetadata.BitsPerPixel;
- this.PhotometricInterpretation = this.bitsPerPixel == TiffBitsPerPixel.Pixel8 ? TiffPhotometricInterpretation.BlackIsZero : TiffPhotometricInterpretation.Rgb;
+ this.SetPhotometricInterpretation();
short bpp = (short)this.bitsPerPixel;
int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32);
@@ -120,14 +135,32 @@ namespace SixLabors.ImageSharp.Formats.Tiff
public long WriteImage(TiffWriter writer, Image image, long ifdOffset)
where TPixel : unmanaged, IPixel
{
+ IExifValue colorMap = null;
var ifdEntries = new List();
// Write the image bytes to the steam.
var imageDataStart = (uint)writer.Position;
- int imageDataBytes = this.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb ? writer.WriteRgbImageData(image, this.padding) : writer.WriteGrayImageData(image, this.padding);
+ int imageDataBytes;
+ if (this.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb)
+ {
+ imageDataBytes = writer.WriteRgbImageData(image, this.padding);
+ }
+ else if (this.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor)
+ {
+ imageDataBytes = writer.WritePalettedRgbImageData(image, this.quantizer, this.padding, out colorMap);
+ }
+ else
+ {
+ imageDataBytes = writer.WriteGrayImageData(image, this.padding);
+ }
// Write info's about the image to the stream.
this.AddImageFormat(image, ifdEntries, imageDataStart, imageDataBytes);
+ if (this.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor)
+ {
+ ifdEntries.Add(colorMap);
+ }
+
writer.WriteMarker(ifdOffset, (uint)writer.Position);
long nextIfdMarker = this.WriteIfd(writer, ifdEntries);
@@ -200,7 +233,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// The start of the image data in the stream.
/// The image data in bytes to write.
public void AddImageFormat(Image image, List ifdEntries, uint imageDataStartOffset, int imageDataBytes)
- where TPixel : unmanaged, IPixel
+ where TPixel : unmanaged, IPixel
{
var width = new ExifLong(ExifTagValue.ImageWidth)
{
@@ -212,7 +245,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
Value = (uint)image.Height
};
- ushort[] bitsPerSampleValue = this.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb ? new ushort[] { 8, 8, 8 } : new ushort[] { 8 };
+ ushort[] bitsPerSampleValue = this.GetBitsPerSampleValue();
var bitPerSample = new ExifShortArray(ExifTagValue.BitsPerSample)
{
Value = bitsPerSampleValue
@@ -265,8 +298,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
var resolutionUnit = new ExifShort(ExifTagValue.ResolutionUnit)
{
- // TODO: what to use here as default?
- Value = 0
+ Value = 3 // 3 is centimeter.
};
var software = new ExifString(ExifTagValue.Software)
@@ -288,5 +320,37 @@ namespace SixLabors.ImageSharp.Formats.Tiff
ifdEntries.Add(resolutionUnit);
ifdEntries.Add(software);
}
+
+ private void SetPhotometricInterpretation()
+ {
+ if (this.UseColorMap)
+ {
+ this.PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor;
+ return;
+ }
+
+ if (this.bitsPerPixel == TiffBitsPerPixel.Pixel8)
+ {
+ this.PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero;
+ }
+ else
+ {
+ this.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb;
+ }
+ }
+
+ private ushort[] GetBitsPerSampleValue()
+ {
+ switch (this.PhotometricInterpretation)
+ {
+ case TiffPhotometricInterpretation.PaletteColor:
+ case TiffPhotometricInterpretation.Rgb:
+ return new ushort[] { 8, 8, 8 };
+ case TiffPhotometricInterpretation.BlackIsZero:
+ return new ushort[] { 8 };
+ default:
+ return new ushort[] { 8, 8, 8 };
+ }
+ }
}
}
diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
index 7578c8213..16c9b87e3 100644
--- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
+++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
@@ -2,10 +2,14 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff
{
@@ -135,14 +139,73 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
using IManagedByteBuffer row = this.AllocateRow(image.Width, 3, padding);
Span rowSpan = row.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);
this.output.Write(rowSpan);
+ bytesWritten += rowSpan.Length;
}
- return image.Width * image.Height * 3;
+ return bytesWritten;
+ }
+
+ public int WritePalettedRgbImageData(Image image, IQuantizer quantizer, int padding, out IExifValue colorMap)
+ where TPixel : unmanaged, IPixel
+ {
+ using IManagedByteBuffer row = this.AllocateRow(image.Width, 1, padding);
+ using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration);
+ using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image.Frames.RootFrame, image.Bounds());
+ using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(256 * 2 * 3);
+ 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);
+
+ // In a TIFF ColorMap, all the Red values come first, followed by the Green values,
+ // then the Blue values. Convert the quantized palette to this format.
+ var palette = new ushort[quantizedColorBytes];
+ int paletteIdx = 0;
+ for (int i = 0; i < quantizedColors.Length; i++)
+ {
+ palette[paletteIdx++] = quantizedColorRgb48[i].R;
+ }
+
+ for (int i = 0; i < quantizedColors.Length; i++)
+ {
+ palette[paletteIdx++] = quantizedColorRgb48[i].G;
+ }
+
+ for (int i = 0; i < quantizedColors.Length; i++)
+ {
+ palette[paletteIdx++] = quantizedColorRgb48[i].B;
+ }
+
+ colorMap = new ExifShortArray(ExifTagValue.ColorMap)
+ {
+ Value = palette
+ };
+
+ int bytesWritten = 0;
+ for (int y = 0; y < image.Height; y++)
+ {
+ ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y);
+ this.output.Write(pixelSpan);
+ bytesWritten += pixelSpan.Length;
+
+ for (int i = 0; i < padding; i++)
+ {
+ this.output.WriteByte(0);
+ bytesWritten++;
+ }
+ }
+
+ return bytesWritten;
}
///
@@ -157,14 +220,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
using IManagedByteBuffer row = this.AllocateRow(image.Width, 1, padding);
Span rowSpan = row.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);
this.output.Write(rowSpan);
+ bytesWritten += rowSpan.Length;
}
- return image.Width * image.Height;
+ return bytesWritten;
}
private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel, int padding) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, padding);
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
index 4d9ea661d..16a2ab012 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
@@ -49,9 +49,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffEncoder_EncodeGray_Works(TestImageProvider provider, TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Pixel8)
where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, bitsPerPixel);
+ [Theory]
+ [WithFile(TestImages.Tiff.RgbUncompressed, PixelTypes.Rgba32)]
+ public void TiffEncoder_EncodeColorPalette_Works(TestImageProvider provider, TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Pixel8, bool useColorPalette = true)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, bitsPerPixel, useColorPalette);
+
private static void TestTiffEncoderCore(
TestImageProvider provider,
TiffBitsPerPixel bitsPerPixel,
+ bool useColorPalette = false,
TiffEncoderCompression compression = TiffEncoderCompression.None,
bool useExactComparer = true,
float compareTolerance = 0.01f)