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)