diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 881b3cc4ee..a83e0606c6 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -19,6 +19,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffBitsPerPixel? BitsPerPixel { get; set; } + /// + /// Gets or sets a value indicating which compression to use. + /// + public TiffEncoderCompression Compression { get; set; } = TiffEncoderCompression.None; + /// public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs new file mode 100644 index 0000000000..334262dbfc --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCompression.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Indicates which tiff compression is used. + /// + public enum TiffEncoderCompression + { + /// + /// No compression is used. + /// + None, + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index d81acf6c6a..ffccce5204 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -47,21 +47,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff public TiffEncoderCore(ITiffEncoderOptions options, MemoryAllocator memoryAllocator) { this.memoryAllocator = memoryAllocator; - - if (options.BitsPerPixel == TiffBitsPerPixel.Pixel8) - { - this.PhotometricInterpretation = TiffPhotometricInterpretation.BlackIsZero; - } - else - { - this.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb; - } } /// /// Gets the photometric interpretation implementation to use when encoding the image. /// - private TiffPhotometricInterpretation PhotometricInterpretation { get; } + private TiffPhotometricInterpretation PhotometricInterpretation { get; set; } /// /// Gets or sets the compression implementation to use when encoding the image. @@ -85,6 +76,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; short bpp = (short)this.bitsPerPixel; int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32); @@ -132,15 +124,7 @@ 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 - { - imageDataBytes = writer.WriteGrayImageData(image, this.padding); - } + int imageDataBytes = this.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb ? writer.WriteRgbImageData(image, this.padding) : writer.WriteGrayImageData(image, this.padding); // Write info's about the image to the stream. this.AddImageFormat(image, ifdEntries, imageDataStart, imageDataBytes); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs index a26f0b1172..3a0ceae74a 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { using (Stream stream = CreateCompressedStream(data)) { - byte[] buffer = new byte[data.Length]; + var buffer = new byte[data.Length]; new LzwTiffCompression(Configuration.Default.MemoryAllocator).Decompress(stream, (int)stream.Length, buffer); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs index 28fbce69fd..c1cde94666 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void Decompress_ReadsData(byte[] inputData, int byteCount, byte[] expectedResult) { Stream stream = new MemoryStream(inputData); - byte[] buffer = new byte[expectedResult.Length]; + var buffer = new byte[expectedResult.Length]; new NoneTiffCompression(null).Decompress(stream, byteCount, buffer); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Tiff/ImageExtensionsTest.cs deleted file mode 100644 index af82b4026f..0000000000 --- a/tests/ImageSharp.Tests/Formats/Tiff/ImageExtensionsTest.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Tiff; -using SixLabors.ImageSharp.PixelFormats; -using Xunit; - -namespace SixLabors.ImageSharp.Tests.Formats.Tiff -{ - [Trait("Category", "Tiff.BlackBox.Encoder")] - [Trait("Category", "Tiff")] - public class ImageExtensionsTest - { - [Theory] - [WithFile(TestImages.Tiff.RgbUncompressed, PixelTypes.Rgba32)] - public void ThrowsSavingNotImplemented(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Assert.Throws(() => - { - string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); - string file = Path.Combine(dir, "SaveAsTiff_Path.tiff"); - using var image = provider.GetImage(new TiffDecoder()); - image.SaveAsTiff(file); - }); - } - - [Fact(Skip = "Saving not implemented")] - public void SaveAsTiff_Path() - { - string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); - string file = Path.Combine(dir, "SaveAsTiff_Path.tiff"); - - using (var image = new Image(10, 10)) - { - image.SaveAsTiff(file); - } - - using (Image.Load(file, out IImageFormat mime)) - { - Assert.Equal("image/tiff", mime.DefaultMimeType); - } - } - - [Fact(Skip = "Saving not implemented")] - public async Task SaveAsTiffAsync_Path() - { - string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); - string file = Path.Combine(dir, "SaveAsTiffAsync_Path.tiff"); - - using (var image = new Image(10, 10)) - { - await image.SaveAsTiffAsync(file); - } - - using (Image.Load(file, out IImageFormat mime)) - { - Assert.Equal("image/tiff", mime.DefaultMimeType); - } - } - - [Fact(Skip = "Saving not implemented")] - public void SaveAsTiff_Path_Encoder() - { - string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); - string file = Path.Combine(dir, "SaveAsTiff_Path_Encoder.tiff"); - - using (var image = new Image(10, 10)) - { - image.SaveAsTiff(file, new TiffEncoder()); - } - - using (Image.Load(file, out IImageFormat mime)) - { - Assert.Equal("image/tiff", mime.DefaultMimeType); - } - } - - [Fact(Skip = "Saving not implemented")] - public async Task SaveAsTiffAsync_Path_Encoder() - { - string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); - string file = Path.Combine(dir, "SaveAsTiffAsync_Path_Encoder.tiff"); - - using (var image = new Image(10, 10)) - { - await image.SaveAsTiffAsync(file, new TiffEncoder()); - } - - using (Image.Load(file, out IImageFormat mime)) - { - Assert.Equal("image/tiff", mime.DefaultMimeType); - } - } - - [Fact(Skip = "Saving not implemented")] - public void SaveAsTiff_Stream() - { - using var memoryStream = new MemoryStream(); - - using (var image = new Image(10, 10)) - { - image.SaveAsTiff(memoryStream); - } - - memoryStream.Position = 0; - - using (Image.Load(memoryStream, out IImageFormat mime)) - { - Assert.Equal("image/tiff", mime.DefaultMimeType); - } - } - - [Fact(Skip = "Saving not implemented")] - public async Task SaveAsTiffAsync_StreamAsync() - { - using var memoryStream = new MemoryStream(); - - using (var image = new Image(10, 10)) - { - await image.SaveAsTiffAsync(memoryStream); - } - - memoryStream.Position = 0; - - using (Image.Load(memoryStream, out IImageFormat mime)) - { - Assert.Equal("image/tiff", mime.DefaultMimeType); - } - } - - [Fact(Skip = "Saving not implemented")] - public void SaveAsTiff_Stream_Encoder() - { - using var memoryStream = new MemoryStream(); - - using (var image = new Image(10, 10)) - { - image.SaveAsTiff(memoryStream, new TiffEncoder()); - } - - memoryStream.Position = 0; - - using (Image.Load(memoryStream, out IImageFormat mime)) - { - Assert.Equal("image/tiff", mime.DefaultMimeType); - } - } - - [Fact(Skip = "Saving not implemented")] - public async Task SaveAsTiffAsync_Stream_Encoder() - { - using var memoryStream = new MemoryStream(); - - using (var image = new Image(10, 10)) - { - await image.SaveAsTiffAsync(memoryStream, new TiffEncoder()); - } - - memoryStream.Position = 0; - - using (Image.Load(memoryStream, out IImageFormat mime)) - { - Assert.Equal("image/tiff", mime.DefaultMimeType); - } - } - } -} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs index 91166bf2d6..e279ea5622 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs @@ -13,12 +13,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { private static readonly MemoryAllocator MemoryAllocator = new ArrayPoolMemoryAllocator(); private static readonly Configuration Configuration = Configuration.Default; + private static readonly ITiffEncoderOptions Options = new TiffEncoder(); [Fact] public void WriteHeader_WritesValidHeader() { - var stream = new MemoryStream(); - var encoder = new TiffEncoderCore(null, MemoryAllocator); + using var stream = new MemoryStream(); + var encoder = new TiffEncoderCore(Options, MemoryAllocator); using (var writer = new TiffWriter(stream, MemoryAllocator, Configuration)) { @@ -31,8 +32,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Fact] public void WriteHeader_ReturnsFirstIfdMarker() { - var stream = new MemoryStream(); - var encoder = new TiffEncoderCore(null, MemoryAllocator); + using var stream = new MemoryStream(); + var encoder = new TiffEncoderCore(Options, MemoryAllocator); using (var writer = new TiffWriter(stream, MemoryAllocator, Configuration)) { diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs new file mode 100644 index 0000000000..4d9ea661d3 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + public class TiffEncoderTests + { + public static readonly TheoryData TiffBitsPerPixelFiles = + new TheoryData + { + { TestImages.Tiff.GrayscaleUncompressed, TiffBitsPerPixel.Pixel8 }, + { TestImages.Tiff.RgbUncompressed, TiffBitsPerPixel.Pixel24 }, + }; + + [Theory] + [MemberData(nameof(TiffBitsPerPixelFiles))] + public void TiffEncoder_PreserveBitsPerPixel(string imagePath, TiffBitsPerPixel expectedBitsPerPixel) + { + // arrange + var tiffEncoder = new TiffEncoder(); + var testFile = TestFile.Create(imagePath); + using Image input = testFile.CreateRgba32Image(); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(memStream); + TiffMetadata meta = output.Metadata.GetTiffMetadata(); + Assert.Equal(expectedBitsPerPixel, meta.BitsPerPixel); + } + + [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); + + [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); + + private static void TestTiffEncoderCore( + TestImageProvider provider, + TiffBitsPerPixel bitsPerPixel, + 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 }; + + 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); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs new file mode 100644 index 0000000000..edf3483302 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs @@ -0,0 +1,63 @@ +// 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; + } + } +}