Browse Source

Add support for writing palette color tiff's

pull/1570/head
Brian Popow 5 years ago
parent
commit
c7511c7bd6
  1. 19
      src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs
  2. 12
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  3. 82
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  4. 69
      src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
  5. 6
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

19
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
{
/// <summary>
/// Encapsulates the options for the <see cref="TiffEncoder"/>.
/// </summary>
public interface ITiffEncoderOptions
internal interface ITiffEncoderOptions
{
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>
TiffBitsPerPixel? BitsPerPixel { get; }
/// <summary>
/// Gets the compression type to use.
/// </summary>
TiffEncoderCompression Compression { get; }
/// <summary>
/// Gets a value indicating whether to use a color palette.
/// </summary>
bool UseColorPalette { get; }
/// <summary>
/// Gets the quantizer for creating a color palette image.
/// </summary>
IQuantizer Quantizer { get; }
}
}

12
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
/// </summary>
public TiffEncoderCompression Compression { get; set; } = TiffEncoderCompression.None;
/// <summary>
/// Gets or sets a value indicating whether to use a color palette.
/// </summary>
public bool UseColorPalette { get; set; }
/// <summary>
/// Gets or sets the quantizer for color images with a palette.
/// Defaults to OctreeQuantizer.
/// </summary>
public IQuantizer Quantizer { get; set; }
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>

82
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
/// </summary>
private TiffBitsPerPixel? bitsPerPixel;
/// <summary>
/// The quantizer for creating color palette image.
/// </summary>
private readonly IQuantizer quantizer;
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoderCore"/> class.
/// </summary>
@ -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;
}
/// <summary>
/// Gets the photometric interpretation implementation to use when encoding the image.
/// Gets or sets the photometric interpretation implementation to use when encoding the image.
/// </summary>
private TiffPhotometricInterpretation PhotometricInterpretation { get; set; }
/// <summary>
/// Gets or sets the compression implementation to use when encoding the image.
/// Gets the compression implementation to use when encoding the image.
/// </summary>
private TiffEncoderCompression CompressionType { get; }
/// <summary>
/// Gets a value indicating whether to use a colormap.
/// </summary>
public TiffCompressionType CompressionType { get; set; }
private bool UseColorMap { get; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
@ -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<TPixel>(TiffWriter writer, Image<TPixel> image, long ifdOffset)
where TPixel : unmanaged, IPixel<TPixel>
{
IExifValue colorMap = null;
var ifdEntries = new List<IExifValue>();
// 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
/// <param name="imageDataStartOffset">The start of the image data in the stream.</param>
/// <param name="imageDataBytes">The image data in bytes to write.</param>
public void AddImageFormat<TPixel>(Image<TPixel> image, List<IExifValue> ifdEntries, uint imageDataStartOffset, int imageDataBytes)
where TPixel : unmanaged, IPixel<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
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 };
}
}
}
}

69
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<byte> rowSpan = row.GetSpan();
int bytesWritten = 0;
for (int y = 0; y < image.Height; y++)
{
Span<TPixel> pixelRow = image.GetPixelRowSpan(y);
PixelOperations<TPixel>.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<TPixel>(Image<TPixel> image, IQuantizer quantizer, int padding, out IExifValue colorMap)
where TPixel : unmanaged, IPixel<TPixel>
{
using IManagedByteBuffer row = this.AllocateRow(image.Width, 1, padding);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image.Frames.RootFrame, image.Bounds());
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(256 * 2 * 3);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> 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<Rgb48> quantizedColorRgb48 = MemoryMarshal.Cast<byte, Rgb48>(colorPalette.Slice(0, quantizedColorBytes));
PixelOperations<TPixel>.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<byte> 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;
}
/// <summary>
@ -157,14 +220,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
using IManagedByteBuffer row = this.AllocateRow(image.Width, 1, padding);
Span<byte> rowSpan = row.GetSpan();
int bytesWritten = 0;
for (int y = 0; y < image.Height; y++)
{
Span<TPixel> pixelRow = image.GetPixelRowSpan(y);
PixelOperations<TPixel>.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);

6
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -49,9 +49,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffEncoder_EncodeGray_Works<TPixel>(TestImageProvider<TPixel> provider, TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Pixel8)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, bitsPerPixel);
[Theory]
[WithFile(TestImages.Tiff.RgbUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeColorPalette_Works<TPixel>(TestImageProvider<TPixel> provider, TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Pixel8, bool useColorPalette = true)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, bitsPerPixel, useColorPalette);
private static void TestTiffEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
TiffBitsPerPixel bitsPerPixel,
bool useColorPalette = false,
TiffEncoderCompression compression = TiffEncoderCompression.None,
bool useExactComparer = true,
float compareTolerance = 0.01f)

Loading…
Cancel
Save