From a0e406bec88ab93b71c8e55507bebbebe49a304c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 26 Nov 2020 20:11:42 +0100 Subject: [PATCH] Add tiff encoding mode enum --- .../Formats/Tiff/ITiffEncoderOptions.cs | 9 +-- src/ImageSharp/Formats/Tiff/TiffEncoder.cs | 9 +-- .../Formats/Tiff/TiffEncoderCore.cs | 68 ++++++++++++------- .../Formats/Tiff/TiffEncodingMode.cs | 31 +++++++++ .../Formats/Tiff/Utils/TiffWriter.cs | 13 +++- .../Formats/Tiff/TiffEncoderTests.cs | 27 ++++---- .../Formats/Tiff/TiffTestUtils.cs | 63 ----------------- 7 files changed, 105 insertions(+), 115 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/TiffEncodingMode.cs delete mode 100644 tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs diff --git a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs index 5b849e1314..97f3d46b06 100644 --- a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs +++ b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs @@ -10,20 +10,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// 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. + /// Gets the encoding mode to use. RGB, RGB with color palette or gray. /// - bool UseColorPalette { get; } + TiffEncodingMode Mode { get; } /// /// Gets the quantizer for creating a color palette image. diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 409d16a68f..3ab17b6c37 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -15,20 +15,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public class TiffEncoder : IImageEncoder, ITiffEncoderOptions { - /// - /// Gets or sets the number of bits per pixel. 8 bit implies a grayscale image. - /// - public TiffBitsPerPixel? BitsPerPixel { get; set; } - /// /// Gets or sets a value indicating which compression to use. /// public TiffEncoderCompression Compression { get; set; } = TiffEncoderCompression.None; /// - /// Gets or sets a value indicating whether to use a color palette. + /// Gets or sets the encoding mode to use. RGB, RGB with a color palette or gray. /// - public bool UseColorPalette { get; set; } + public TiffEncodingMode Mode { get; set; } /// /// Gets or sets the quantizer for color images with a palette. diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index f2aec7a61b..c493d34a41 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff { this.memoryAllocator = memoryAllocator; this.CompressionType = options.Compression; - this.UseColorMap = options.UseColorPalette; + this.Mode = options.Mode; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; } @@ -70,9 +70,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff private TiffEncoderCompression CompressionType { get; } /// - /// Gets a value indicating whether to use a colormap. + /// Gets or sets the encoding mode to use. RGB, RGB with color palette or gray. /// - private bool UseColorMap { get; } + private TiffEncodingMode Mode { get; set; } /// /// Encodes the image to the specified stream from the . @@ -91,6 +91,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff ImageMetadata metadata = image.Metadata; TiffMetadata tiffMetadata = metadata.GetTiffMetadata(); this.bitsPerPixel ??= tiffMetadata.BitsPerPixel; + if (this.Mode == TiffEncodingMode.Default) + { + // Preserve input bits per pixel, if no mode was specified. + if (this.bitsPerPixel == TiffBitsPerPixel.Pixel8) + { + this.Mode = TiffEncodingMode.Gray; + } + } + this.SetPhotometricInterpretation(); short bpp = (short)this.bitsPerPixel; @@ -141,17 +150,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff // Write the image bytes to the steam. var imageDataStart = (uint)writer.Position; 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 + switch (this.PhotometricInterpretation) { - imageDataBytes = writer.WriteGrayImageData(image, this.padding); + case TiffPhotometricInterpretation.PaletteColor: + imageDataBytes = writer.WritePalettedRgbImageData(image, this.quantizer, this.padding, out colorMap); + break; + case TiffPhotometricInterpretation.BlackIsZero: + imageDataBytes = writer.WriteGrayImageData(image, this.padding); + break; + default: + imageDataBytes = writer.WriteRgbImageData(image, this.padding); + break; } // Write info's about the image to the stream. @@ -270,7 +279,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel) { - Value = 3 + Value = this.GetSamplesPerPixel() }; var rowsPerStrip = new ExifLong(ExifTagValue.RowsPerStrip) @@ -323,19 +332,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff private void SetPhotometricInterpretation() { - if (this.UseColorMap) + switch (this.Mode) { - this.PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor; - return; + case TiffEncodingMode.ColorPalette: + this.PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor; + break; + case TiffEncodingMode.Gray: + this.PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero; + break; + default: + this.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + break; } + } - if (this.bitsPerPixel == TiffBitsPerPixel.Pixel8) - { - this.PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero; - } - else + private uint GetSamplesPerPixel() + { + switch (this.PhotometricInterpretation) { - this.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + case TiffPhotometricInterpretation.PaletteColor: + case TiffPhotometricInterpretation.Rgb: + return 3; + case TiffPhotometricInterpretation.BlackIsZero: + return 1; + default: + return 3; } } @@ -344,6 +365,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff switch (this.PhotometricInterpretation) { case TiffPhotometricInterpretation.PaletteColor: + return new ushort[] { 8 }; case TiffPhotometricInterpretation.Rgb: return new ushort[] { 8, 8, 8 }; case TiffPhotometricInterpretation.BlackIsZero: diff --git a/src/ImageSharp/Formats/Tiff/TiffEncodingMode.cs b/src/ImageSharp/Formats/Tiff/TiffEncodingMode.cs new file mode 100644 index 0000000000..195353ec4e --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffEncodingMode.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Enum for the different tiff encoding options. + /// + public enum TiffEncodingMode + { + /// + /// No mode specified. Will preserve the bits per pixels of the input image. + /// + Default = 0, + + /// + /// The image will be encoded as RGB, 8 bit per channel. + /// + Rgb = 1, + + /// + /// The image will be encoded as RGB with a color palette. + /// + ColorPalette = 2, + + /// + /// The image will be encoded as 8 bit gray. + /// + Gray = 3, + } +} diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs index 16c9b87e32..e99682bc08 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs @@ -133,7 +133,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The pixel data. /// The image to write to the stream. /// The padding bytes for each row. - /// The number of bytes written + /// The number of bytes written. public int WriteRgbImageData(Image image, int padding) where TPixel : unmanaged, IPixel { @@ -151,6 +151,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff 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 padding bytes for each row. + /// The color map. + /// The number of bytes written. public int WritePalettedRgbImageData(Image image, IQuantizer quantizer, int padding, out IExifValue colorMap) where TPixel : unmanaged, IPixel { @@ -214,7 +223,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The pixel data. /// The image to write to the stream. /// The padding bytes for each row. - /// The number of bytes written + /// The number of bytes written. public int WriteGrayImageData(Image image, int padding) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 16a2ab0126..27ca717e66 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -5,13 +5,17 @@ using System.IO; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Tiff { + [Trait("Category", "Tiff")] public class TiffEncoderTests { + private static TiffDecoder referenceDecoder = new TiffDecoder(); + public static readonly TheoryData TiffBitsPerPixelFiles = new TheoryData { @@ -41,36 +45,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(TestImages.Tiff.RgbUncompressed, PixelTypes.Rgba32)] - public void TiffEncoder_EncodeRgb_Works(TestImageProvider provider, TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Pixel24) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, bitsPerPixel); + public void TiffEncoder_EncodeRgb_Works(TestImageProvider provider, TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Pixel24, TiffEncodingMode mode = TiffEncodingMode.Rgb) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, bitsPerPixel, mode); [Theory] [WithFile(TestImages.Tiff.RgbUncompressed, PixelTypes.Rgba32)] - public void TiffEncoder_EncodeGray_Works(TestImageProvider provider, TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Pixel8) - where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, bitsPerPixel); + public void TiffEncoder_EncodeGray_Works(TestImageProvider provider, TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Pixel8, TiffEncodingMode mode = TiffEncodingMode.Gray) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, bitsPerPixel, mode); [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); + public void TiffEncoder_EncodeColorPalette_Works(TestImageProvider provider, TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Pixel24, TiffEncodingMode mode = TiffEncodingMode.ColorPalette) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, bitsPerPixel, mode); private static void TestTiffEncoderCore( TestImageProvider provider, TiffBitsPerPixel bitsPerPixel, - bool useColorPalette = false, + TiffEncodingMode mode, TiffEncoderCompression compression = TiffEncoderCompression.None, bool useExactComparer = true, float compareTolerance = 0.01f) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - var encoder = new TiffEncoder { BitsPerPixel = bitsPerPixel, Compression = compression }; + var encoder = new TiffEncoder { Mode = mode, Compression = compression }; - using var memStream = new MemoryStream(); - image.Save(memStream, encoder); - memStream.Position = 0; - using var encodedImage = (Image)Image.Load(memStream); - TiffTestUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance); + // Does DebugSave & load reference CompareToReferenceInput(): + image.VerifyEncoder(provider, "tiff", bitsPerPixel, encoder, useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), referenceDecoder: referenceDecoder); } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs deleted file mode 100644 index edf3483302..0000000000 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.IO; -using ImageMagick; - -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; - -using Xunit; - -namespace SixLabors.ImageSharp.Tests.Formats.Tiff -{ - public class TiffTestUtils - { - public static void CompareWithReferenceDecoder( - TestImageProvider provider, - Image image, - bool useExactComparer = true, - float compareTolerance = 0.01f) - where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel - { - string path = TestImageProvider.GetFilePathOrNull(provider); - if (path == null) - { - throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); - } - - var testFile = TestFile.Create(path); - Image magickImage = DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); - if (useExactComparer) - { - ImageComparer.Exact.VerifySimilarity(magickImage, image); - } - else - { - ImageComparer.Tolerant(compareTolerance).VerifySimilarity(magickImage, image); - } - } - - public static Image DecodeWithMagick(Configuration configuration, FileInfo fileInfo) - where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel - { - using var magickImage = new MagickImage(fileInfo); - magickImage.AutoOrient(); - var result = new Image(configuration, magickImage.Width, magickImage.Height); - - Assert.True(result.TryGetSinglePixelSpan(out Span resultPixels)); - - using IUnsafePixelCollection pixels = magickImage.GetPixelsUnsafe(); - byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - - PixelOperations.Instance.FromRgba32Bytes( - configuration, - data, - resultPixels, - resultPixels.Length); - - return result; - } - } -}