diff --git a/ImageSharp.sln b/ImageSharp.sln index 83208994cb..5555764353 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28902.138 diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index ea4cd1c8c4..be2e964fc0 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -56,7 +57,7 @@ internal static class AotCompilerTools /// necessary methods to complete the SaveAsGif call. That's it, otherwise you should NEVER need this method!!! /// [Preserve] - private static void SeedEverything() + private static void SeedPixelFormats() { try { @@ -199,6 +200,7 @@ private static void AotCompileImageEncoderInternals() default(JpegEncoderCore).Encode(default, default, default); default(PngEncoderCore).Encode(default, default, default); default(TgaEncoderCore).Encode(default, default, default); + default(TiffEncoderCore).Encode(default, default, default); } /// @@ -214,6 +216,7 @@ private static void AotCompileImageDecoderInternals() default(JpegDecoderCore).Decode(default, default, default); default(PngDecoderCore).Decode(default, default, default); default(TgaDecoderCore).Decode(default, default, default); + default(TiffDecoderCore).Decode(default, default, default); } /// @@ -229,6 +232,7 @@ private static void AotCompileImageEncoders() AotCompileImageEncoder(); AotCompileImageEncoder(); AotCompileImageEncoder(); + AotCompileImageEncoder(); } /// @@ -244,6 +248,7 @@ private static void AotCompileImageDecoders() AotCompileImageDecoder(); AotCompileImageDecoder(); AotCompileImageDecoder(); + AotCompileImageDecoder(); } /// diff --git a/src/ImageSharp/Common/ByteOrder.cs b/src/ImageSharp/Common/ByteOrder.cs new file mode 100644 index 0000000000..cc38f1cdee --- /dev/null +++ b/src/ImageSharp/Common/ByteOrder.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp +{ + /// + /// The byte order of the data stream. + /// + public enum ByteOrder + { + /// + /// The big-endian byte order (Motorola). + /// Most-significant byte comes first, and ends with the least-significant byte. + /// + BigEndian, + + /// + /// The little-endian byte order (Intel). + /// Least-significant byte comes first and ends with the most-significant byte. + /// + LittleEndian + } +} diff --git a/src/ImageSharp/Common/Helpers/UnitConverter.cs b/src/ImageSharp/Common/Helpers/UnitConverter.cs index 4a6e6abcb6..efc0e0e152 100644 --- a/src/ImageSharp/Common/Helpers/UnitConverter.cs +++ b/src/ImageSharp/Common/Helpers/UnitConverter.cs @@ -30,6 +30,11 @@ internal static class UnitConverter /// private const double InchesInMeter = 1 / 0.0254D; + /// + /// The default resolution unit value. + /// + private const PixelResolutionUnit DefaultResolutionUnit = PixelResolutionUnit.PixelsPerInch; + /// /// Scales the value from centimeters to meters. /// @@ -89,7 +94,50 @@ public static PixelResolutionUnit ExifProfileToResolutionUnit(ExifProfile profil IExifValue resolution = profile.GetValue(ExifTag.ResolutionUnit); // EXIF is 1, 2, 3 so we minus "1" off the result. - return resolution is null ? default : (PixelResolutionUnit)(byte)(resolution.Value - 1); + return resolution is null ? DefaultResolutionUnit : (PixelResolutionUnit)(byte)(resolution.Value - 1); + } + + /// + /// Sets the exif profile resolution values. + /// + /// The exif profile. + /// The resolution unit. + /// The horizontal resolution value. + /// The vertical resolution value. + [MethodImpl(InliningOptions.ShortMethod)] + public static void SetResolutionValues(ExifProfile exifProfile, PixelResolutionUnit unit, double horizontal, double vertical) + { + switch (unit) + { + case PixelResolutionUnit.AspectRatio: + case PixelResolutionUnit.PixelsPerInch: + case PixelResolutionUnit.PixelsPerCentimeter: + break; + case PixelResolutionUnit.PixelsPerMeter: + { + unit = PixelResolutionUnit.PixelsPerCentimeter; + horizontal = UnitConverter.MeterToCm(horizontal); + vertical = UnitConverter.MeterToCm(vertical); + } + + break; + default: + unit = PixelResolutionUnit.PixelsPerInch; + break; + } + + exifProfile.SetValue(ExifTag.ResolutionUnit, (ushort)(unit + 1)); + + if (unit == PixelResolutionUnit.AspectRatio) + { + exifProfile.RemoveValue(ExifTag.XResolution); + exifProfile.RemoveValue(ExifTag.YResolution); + } + else + { + exifProfile.SetValue(ExifTag.XResolution, new Rational(horizontal)); + exifProfile.SetValue(ExifTag.YResolution, new Rational(vertical)); + } } } } diff --git a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs b/src/ImageSharp/Compression/Zlib/Adler32.cs similarity index 99% rename from src/ImageSharp/Formats/Png/Zlib/Adler32.cs rename to src/ImageSharp/Compression/Zlib/Adler32.cs index 534aba8f5a..9b3abd298b 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Adler32.cs +++ b/src/ImageSharp/Compression/Zlib/Adler32.cs @@ -9,7 +9,7 @@ #endif #pragma warning disable IDE0007 // Use implicit type -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// Calculates the 32 bit Adler checksum of a given buffer according to diff --git a/src/ImageSharp/Formats/Png/Zlib/Crc32.Lut.cs b/src/ImageSharp/Compression/Zlib/Crc32.Lut.cs similarity index 98% rename from src/ImageSharp/Formats/Png/Zlib/Crc32.Lut.cs rename to src/ImageSharp/Compression/Zlib/Crc32.Lut.cs index 5007833539..059bd9f312 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Crc32.Lut.cs +++ b/src/ImageSharp/Compression/Zlib/Crc32.Lut.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// Contains precalulated tables for scalar calculations. diff --git a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs b/src/ImageSharp/Compression/Zlib/Crc32.cs similarity index 99% rename from src/ImageSharp/Formats/Png/Zlib/Crc32.cs rename to src/ImageSharp/Compression/Zlib/Crc32.cs index 6b19987cb1..0ba368df64 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs +++ b/src/ImageSharp/Compression/Zlib/Crc32.cs @@ -9,7 +9,7 @@ using System.Runtime.Intrinsics.X86; #endif -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// Calculates the 32 bit Cyclic Redundancy Check (CRC) checksum of a given buffer diff --git a/src/ImageSharp/Compression/Zlib/DeflateCompressionLevel.cs b/src/ImageSharp/Compression/Zlib/DeflateCompressionLevel.cs new file mode 100644 index 0000000000..2edf76e7d5 --- /dev/null +++ b/src/ImageSharp/Compression/Zlib/DeflateCompressionLevel.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Compression.Zlib +{ + /// + /// Provides enumeration of available deflate compression levels. + /// + public enum DeflateCompressionLevel + { + /// + /// Level 0. Equivalent to . + /// + Level0 = 0, + + /// + /// No compression. Equivalent to . + /// + NoCompression = Level0, + + /// + /// Level 1. Equivalent to . + /// + Level1 = 1, + + /// + /// Best speed compression level. + /// + BestSpeed = Level1, + + /// + /// Level 2. + /// + Level2 = 2, + + /// + /// Level 3. + /// + Level3 = 3, + + /// + /// Level 4. + /// + Level4 = 4, + + /// + /// Level 5. + /// + Level5 = 5, + + /// + /// Level 6. Equivalent to . + /// + Level6 = 6, + + /// + /// The default compression level. Equivalent to . + /// + DefaultCompression = Level6, + + /// + /// Level 7. + /// + Level7 = 7, + + /// + /// Level 8. + /// + Level8 = 8, + + /// + /// Level 9. Equivalent to . + /// + Level9 = 9, + + /// + /// Best compression level. Equivalent to . + /// + BestCompression = Level9, + } +} diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs b/src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs similarity index 96% rename from src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs rename to src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs index a5d129c92c..02590ca253 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs +++ b/src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs @@ -4,7 +4,7 @@ using System; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { internal static class DeflateThrowHelper { diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Compression/Zlib/Deflater.cs similarity index 99% rename from src/ImageSharp/Formats/Png/Zlib/Deflater.cs rename to src/ImageSharp/Compression/Zlib/Deflater.cs index 8389215813..800c96703e 100644 --- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs +++ b/src/ImageSharp/Compression/Zlib/Deflater.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// This class compresses input with the deflate algorithm described in RFC 1951. diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs b/src/ImageSharp/Compression/Zlib/DeflaterConstants.cs similarity index 98% rename from src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs rename to src/ImageSharp/Compression/Zlib/DeflaterConstants.cs index ec224d748d..30bd75ffcd 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs +++ b/src/ImageSharp/Compression/Zlib/DeflaterConstants.cs @@ -4,7 +4,7 @@ // using System; -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// This class contains constants used for deflation. diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs similarity index 99% rename from src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs rename to src/ImageSharp/Compression/Zlib/DeflaterEngine.cs index 797f5d2101..d3cfa7c3d1 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// Strategies for deflater diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs similarity index 99% rename from src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs rename to src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs index 96b47fb24b..d6892dfd2d 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs +++ b/src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs @@ -7,7 +7,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// Performs Deflate Huffman encoding. diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs b/src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs similarity index 98% rename from src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs rename to src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs index 5c5651996f..cbbf7ea792 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs +++ b/src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs @@ -5,7 +5,7 @@ using System.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// A special stream deflating or compressing the bytes that are diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs similarity index 99% rename from src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs rename to src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs index f702a7eada..36dfd92da2 100644 --- a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs +++ b/src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// Stores pending data for writing data to the Deflater. diff --git a/src/ImageSharp/Formats/Png/Zlib/README.md b/src/ImageSharp/Compression/Zlib/README.md similarity index 100% rename from src/ImageSharp/Formats/Png/Zlib/README.md rename to src/ImageSharp/Compression/Zlib/README.md diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs similarity index 89% rename from src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs rename to src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs index 06c6e3dea4..44883665ab 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs +++ b/src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs @@ -4,9 +4,10 @@ using System; using System.IO; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// Provides methods and properties for compressing streams by using the Zlib Deflate algorithm. @@ -39,9 +40,19 @@ internal sealed class ZlibDeflateStream : Stream /// /// The stream responsible for compressing the input stream. /// - // private DeflateStream deflateStream; private DeflaterOutputStream deflateStream; + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator to use for buffer allocations. + /// The stream to compress. + /// The compression level. + public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, DeflateCompressionLevel level) + : this(memoryAllocator, stream, (PngCompressionLevel)level) + { + } + /// /// Initializes a new instance of the class. /// diff --git a/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs similarity index 99% rename from src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs rename to src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs index 52ef0e85ba..f4b0543b84 100644 --- a/src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs +++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs @@ -6,7 +6,7 @@ using System.IO.Compression; using SixLabors.ImageSharp.IO; -namespace SixLabors.ImageSharp.Formats.Png.Zlib +namespace SixLabors.ImageSharp.Compression.Zlib { /// /// Provides methods and properties for deframing streams from PNGs. diff --git a/src/ImageSharp/Formats/Png/Zlib/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf b/src/ImageSharp/Compression/Zlib/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf similarity index 100% rename from src/ImageSharp/Formats/Png/Zlib/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf rename to src/ImageSharp/Compression/Zlib/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 062bcb229c..8a13ad82dd 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -94,9 +94,9 @@ public int StreamProcessingBufferSize } /// - /// Gets a set of properties for the Congiguration. + /// Gets a set of properties for the Configuration. /// - /// This can be used for storing global settings and defaults to be accessable to processors. + /// This can be used for storing global settings and defaults to be accessible to processors. public IDictionary Properties { get; } = new ConcurrentDictionary(); /// diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs index 075c708b6a..0f8b1e16d9 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. // @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp { @@ -535,5 +536,108 @@ public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder e encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance), cancellationToken); + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsTiff(this Image source, string path) => SaveAsTiff(source, path, null); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, string path) => SaveAsTiffAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsTiffAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsTiff(this Image source, string path, TiffEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, string path, TiffEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsTiff(this Image source, Stream stream) + => SaveAsTiff(source, stream, null); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsTiffAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tiff format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance), + cancellationToken); + } } diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.tt b/src/ImageSharp/Formats/ImageExtensions.Save.tt index 63b404cc44..af95312254 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.tt +++ b/src/ImageSharp/Formats/ImageExtensions.Save.tt @@ -1,4 +1,4 @@ -<#@ template language="C#" #> +<#@ template language="C#" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> // Copyright (c) Six Labors. @@ -17,6 +17,7 @@ using SixLabors.ImageSharp.Advanced; "Jpeg", "Png", "Tga", + "Tiff", }; foreach (string fmt in formats) diff --git a/src/ImageSharp/Formats/Png/PngCompressionLevel.cs b/src/ImageSharp/Formats/Png/PngCompressionLevel.cs index 7516e0987d..961f9b05b8 100644 --- a/src/ImageSharp/Formats/Png/PngCompressionLevel.cs +++ b/src/ImageSharp/Formats/Png/PngCompressionLevel.cs @@ -1,11 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System.ComponentModel; + namespace SixLabors.ImageSharp.Formats.Png { /// /// Provides enumeration of available PNG compression levels. /// + [EditorBrowsable(EditorBrowsableState.Never)] public enum PngCompressionLevel { /// diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 3fa0e3f586..c2c336c039 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -10,10 +10,9 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading; -using System.Threading.Tasks; +using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; -using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 5d2af8ec6a..7a285eb70b 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -8,11 +8,9 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; -using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Formats/README.md b/src/ImageSharp/Formats/Tga/README.md similarity index 78% rename from src/ImageSharp/Formats/README.md rename to src/ImageSharp/Formats/Tga/README.md index 4a2b401b1d..219f111b9d 100644 --- a/src/ImageSharp/Formats/README.md +++ b/src/ImageSharp/Formats/Tga/README.md @@ -1,4 +1,4 @@ -# Encoder/Decoder for true vision targa files +# Encoder/Decoder for true vision targa files Useful links for reference: diff --git a/src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs b/src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs new file mode 100644 index 0000000000..08d1475268 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + internal static class BitWriterUtils + { + public static void WriteBits(Span buffer, int pos, uint count, byte value) + { + int bitPos = pos % 8; + int bufferPos = pos / 8; + int startIdx = bufferPos + bitPos; + int endIdx = (int)(startIdx + count); + + if (value == 1) + { + for (int i = startIdx; i < endIdx; i++) + { + WriteBit(buffer, bufferPos, bitPos); + + bitPos++; + if (bitPos >= 8) + { + bitPos = 0; + bufferPos++; + } + } + } + else + { + for (int i = startIdx; i < endIdx; i++) + { + WriteZeroBit(buffer, bufferPos, bitPos); + + bitPos++; + if (bitPos >= 8) + { + bitPos = 0; + bufferPos++; + } + } + } + } + + public static void WriteBit(Span buffer, int bufferPos, int bitPos) => buffer[bufferPos] |= (byte)(1 << (7 - bitPos)); + + public static void WriteZeroBit(Span buffer, int bufferPos, int bitPos) => buffer[bufferPos] = (byte)(buffer[bufferPos] & ~(1 << (7 - bitPos))); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs new file mode 100644 index 0000000000..225036f909 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + internal sealed class DeflateCompressor : TiffBaseCompressor + { + private readonly DeflateCompressionLevel compressionLevel; + + private readonly MemoryStream memoryStream = new MemoryStream(); + + public DeflateCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor, DeflateCompressionLevel compressionLevel) + : base(output, allocator, width, bitsPerPixel, predictor) + => this.compressionLevel = compressionLevel; + + /// + public override TiffCompression Method => TiffCompression.Deflate; + + /// + public override void Initialize(int rowsPerStrip) + { + } + + /// + public override void CompressStrip(Span rows, int height) + { + this.memoryStream.Seek(0, SeekOrigin.Begin); + using (var stream = new ZlibDeflateStream(this.Allocator, this.memoryStream, this.compressionLevel)) + { + if (this.Predictor == TiffPredictor.Horizontal) + { + HorizontalPredictor.ApplyHorizontalPrediction(rows, this.BytesPerRow, this.BitsPerPixel); + } + + stream.Write(rows); + stream.Flush(); + } + + int size = (int)this.memoryStream.Position; + +#if !NETSTANDARD1_3 + byte[] buffer = this.memoryStream.GetBuffer(); + this.Output.Write(buffer, 0, size); +#else + this.memoryStream.SetLength(size); + this.memoryStream.Position = 0; + this.memoryStream.CopyTo(this.Output); +#endif + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/LzwCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/LzwCompressor.cs new file mode 100644 index 0000000000..d2ae9096e2 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/LzwCompressor.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + internal sealed class LzwCompressor : TiffBaseCompressor + { + private TiffLzwEncoder lzwEncoder; + + public LzwCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor) + : base(output, allocator, width, bitsPerPixel, predictor) + { + } + + /// + public override TiffCompression Method => TiffCompression.Lzw; + + /// + public override void Initialize(int rowsPerStrip) => this.lzwEncoder = new TiffLzwEncoder(this.Allocator); + + /// + public override void CompressStrip(Span rows, int height) + { + if (this.Predictor == TiffPredictor.Horizontal) + { + HorizontalPredictor.ApplyHorizontalPrediction(rows, this.BytesPerRow, this.BitsPerPixel); + } + + this.lzwEncoder.Encode(rows, this.Output); + } + + /// + protected override void Dispose(bool disposing) => this.lzwEncoder?.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/NoCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/NoCompressor.cs new file mode 100644 index 0000000000..79bb2e98f8 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/NoCompressor.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + internal sealed class NoCompressor : TiffBaseCompressor + { + public NoCompressor(Stream output, MemoryAllocator memoryAllocator, int width, int bitsPerPixel) + : base(output, memoryAllocator, width, bitsPerPixel) + { + } + + /// + public override TiffCompression Method => TiffCompression.None; + + /// + public override void Initialize(int rowsPerStrip) + { + } + + /// + public override void CompressStrip(Span rows, int height) => this.Output.Write(rows); + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsCompressor.cs new file mode 100644 index 0000000000..5a23831878 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsCompressor.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + internal sealed class PackBitsCompressor : TiffBaseCompressor + { + private IManagedByteBuffer pixelData; + + public PackBitsCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel) + : base(output, allocator, width, bitsPerPixel) + { + } + + /// + public override TiffCompression Method => TiffCompression.PackBits; + + /// + public override void Initialize(int rowsPerStrip) + { + int additionalBytes = ((this.BytesPerRow + 126) / 127) + 1; + this.pixelData = this.Allocator.AllocateManagedByteBuffer(this.BytesPerRow + additionalBytes); + } + + /// + public override void CompressStrip(Span rows, int height) + { + DebugGuard.IsTrue(rows.Length % height == 0, "Invalid height"); + DebugGuard.IsTrue(this.BytesPerRow == rows.Length / height, "The widths must match"); + + Span span = this.pixelData.GetSpan(); + for (int i = 0; i < height; i++) + { + Span row = rows.Slice(i * this.BytesPerRow, this.BytesPerRow); + int size = PackBitsWriter.PackBits(row, span); + this.Output.Write(span.Slice(0, size)); + } + } + + /// + protected override void Dispose(bool disposing) => this.pixelData?.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs new file mode 100644 index 0000000000..30d21e54ce --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + /// + /// Pack Bits compression for tiff images. See Tiff Spec v6, section 9. + /// + internal static class PackBitsWriter + { + public static int PackBits(ReadOnlySpan rowSpan, Span compressedRowSpan) + { + int maxRunLength = 127; + int posInRowSpan = 0; + int bytesWritten = 0; + int literalRunLength = 0; + + while (posInRowSpan < rowSpan.Length) + { + bool useReplicateRun = IsReplicateRun(rowSpan, posInRowSpan); + if (useReplicateRun) + { + if (literalRunLength > 0) + { + WriteLiteralRun(rowSpan, posInRowSpan, literalRunLength, compressedRowSpan, bytesWritten); + bytesWritten += literalRunLength + 1; + } + + // Write a run with the same bytes. + int runLength = FindRunLength(rowSpan, posInRowSpan, maxRunLength); + WriteRun(rowSpan, posInRowSpan, runLength, compressedRowSpan, bytesWritten); + + bytesWritten += 2; + literalRunLength = 0; + posInRowSpan += runLength; + continue; + } + + literalRunLength++; + posInRowSpan++; + + if (literalRunLength >= maxRunLength) + { + WriteLiteralRun(rowSpan, posInRowSpan, literalRunLength, compressedRowSpan, bytesWritten); + bytesWritten += literalRunLength + 1; + literalRunLength = 0; + } + } + + if (literalRunLength > 0) + { + WriteLiteralRun(rowSpan, posInRowSpan, literalRunLength, compressedRowSpan, bytesWritten); + bytesWritten += literalRunLength + 1; + } + + return bytesWritten; + } + + private static void WriteLiteralRun(ReadOnlySpan rowSpan, int end, int literalRunLength, Span compressedRowSpan, int compressedRowPos) + { + DebugGuard.MustBeLessThanOrEqualTo(literalRunLength, 127, nameof(literalRunLength)); + + int literalRunStart = end - literalRunLength; + sbyte runLength = (sbyte)(literalRunLength - 1); + compressedRowSpan[compressedRowPos] = (byte)runLength; + rowSpan.Slice(literalRunStart, literalRunLength).CopyTo(compressedRowSpan.Slice(compressedRowPos + 1)); + } + + private static void WriteRun(ReadOnlySpan rowSpan, int start, int runLength, Span compressedRowSpan, int compressedRowPos) + { + DebugGuard.MustBeLessThanOrEqualTo(runLength, 127, nameof(runLength)); + + sbyte headerByte = (sbyte)(-runLength + 1); + compressedRowSpan[compressedRowPos] = (byte)headerByte; + compressedRowSpan[compressedRowPos + 1] = rowSpan[start]; + } + + private static bool IsReplicateRun(ReadOnlySpan rowSpan, int startPos) + { + // We consider run which has at least 3 same consecutive bytes a candidate for a run. + var startByte = rowSpan[startPos]; + int count = 0; + for (int i = startPos + 1; i < rowSpan.Length; i++) + { + if (rowSpan[i] == startByte) + { + count++; + if (count >= 2) + { + return true; + } + } + else + { + break; + } + } + + return false; + } + + private static int FindRunLength(ReadOnlySpan rowSpan, int startPos, int maxRunLength) + { + var startByte = rowSpan[startPos]; + int count = 1; + for (int i = startPos + 1; i < rowSpan.Length; i++) + { + if (rowSpan[i] == startByte) + { + count++; + } + else + { + break; + } + + if (count == maxRunLength) + { + break; + } + } + + return count; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs new file mode 100644 index 0000000000..3e9b7f4e63 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs @@ -0,0 +1,592 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + /// + /// Bitwriter for writing compressed CCITT T4 1D data. + /// + internal sealed class T4BitCompressor : TiffBaseCompressor + { + private const uint WhiteZeroRunTermCode = 0x35; + + private const uint BlackZeroRunTermCode = 0x37; + + private static readonly uint[] MakeupRunLength = + { + 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792, 1856, 1920, 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560 + }; + + private static readonly Dictionary WhiteLen4TermCodes = new Dictionary() + { + { 2, 0x7 }, { 3, 0x8 }, { 4, 0xB }, { 5, 0xC }, { 6, 0xE }, { 7, 0xF } + }; + + private static readonly Dictionary WhiteLen5TermCodes = new Dictionary() + { + { 8, 0x13 }, { 9, 0x14 }, { 10, 0x7 }, { 11, 0x8 } + }; + + private static readonly Dictionary WhiteLen6TermCodes = new Dictionary() + { + { 1, 0x7 }, { 12, 0x8 }, { 13, 0x3 }, { 14, 0x34 }, { 15, 0x35 }, { 16, 0x2A }, { 17, 0x2B } + }; + + private static readonly Dictionary WhiteLen7TermCodes = new Dictionary() + { + { 18, 0x27 }, { 19, 0xC }, { 20, 0x8 }, { 21, 0x17 }, { 22, 0x3 }, { 23, 0x4 }, { 24, 0x28 }, { 25, 0x2B }, { 26, 0x13 }, + { 27, 0x24 }, { 28, 0x18 } + }; + + private static readonly Dictionary WhiteLen8TermCodes = new Dictionary() + { + { 0, WhiteZeroRunTermCode }, { 29, 0x2 }, { 30, 0x3 }, { 31, 0x1A }, { 32, 0x1B }, { 33, 0x12 }, { 34, 0x13 }, { 35, 0x14 }, + { 36, 0x15 }, { 37, 0x16 }, { 38, 0x17 }, { 39, 0x28 }, { 40, 0x29 }, { 41, 0x2A }, { 42, 0x2B }, { 43, 0x2C }, { 44, 0x2D }, + { 45, 0x4 }, { 46, 0x5 }, { 47, 0xA }, { 48, 0xB }, { 49, 0x52 }, { 50, 0x53 }, { 51, 0x54 }, { 52, 0x55 }, { 53, 0x24 }, + { 54, 0x25 }, { 55, 0x58 }, { 56, 0x59 }, { 57, 0x5A }, { 58, 0x5B }, { 59, 0x4A }, { 60, 0x4B }, { 61, 0x32 }, { 62, 0x33 }, + { 63, 0x34 } + }; + + private static readonly Dictionary BlackLen2TermCodes = new Dictionary() + { + { 2, 0x3 }, { 3, 0x2 } + }; + + private static readonly Dictionary BlackLen3TermCodes = new Dictionary() + { + { 1, 0x2 }, { 4, 0x3 } + }; + + private static readonly Dictionary BlackLen4TermCodes = new Dictionary() + { + { 5, 0x3 }, { 6, 0x2 } + }; + + private static readonly Dictionary BlackLen5TermCodes = new Dictionary() + { + { 7, 0x3 } + }; + + private static readonly Dictionary BlackLen6TermCodes = new Dictionary() + { + { 8, 0x5 }, { 9, 0x4 } + }; + + private static readonly Dictionary BlackLen7TermCodes = new Dictionary() + { + { 10, 0x4 }, { 11, 0x5 }, { 12, 0x7 } + }; + + private static readonly Dictionary BlackLen8TermCodes = new Dictionary() + { + { 13, 0x4 }, { 14, 0x7 } + }; + + private static readonly Dictionary BlackLen9TermCodes = new Dictionary() + { + { 15, 0x18 } + }; + + private static readonly Dictionary BlackLen10TermCodes = new Dictionary() + { + { 0, BlackZeroRunTermCode }, { 16, 0x17 }, { 17, 0x18 }, { 18, 0x8 } + }; + + private static readonly Dictionary BlackLen11TermCodes = new Dictionary() + { + { 19, 0x67 }, { 20, 0x68 }, { 21, 0x6C }, { 22, 0x37 }, { 23, 0x28 }, { 24, 0x17 }, { 25, 0x18 } + }; + + private static readonly Dictionary BlackLen12TermCodes = new Dictionary() + { + { 26, 0xCA }, { 27, 0xCB }, { 28, 0xCC }, { 29, 0xCD }, { 30, 0x68 }, { 31, 0x69 }, { 32, 0x6A }, { 33, 0x6B }, { 34, 0xD2 }, + { 35, 0xD3 }, { 36, 0xD4 }, { 37, 0xD5 }, { 38, 0xD6 }, { 39, 0xD7 }, { 40, 0x6C }, { 41, 0x6D }, { 42, 0xDA }, { 43, 0xDB }, + { 44, 0x54 }, { 45, 0x55 }, { 46, 0x56 }, { 47, 0x57 }, { 48, 0x64 }, { 49, 0x65 }, { 50, 0x52 }, { 51, 0x53 }, { 52, 0x24 }, + { 53, 0x37 }, { 54, 0x38 }, { 55, 0x27 }, { 56, 0x28 }, { 57, 0x58 }, { 58, 0x59 }, { 59, 0x2B }, { 60, 0x2C }, { 61, 0x5A }, + { 62, 0x66 }, { 63, 0x67 } + }; + + private static readonly Dictionary WhiteLen5MakeupCodes = new Dictionary() + { + { 64, 0x1B }, { 128, 0x12 } + }; + + private static readonly Dictionary WhiteLen6MakeupCodes = new Dictionary() + { + { 192, 0x17 }, { 1664, 0x18 } + }; + + private static readonly Dictionary WhiteLen8MakeupCodes = new Dictionary() + { + { 320, 0x36 }, { 384, 0x37 }, { 448, 0x64 }, { 512, 0x65 }, { 576, 0x68 }, { 640, 0x67 } + }; + + private static readonly Dictionary WhiteLen7MakeupCodes = new Dictionary() + { + { 256, 0x37 } + }; + + private static readonly Dictionary WhiteLen9MakeupCodes = new Dictionary() + { + { 704, 0xCC }, { 768, 0xCD }, { 832, 0xD2 }, { 896, 0xD3 }, { 960, 0xD4 }, { 1024, 0xD5 }, { 1088, 0xD6 }, + { 1152, 0xD7 }, { 1216, 0xD8 }, { 1280, 0xD9 }, { 1344, 0xDA }, { 1408, 0xDB }, { 1472, 0x98 }, { 1536, 0x99 }, + { 1600, 0x9A }, { 1728, 0x9B } + }; + + private static readonly Dictionary WhiteLen11MakeupCodes = new Dictionary() + { + { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } + }; + + private static readonly Dictionary WhiteLen12MakeupCodes = new Dictionary() + { + { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, + { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } + }; + + private static readonly Dictionary BlackLen10MakeupCodes = new Dictionary() + { + { 64, 0xF } + }; + + private static readonly Dictionary BlackLen11MakeupCodes = new Dictionary() + { + { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } + }; + + private static readonly Dictionary BlackLen12MakeupCodes = new Dictionary() + { + { 128, 0xC8 }, { 192, 0xC9 }, { 256, 0x5B }, { 320, 0x33 }, { 384, 0x34 }, { 448, 0x35 }, + { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, + { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } + }; + + private static readonly Dictionary BlackLen13MakeupCodes = new Dictionary() + { + { 512, 0x6C }, { 576, 0x6D }, { 640, 0x4A }, { 704, 0x4B }, { 768, 0x4C }, { 832, 0x4D }, { 896, 0x72 }, + { 960, 0x73 }, { 1024, 0x74 }, { 1088, 0x75 }, { 1152, 0x76 }, { 1216, 0x77 }, { 1280, 0x52 }, { 1344, 0x53 }, + { 1408, 0x54 }, { 1472, 0x55 }, { 1536, 0x5A }, { 1600, 0x5B }, { 1664, 0x64 }, { 1728, 0x65 } + }; + + /// + /// The modified huffman is basically the same as CCITT T4, but without EOL markers and padding at the end of the rows. + /// + private readonly bool useModifiedHuffman; + + private IMemoryOwner compressedDataBuffer; + + private int bytePosition; + + private byte bitPosition; + + /// + /// Initializes a new instance of the class. + /// + /// The output. + /// The allocator. + /// The width. + /// The bits per pixel. + /// Indicates if the modified huffman RLE should be used. + public T4BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, bool useModifiedHuffman = false) + : base(output, allocator, width, bitsPerPixel) + { + this.bytePosition = 0; + this.bitPosition = 0; + this.useModifiedHuffman = useModifiedHuffman; + } + + /// + public override TiffCompression Method => this.useModifiedHuffman ? TiffCompression.Ccitt1D : TiffCompression.CcittGroup3Fax; + + /// + public override void Initialize(int rowsPerStrip) + { + // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. + int maxNeededBytes = this.Width * rowsPerStrip; + this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); + } + + /// Writes a image compressed with CCITT T4 to the stream. + /// The pixels as 8-bit gray array. + /// The strip height. + public override void CompressStrip(Span pixelsAsGray, int height) + { + DebugGuard.IsTrue(pixelsAsGray.Length / height == this.Width, "Values must be equals"); + DebugGuard.IsTrue(pixelsAsGray.Length % height == 0, "Values must be equals"); + + this.compressedDataBuffer.Clear(); + Span compressedData = this.compressedDataBuffer.GetSpan(); + + this.bytePosition = 0; + this.bitPosition = 0; + + if (!this.useModifiedHuffman) + { + // An EOL code is expected at the start of the data. + this.WriteCode(12, 1, compressedData); + } + + for (int y = 0; y < height; y++) + { + bool isWhiteRun = true; + bool isStartOrRow = true; + int x = 0; + + Span row = pixelsAsGray.Slice(y * this.Width, this.Width); + while (x < this.Width) + { + uint runLength = 0; + for (int i = x; i < this.Width; i++) + { + if (isWhiteRun && row[i] != 255) + { + break; + } + + if (isWhiteRun && row[i] == 255) + { + runLength++; + continue; + } + + if (!isWhiteRun && row[i] != 0) + { + break; + } + + if (!isWhiteRun && row[i] == 0) + { + runLength++; + } + } + + if (isStartOrRow && runLength == 0) + { + this.WriteCode(8, WhiteZeroRunTermCode, compressedData); + + isWhiteRun = false; + isStartOrRow = false; + continue; + } + + uint code; + uint codeLength; + if (runLength <= 63) + { + code = this.GetTermCode(runLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + x += (int)runLength; + } + else + { + runLength = this.GetBestFittingMakeupRunLength(runLength); + code = this.GetMakeupCode(runLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + x += (int)runLength; + + // If we are at the end of the line with a makeup code, we need to write a final term code with a length of zero. + if (x == this.Width) + { + if (isWhiteRun) + { + this.WriteCode(8, WhiteZeroRunTermCode, compressedData); + } + else + { + this.WriteCode(10, BlackZeroRunTermCode, compressedData); + } + } + + continue; + } + + isStartOrRow = false; + isWhiteRun = !isWhiteRun; + } + + this.WriteEndOfLine(compressedData); + } + + // Write the compressed data to the stream. + int bytesToWrite = this.bitPosition != 0 ? this.bytePosition + 1 : this.bytePosition; + this.Output.Write(compressedData.Slice(0, bytesToWrite)); + } + + protected override void Dispose(bool disposing) => this.compressedDataBuffer?.Dispose(); + + private void WriteEndOfLine(Span compressedData) + { + if (this.useModifiedHuffman) + { + // Check if padding is necessary. + if (this.bitPosition % 8 != 0) + { + // Skip padding bits, move to next byte. + this.bytePosition++; + this.bitPosition = 0; + } + } + else + { + // Write EOL. + this.WriteCode(12, 1, compressedData); + } + } + + private void WriteCode(uint codeLength, uint code, Span compressedData) + { + while (codeLength > 0) + { + int bitNumber = (int)codeLength; + bool bit = (code & (1 << (bitNumber - 1))) != 0; + if (bit) + { + BitWriterUtils.WriteBit(compressedData, this.bytePosition, this.bitPosition); + } + else + { + BitWriterUtils.WriteZeroBit(compressedData, this.bytePosition, this.bitPosition); + } + + this.bitPosition++; + if (this.bitPosition == 8) + { + this.bytePosition++; + this.bitPosition = 0; + } + + codeLength--; + } + } + + private uint GetBestFittingMakeupRunLength(uint runLength) + { + for (int i = 0; i < MakeupRunLength.Length - 1; i++) + { + if (MakeupRunLength[i] <= runLength && MakeupRunLength[i + 1] > runLength) + { + return MakeupRunLength[i]; + } + } + + return MakeupRunLength[MakeupRunLength.Length - 1]; + } + + private uint GetTermCode(uint runLength, out uint codeLength, bool isWhiteRun) + { + if (isWhiteRun) + { + return this.GetWhiteTermCode(runLength, out codeLength); + } + + return this.GetBlackTermCode(runLength, out codeLength); + } + + private uint GetMakeupCode(uint runLength, out uint codeLength, bool isWhiteRun) + { + if (isWhiteRun) + { + return this.GetWhiteMakeupCode(runLength, out codeLength); + } + + return this.GetBlackMakeupCode(runLength, out codeLength); + } + + private uint GetWhiteMakeupCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (WhiteLen5MakeupCodes.ContainsKey(runLength)) + { + codeLength = 5; + return WhiteLen5MakeupCodes[runLength]; + } + + if (WhiteLen6MakeupCodes.ContainsKey(runLength)) + { + codeLength = 6; + return WhiteLen6MakeupCodes[runLength]; + } + + if (WhiteLen7MakeupCodes.ContainsKey(runLength)) + { + codeLength = 7; + return WhiteLen7MakeupCodes[runLength]; + } + + if (WhiteLen8MakeupCodes.ContainsKey(runLength)) + { + codeLength = 8; + return WhiteLen8MakeupCodes[runLength]; + } + + if (WhiteLen9MakeupCodes.ContainsKey(runLength)) + { + codeLength = 9; + return WhiteLen9MakeupCodes[runLength]; + } + + if (WhiteLen11MakeupCodes.ContainsKey(runLength)) + { + codeLength = 11; + return WhiteLen11MakeupCodes[runLength]; + } + + if (WhiteLen12MakeupCodes.ContainsKey(runLength)) + { + codeLength = 12; + return WhiteLen12MakeupCodes[runLength]; + } + + return 0; + } + + private uint GetBlackMakeupCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (BlackLen10MakeupCodes.ContainsKey(runLength)) + { + codeLength = 10; + return BlackLen10MakeupCodes[runLength]; + } + + if (BlackLen11MakeupCodes.ContainsKey(runLength)) + { + codeLength = 11; + return BlackLen11MakeupCodes[runLength]; + } + + if (BlackLen12MakeupCodes.ContainsKey(runLength)) + { + codeLength = 12; + return BlackLen12MakeupCodes[runLength]; + } + + if (BlackLen13MakeupCodes.ContainsKey(runLength)) + { + codeLength = 13; + return BlackLen13MakeupCodes[runLength]; + } + + return 0; + } + + private uint GetWhiteTermCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (WhiteLen4TermCodes.ContainsKey(runLength)) + { + codeLength = 4; + return WhiteLen4TermCodes[runLength]; + } + + if (WhiteLen5TermCodes.ContainsKey(runLength)) + { + codeLength = 5; + return WhiteLen5TermCodes[runLength]; + } + + if (WhiteLen6TermCodes.ContainsKey(runLength)) + { + codeLength = 6; + return WhiteLen6TermCodes[runLength]; + } + + if (WhiteLen7TermCodes.ContainsKey(runLength)) + { + codeLength = 7; + return WhiteLen7TermCodes[runLength]; + } + + if (WhiteLen8TermCodes.ContainsKey(runLength)) + { + codeLength = 8; + return WhiteLen8TermCodes[runLength]; + } + + return 0; + } + + private uint GetBlackTermCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (BlackLen2TermCodes.ContainsKey(runLength)) + { + codeLength = 2; + return BlackLen2TermCodes[runLength]; + } + + if (BlackLen3TermCodes.ContainsKey(runLength)) + { + codeLength = 3; + return BlackLen3TermCodes[runLength]; + } + + if (BlackLen4TermCodes.ContainsKey(runLength)) + { + codeLength = 4; + return BlackLen4TermCodes[runLength]; + } + + if (BlackLen5TermCodes.ContainsKey(runLength)) + { + codeLength = 5; + return BlackLen5TermCodes[runLength]; + } + + if (BlackLen6TermCodes.ContainsKey(runLength)) + { + codeLength = 6; + return BlackLen6TermCodes[runLength]; + } + + if (BlackLen7TermCodes.ContainsKey(runLength)) + { + codeLength = 7; + return BlackLen7TermCodes[runLength]; + } + + if (BlackLen8TermCodes.ContainsKey(runLength)) + { + codeLength = 8; + return BlackLen8TermCodes[runLength]; + } + + if (BlackLen9TermCodes.ContainsKey(runLength)) + { + codeLength = 9; + return BlackLen9TermCodes[runLength]; + } + + if (BlackLen10TermCodes.ContainsKey(runLength)) + { + codeLength = 10; + return BlackLen10TermCodes[runLength]; + } + + if (BlackLen11TermCodes.ContainsKey(runLength)) + { + codeLength = 11; + return BlackLen11TermCodes[runLength]; + } + + if (BlackLen12TermCodes.ContainsKey(runLength)) + { + codeLength = 12; + return BlackLen12TermCodes[runLength]; + } + + return 0; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffLzwEncoder.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffLzwEncoder.cs new file mode 100644 index 0000000000..baeabdbb20 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffLzwEncoder.cs @@ -0,0 +1,270 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + /* + This implementation is a port of a java tiff encoder by Harald Kuhr: https://github.com/haraldk/TwelveMonkeys + + Original licence: + + BSD 3-Clause License + + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + ** Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + /// + /// Encodes and compresses the image data using dynamic Lempel-Ziv compression. + /// + /// + /// + /// This code is based on the used for GIF encoding. There is potential + /// for a shared implementation. Differences between the GIF and TIFF implementations of the LZW + /// encoding are: (i) The GIF implementation includes an initial 'data size' byte, whilst this is + /// always 8 for TIFF. (ii) The GIF implementation writes a number of sub-blocks with an initial + /// byte indicating the length of the sub-block. In TIFF the data is written as a single block + /// with no length indicator (this can be determined from the 'StripByteCounts' entry). + /// + /// + internal sealed class TiffLzwEncoder : IDisposable + { + // Clear: Re-initialize tables. + private static readonly int ClearCode = 256; + + // End of Information. + private static readonly int EoiCode = 257; + + private static readonly int MinBits = 9; + private static readonly int MaxBits = 12; + + private static readonly int TableSize = 1 << MaxBits; + + // A child is made up of a parent (or prefix) code plus a suffix byte + // and siblings are strings with a common parent(or prefix) and different suffix bytes. + private readonly IMemoryOwner children; + + private readonly IMemoryOwner siblings; + + private readonly IMemoryOwner suffixes; + + // Initial setup + private int parent; + private int bitsPerCode; + private int nextValidCode; + private int maxCode; + + // Buffer for partial codes + private int bits; + private int bitPos; + private int bufferPosition; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + public TiffLzwEncoder(MemoryAllocator memoryAllocator) + { + this.children = memoryAllocator.Allocate(TableSize); + this.siblings = memoryAllocator.Allocate(TableSize); + this.suffixes = memoryAllocator.Allocate(TableSize); + } + + /// + /// Encodes and compresses the indexed pixels to the stream. + /// + /// The data to compress. + /// The stream to write to. + public void Encode(Span data, Stream stream) + { + this.Reset(); + + Span childrenSpan = this.children.GetSpan(); + Span suffixesSpan = this.suffixes.GetSpan(); + Span siblingsSpan = this.siblings.GetSpan(); + int length = data.Length; + + if (length == 0) + { + return; + } + + if (this.parent == -1) + { + // Init stream. + this.WriteCode(stream, ClearCode); + this.parent = this.ReadNextByte(data); + } + + while (this.bufferPosition < data.Length) + { + int value = this.ReadNextByte(data); + int child = childrenSpan[this.parent]; + + if (child > 0) + { + if (suffixesSpan[child] == value) + { + this.parent = child; + } + else + { + int sibling = child; + + while (true) + { + if (siblingsSpan[sibling] > 0) + { + sibling = siblingsSpan[sibling]; + + if (suffixesSpan[sibling] == value) + { + this.parent = sibling; + break; + } + } + else + { + siblingsSpan[sibling] = (short)this.nextValidCode; + suffixesSpan[this.nextValidCode] = (short)value; + this.WriteCode(stream, this.parent); + this.parent = value; + this.nextValidCode++; + + this.IncreaseCodeSizeOrResetIfNeeded(stream); + + break; + } + } + } + } + else + { + childrenSpan[this.parent] = (short)this.nextValidCode; + suffixesSpan[this.nextValidCode] = (short)value; + this.WriteCode(stream, this.parent); + this.parent = value; + this.nextValidCode++; + + this.IncreaseCodeSizeOrResetIfNeeded(stream); + } + } + + // Write EOI when we are done. + this.WriteCode(stream, this.parent); + this.WriteCode(stream, EoiCode); + + // Flush partial codes by writing 0 pad. + if (this.bitPos > 0) + { + this.WriteCode(stream, 0); + } + } + + /// + public void Dispose() + { + this.children.Dispose(); + this.siblings.Dispose(); + this.suffixes.Dispose(); + } + + private void Reset() + { + this.children.Clear(); + this.siblings.Clear(); + this.suffixes.Clear(); + + this.parent = -1; + this.bitsPerCode = MinBits; + this.nextValidCode = EoiCode + 1; + this.maxCode = (1 << this.bitsPerCode) - 1; + + this.bits = 0; + this.bitPos = 0; + this.bufferPosition = 0; + } + + private byte ReadNextByte(Span data) => data[this.bufferPosition++]; + + private void IncreaseCodeSizeOrResetIfNeeded(Stream stream) + { + if (this.nextValidCode > this.maxCode) + { + if (this.bitsPerCode == MaxBits) + { + // Reset stream by writing Clear code. + this.WriteCode(stream, ClearCode); + + // Reset tables. + this.ResetTables(); + } + else + { + // Increase code size. + this.bitsPerCode++; + this.maxCode = MaxValue(this.bitsPerCode); + } + } + } + + private void WriteCode(Stream stream, int code) + { + this.bits = (this.bits << this.bitsPerCode) | (code & this.maxCode); + this.bitPos += this.bitsPerCode; + + while (this.bitPos >= 8) + { + int b = (this.bits >> (this.bitPos - 8)) & 0xff; + stream.WriteByte((byte)b); + this.bitPos -= 8; + } + + this.bits &= BitmaskFor(this.bitPos); + } + + private void ResetTables() + { + this.children.GetSpan().Fill(0); + this.siblings.GetSpan().Fill(0); + this.bitsPerCode = MinBits; + this.maxCode = MaxValue(this.bitsPerCode); + this.nextValidCode = EoiCode + 1; + } + + private static int MaxValue(int codeLen) => (1 << codeLen) - 1; + + private static int BitmaskFor(int bits) => MaxValue(bits); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs new file mode 100644 index 0000000000..67af4ff6cb --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO.Compression; + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is compressed using Deflate compression. + /// + /// + /// Note that the 'OldDeflate' compression type is identical to the 'Deflate' compression type. + /// + internal class DeflateTiffCompression : TiffBaseDecompressor + { + /// + /// Initializes a new instance of the class. + /// + /// The memoryAllocator to use for buffer allocations. + /// The image width. + /// The bits used per pixel. + /// The tiff predictor used. + public DeflateTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + { + long pos = stream.Position; + using (var deframeStream = new ZlibInflateStream( + stream, + () => + { + int left = (int)(byteCount - (stream.Position - pos)); + return left > 0 ? left : 0; + })) + { + deframeStream.AllocateNewBytes(byteCount, true); + DeflateStream dataStream = deframeStream.CompressedStream; + dataStream.Read(buffer, 0, buffer.Length); + } + + if (this.Predictor == TiffPredictor.Horizontal) + { + HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel); + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwString.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwString.cs new file mode 100644 index 0000000000..0f4fb9c9e2 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwString.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Represents a lzw string with a code word and a code length. + /// + public class LzwString + { + private static readonly LzwString Empty = new LzwString(0, 0, 0, null); + + private readonly LzwString previous; + private readonly byte value; + + /// + /// Initializes a new instance of the class. + /// + /// The code word. + public LzwString(byte code) + : this(code, code, 1, null) + { + } + + private LzwString(byte value, byte firstChar, int length, LzwString previous) + { + this.value = value; + this.FirstChar = firstChar; + this.Length = length; + this.previous = previous; + } + + /// + /// Gets the code length; + /// + public int Length { get; } + + /// + /// Gets the first character of the codeword. + /// + public byte FirstChar { get; } + + /// + /// Concatenates two code words. + /// + /// The code word to concatenate. + /// A concatenated lzw string. + public LzwString Concatenate(byte other) + { + if (this == Empty) + { + return new LzwString(other); + } + + return new LzwString(other, this.FirstChar, this.Length + 1, this); + } + + /// + /// Writes decoded pixel to buffer at a given position. + /// + /// The buffer to write to. + /// The position to write to. + /// The number of bytes written. + public int WriteTo(Span buffer, int offset) + { + if (this.Length == 0) + { + return 0; + } + + if (this.Length == 1) + { + buffer[offset] = this.value; + return 1; + } + + LzwString e = this; + int endIdx = this.Length - 1; + if (endIdx >= buffer.Length) + { + TiffThrowHelper.ThrowImageFormatException("Error reading lzw compressed stream. Either pixel buffer to write to is to small or code length is invalid!"); + } + + for (int i = endIdx; i >= 0; i--) + { + buffer[offset + i] = e.value; + e = e.previous; + } + + return this.Length; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs new file mode 100644 index 0000000000..7e75dd4f05 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is compressed using LZW compression. + /// + internal class LzwTiffCompression : TiffBaseDecompressor + { + /// + /// Initializes a new instance of the class. + /// + /// The memoryAllocator to use for buffer allocations. + /// The image width. + /// The bits used per pixel. + /// The tiff predictor used. + public LzwTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + { + var decoder = new TiffLzwDecoder(stream); + decoder.DecodePixels(buffer); + + if (this.Predictor == TiffPredictor.Horizontal) + { + HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel); + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs new file mode 100644 index 0000000000..017591e53f --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is compressed using Modified Huffman Compression. + /// + internal class ModifiedHuffmanTiffCompression : T4TiffCompression + { + private readonly byte whiteValue; + + private readonly byte blackValue; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The image width. + /// The number of bits per pixel. + /// The photometric interpretation. + public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation) + : base(allocator, width, bitsPerPixel, FaxCompressionOptions.None, photometricInterpretation) + { + bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; + this.whiteValue = (byte)(isWhiteZero ? 0 : 1); + this.blackValue = (byte)(isWhiteZero ? 1 : 0); + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + { + using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding: false, isModifiedHuffman: true); + + buffer.Clear(); + uint bitsWritten = 0; + uint pixelsWritten = 0; + while (bitReader.HasMoreData) + { + bitReader.ReadNextRun(); + + if (bitReader.RunLength > 0) + { + if (bitReader.IsWhiteRun) + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue); + bitsWritten += bitReader.RunLength; + pixelsWritten += bitReader.RunLength; + } + else + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue); + bitsWritten += bitReader.RunLength; + pixelsWritten += bitReader.RunLength; + } + } + + if (pixelsWritten % this.Width == 0) + { + bitReader.StartNewRow(); + + // Write padding bits, if necessary. + uint pad = 8 - (bitsWritten % 8); + if (pad != 8) + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0); + bitsWritten += pad; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs new file mode 100644 index 0000000000..58a1c98781 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is not compressed. + /// + internal class NoneTiffCompression : TiffBaseDecompressor + { + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + public NoneTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel) + : base(memoryAllocator, width, bitsPerPixel) + { + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) => _ = stream.Read(buffer, 0, Math.Min(buffer.Length, byteCount)); + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs new file mode 100644 index 0000000000..e14736b734 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is compressed using PackBits compression. + /// + internal class PackBitsTiffCompression : TiffBaseDecompressor + { + private IMemoryOwner compressedDataMemory; + + /// + /// Initializes a new instance of the class. + /// + /// The memoryAllocator to use for buffer allocations. + /// The width of the image. + /// The number of bits per pixel. + public PackBitsTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel) + : base(memoryAllocator, width, bitsPerPixel) + { + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + { + if (this.compressedDataMemory == null) + { + this.compressedDataMemory = this.Allocator.Allocate(byteCount); + } + else if (this.compressedDataMemory.Length() < byteCount) + { + this.compressedDataMemory.Dispose(); + this.compressedDataMemory = this.Allocator.Allocate(byteCount); + } + + Span compressedData = this.compressedDataMemory.GetSpan(); + + stream.Read(compressedData, 0, byteCount); + int compressedOffset = 0; + int decompressedOffset = 0; + + while (compressedOffset < byteCount) + { + byte headerByte = compressedData[compressedOffset]; + + if (headerByte <= 127) + { + int literalOffset = compressedOffset + 1; + int literalLength = compressedData[compressedOffset] + 1; + + if ((literalOffset + literalLength) > compressedData.Length) + { + TiffThrowHelper.ThrowImageFormatException("Tiff packbits compression error: not enough data."); + } + + compressedData.Slice(literalOffset, literalLength).CopyTo(buffer.Slice(decompressedOffset)); + + compressedOffset += literalLength + 1; + decompressedOffset += literalLength; + } + else if (headerByte == 0x80) + { + compressedOffset += 1; + } + else + { + byte repeatData = compressedData[compressedOffset + 1]; + int repeatLength = 257 - headerByte; + + ArrayCopyRepeat(repeatData, buffer, decompressedOffset, repeatLength); + + compressedOffset += 2; + decompressedOffset += repeatLength; + } + } + } + + private static void ArrayCopyRepeat(byte value, Span destinationArray, int destinationIndex, int length) + { + for (int i = 0; i < length; i++) + { + destinationArray[i + destinationIndex] = value; + } + } + + /// + protected override void Dispose(bool disposing) => this.compressedDataMemory?.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs new file mode 100644 index 0000000000..09f8c71f72 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs @@ -0,0 +1,842 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Bitreader for reading compressed CCITT T4 1D data. + /// + internal class T4BitReader : IDisposable + { + /// + /// Number of bits read. + /// + private int bitsRead; + + /// + /// Current value. + /// + private uint value; + + /// + /// Number of bits read for the current run value. + /// + private int curValueBitsRead; + + /// + /// Byte position in the buffer. + /// + private ulong position; + + /// + /// Indicates whether its the first line of data which is read from the image. + /// + private bool isFirstScanLine; + + /// + /// Indicates whether we have found a termination code which signals the end of a run. + /// + private bool terminationCodeFound; + + /// + /// We keep track if its the start of the row, because each run is expected to start with a white run. + /// If the image row itself starts with black, a white run of zero is expected. + /// + private bool isStartOfRow; + + /// + /// Indicates whether the modified huffman compression, as specified in the TIFF spec in section 10, is used. + /// + private readonly bool isModifiedHuffmanRle; + + /// + /// Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false. + /// + private readonly bool eolPadding; + + private readonly int dataLength; + + private const int MinCodeLength = 2; + + private readonly int maxCodeLength = 13; + + private static readonly Dictionary WhiteLen4TermCodes = new Dictionary() + { + { 0x7, 2 }, { 0x8, 3 }, { 0xB, 4 }, { 0xC, 5 }, { 0xE, 6 }, { 0xF, 7 } + }; + + private static readonly Dictionary WhiteLen5TermCodes = new Dictionary() + { + { 0x13, 8 }, { 0x14, 9 }, { 0x7, 10 }, { 0x8, 11 } + }; + + private static readonly Dictionary WhiteLen6TermCodes = new Dictionary() + { + { 0x7, 1 }, { 0x8, 12 }, { 0x3, 13 }, { 0x34, 14 }, { 0x35, 15 }, { 0x2A, 16 }, { 0x2B, 17 } + }; + + private static readonly Dictionary WhiteLen7TermCodes = new Dictionary() + { + { 0x27, 18 }, { 0xC, 19 }, { 0x8, 20 }, { 0x17, 21 }, { 0x3, 22 }, { 0x4, 23 }, { 0x28, 24 }, { 0x2B, 25 }, { 0x13, 26 }, + { 0x24, 27 }, { 0x18, 28 } + }; + + private static readonly Dictionary WhiteLen8TermCodes = new Dictionary() + { + { 0x35, 0 }, { 0x2, 29 }, { 0x3, 30 }, { 0x1A, 31 }, { 0x1B, 32 }, { 0x12, 33 }, { 0x13, 34 }, { 0x14, 35 }, { 0x15, 36 }, + { 0x16, 37 }, { 0x17, 38 }, { 0x28, 39 }, { 0x29, 40 }, { 0x2A, 41 }, { 0x2B, 42 }, { 0x2C, 43 }, { 0x2D, 44 }, { 0x4, 45 }, + { 0x5, 46 }, { 0xA, 47 }, { 0xB, 48 }, { 0x52, 49 }, { 0x53, 50 }, { 0x54, 51 }, { 0x55, 52 }, { 0x24, 53 }, { 0x25, 54 }, + { 0x58, 55 }, { 0x59, 56 }, { 0x5A, 57 }, { 0x5B, 58 }, { 0x4A, 59 }, { 0x4B, 60 }, { 0x32, 61 }, { 0x33, 62 }, { 0x34, 63 } + }; + + private static readonly Dictionary BlackLen2TermCodes = new Dictionary() + { + { 0x3, 2 }, { 0x2, 3 } + }; + + private static readonly Dictionary BlackLen3TermCodes = new Dictionary() + { + { 0x2, 1 }, { 0x3, 4 } + }; + + private static readonly Dictionary BlackLen4TermCodes = new Dictionary() + { + { 0x3, 5 }, { 0x2, 6 } + }; + + private static readonly Dictionary BlackLen5TermCodes = new Dictionary() + { + { 0x3, 7 } + }; + + private static readonly Dictionary BlackLen6TermCodes = new Dictionary() + { + { 0x5, 8 }, { 0x4, 9 } + }; + + private static readonly Dictionary BlackLen7TermCodes = new Dictionary() + { + { 0x4, 10 }, { 0x5, 11 }, { 0x7, 12 } + }; + + private static readonly Dictionary BlackLen8TermCodes = new Dictionary() + { + { 0x4, 13 }, { 0x7, 14 } + }; + + private static readonly Dictionary BlackLen9TermCodes = new Dictionary() + { + { 0x18, 15 } + }; + + private static readonly Dictionary BlackLen10TermCodes = new Dictionary() + { + { 0x37, 0 }, { 0x17, 16 }, { 0x18, 17 }, { 0x8, 18 } + }; + + private static readonly Dictionary BlackLen11TermCodes = new Dictionary() + { + { 0x67, 19 }, { 0x68, 20 }, { 0x6C, 21 }, { 0x37, 22 }, { 0x28, 23 }, { 0x17, 24 }, { 0x18, 25 } + }; + + private static readonly Dictionary BlackLen12TermCodes = new Dictionary() + { + { 0xCA, 26 }, { 0xCB, 27 }, { 0xCC, 28 }, { 0xCD, 29 }, { 0x68, 30 }, { 0x69, 31 }, { 0x6A, 32 }, { 0x6B, 33 }, { 0xD2, 34 }, + { 0xD3, 35 }, { 0xD4, 36 }, { 0xD5, 37 }, { 0xD6, 38 }, { 0xD7, 39 }, { 0x6C, 40 }, { 0x6D, 41 }, { 0xDA, 42 }, { 0xDB, 43 }, + { 0x54, 44 }, { 0x55, 45 }, { 0x56, 46 }, { 0x57, 47 }, { 0x64, 48 }, { 0x65, 49 }, { 0x52, 50 }, { 0x53, 51 }, { 0x24, 52 }, + { 0x37, 53 }, { 0x38, 54 }, { 0x27, 55 }, { 0x28, 56 }, { 0x58, 57 }, { 0x59, 58 }, { 0x2B, 59 }, { 0x2C, 60 }, { 0x5A, 61 }, + { 0x66, 62 }, { 0x67, 63 } + }; + + private static readonly Dictionary WhiteLen5MakeupCodes = new Dictionary() + { + { 0x1B, 64 }, { 0x12, 128 } + }; + + private static readonly Dictionary WhiteLen6MakeupCodes = new Dictionary() + { + { 0x17, 192 }, { 0x18, 1664 } + }; + + private static readonly Dictionary WhiteLen8MakeupCodes = new Dictionary() + { + { 0x36, 320 }, { 0x37, 384 }, { 0x64, 448 }, { 0x65, 512 }, { 0x68, 576 }, { 0x67, 640 } + }; + + private static readonly Dictionary WhiteLen7MakeupCodes = new Dictionary() + { + { 0x37, 256 } + }; + + private static readonly Dictionary WhiteLen9MakeupCodes = new Dictionary() + { + { 0xCC, 704 }, { 0xCD, 768 }, { 0xD2, 832 }, { 0xD3, 896 }, { 0xD4, 960 }, { 0xD5, 1024 }, { 0xD6, 1088 }, + { 0xD7, 1152 }, { 0xD8, 1216 }, { 0xD9, 1280 }, { 0xDA, 1344 }, { 0xDB, 1408 }, { 0x98, 1472 }, { 0x99, 1536 }, + { 0x9A, 1600 }, { 0x9B, 1728 } + }; + + private static readonly Dictionary WhiteLen11MakeupCodes = new Dictionary() + { + { 0x8, 1792 }, { 0xC, 1856 }, { 0xD, 1920 } + }; + + private static readonly Dictionary WhiteLen12MakeupCodes = new Dictionary() + { + { 0x12, 1984 }, { 0x13, 2048 }, { 0x14, 2112 }, { 0x15, 2176 }, { 0x16, 2240 }, { 0x17, 2304 }, { 0x1C, 2368 }, + { 0x1D, 2432 }, { 0x1E, 2496 }, { 0x1F, 2560 } + }; + + private static readonly Dictionary BlackLen10MakeupCodes = new Dictionary() + { + { 0xF, 64 } + }; + + private static readonly Dictionary BlackLen11MakeupCodes = new Dictionary() + { + { 0x8, 1792 }, { 0xC, 1856 }, { 0xD, 1920 } + }; + + private static readonly Dictionary BlackLen12MakeupCodes = new Dictionary() + { + { 0xC8, 128 }, { 0xC9, 192 }, { 0x5B, 256 }, { 0x33, 320 }, { 0x34, 384 }, { 0x35, 448 }, + { 0x12, 1984 }, { 0x13, 2048 }, { 0x14, 2112 }, { 0x15, 2176 }, { 0x16, 2240 }, { 0x17, 2304 }, { 0x1C, 2368 }, + { 0x1D, 2432 }, { 0x1E, 2496 }, { 0x1F, 2560 } + }; + + private static readonly Dictionary BlackLen13MakeupCodes = new Dictionary() + { + { 0x6C, 512 }, { 0x6D, 576 }, { 0x4A, 640 }, { 0x4B, 704 }, { 0x4C, 768 }, { 0x4D, 832 }, { 0x72, 896 }, + { 0x73, 960 }, { 0x74, 1024 }, { 0x75, 1088 }, { 0x76, 1152 }, { 0x77, 1216 }, { 0x52, 1280 }, { 0x53, 1344 }, + { 0x54, 1408 }, { 0x55, 1472 }, { 0x5A, 1536 }, { 0x5B, 1600 }, { 0x64, 1664 }, { 0x65, 1728 } + }; + + /// + /// Initializes a new instance of the class. + /// + /// The compressed input stream. + /// The number of bytes to read from the stream. + /// The memory allocator. + /// Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false. + /// Indicates, if its the modified huffman code variation. Defaults to false. + public T4BitReader(Stream input, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false, bool isModifiedHuffman = false) + { + this.Data = allocator.Allocate(bytesToRead); + this.ReadImageDataFromStream(input, bytesToRead); + + this.isModifiedHuffmanRle = isModifiedHuffman; + this.dataLength = bytesToRead; + this.bitsRead = 0; + this.value = 0; + this.curValueBitsRead = 0; + this.position = 0; + this.IsWhiteRun = true; + this.isFirstScanLine = true; + this.isStartOfRow = true; + this.terminationCodeFound = false; + this.RunLength = 0; + this.eolPadding = eolPadding; + + if (this.eolPadding) + { + this.maxCodeLength = 24; + } + } + + /// + /// Gets the compressed image data. + /// + public IMemoryOwner Data { get; } + + /// + /// Gets a value indicating whether there is more data to read left. + /// + public bool HasMoreData + { + get + { + if (this.isModifiedHuffmanRle) + { + return this.position < (ulong)this.dataLength - 1 || (this.bitsRead > 0 && this.bitsRead < 7); + } + + return this.position < (ulong)this.dataLength - 1; + } + } + + /// + /// Gets a value indicating whether the current run is a white pixel run, otherwise its a black pixel run. + /// + public bool IsWhiteRun { get; private set; } + + /// + /// Gets the number of pixels in the current run. + /// + public uint RunLength { get; private set; } + + /// + /// Gets a value indicating whether the end of a pixel row has been reached. + /// + public bool IsEndOfScanLine + { + get + { + if (this.eolPadding) + { + return this.curValueBitsRead >= 12 && this.value == 1; + } + + return this.curValueBitsRead == 12 && this.value == 1; + } + } + + /// + /// Read the next run of pixels. + /// + public void ReadNextRun() + { + if (this.terminationCodeFound) + { + this.IsWhiteRun = !this.IsWhiteRun; + this.terminationCodeFound = false; + } + + this.Reset(); + + if (this.isFirstScanLine && !this.isModifiedHuffmanRle) + { + // We expect an EOL before the first data. + this.value = this.ReadValue(this.eolPadding ? 16 : 12); + + if (!this.IsEndOfScanLine) + { + TiffThrowHelper.ThrowImageFormatException("t4 parsing error: expected start of data marker not found"); + } + + this.Reset(); + } + + // A code word must have at least 2 bits. + this.value = this.ReadValue(MinCodeLength); + + do + { + if (this.curValueBitsRead > this.maxCodeLength) + { + TiffThrowHelper.ThrowImageFormatException("t4 parsing error: invalid code length read"); + } + + bool isMakeupCode = this.IsMakeupCode(); + if (isMakeupCode) + { + if (this.IsWhiteRun) + { + this.RunLength += this.WhiteMakeupCodeRunLength(); + } + else + { + this.RunLength += this.BlackMakeupCodeRunLength(); + } + + this.isStartOfRow = false; + this.Reset(resetRunLength: false); + continue; + } + + bool isTerminatingCode = this.IsTerminatingCode(); + if (isTerminatingCode) + { + // Each line starts with a white run. If the image starts with black, a white run with length zero is written. + if (this.isStartOfRow && this.IsWhiteRun && this.WhiteTerminatingCodeRunLength() == 0) + { + this.IsWhiteRun = !this.IsWhiteRun; + this.Reset(); + this.isStartOfRow = false; + continue; + } + + if (this.IsWhiteRun) + { + this.RunLength += this.WhiteTerminatingCodeRunLength(); + } + else + { + this.RunLength += this.BlackTerminatingCodeRunLength(); + } + + this.terminationCodeFound = true; + this.isStartOfRow = false; + break; + } + + var currBit = this.ReadValue(1); + this.value = (this.value << 1) | currBit; + + if (this.IsEndOfScanLine) + { + this.StartNewRow(); + } + } + while (!this.IsEndOfScanLine); + + this.isFirstScanLine = false; + } + + public void StartNewRow() + { + // Each new row starts with a white run. + this.IsWhiteRun = true; + this.isStartOfRow = true; + this.terminationCodeFound = false; + + if (this.isModifiedHuffmanRle) + { + int pad = 8 - (this.bitsRead % 8); + if (pad != 8) + { + // Skip padding bits, move to next byte. + this.position++; + this.bitsRead = 0; + } + } + } + + /// + public void Dispose() => this.Data.Dispose(); + + private uint WhiteTerminatingCodeRunLength() + { + switch (this.curValueBitsRead) + { + case 4: + { + return WhiteLen4TermCodes[this.value]; + } + + case 5: + { + return WhiteLen5TermCodes[this.value]; + } + + case 6: + { + return WhiteLen6TermCodes[this.value]; + } + + case 7: + { + return WhiteLen7TermCodes[this.value]; + } + + case 8: + { + return WhiteLen8TermCodes[this.value]; + } + } + + return 0; + } + + private uint BlackTerminatingCodeRunLength() + { + switch (this.curValueBitsRead) + { + case 2: + { + return BlackLen2TermCodes[this.value]; + } + + case 3: + { + return BlackLen3TermCodes[this.value]; + } + + case 4: + { + return BlackLen4TermCodes[this.value]; + } + + case 5: + { + return BlackLen5TermCodes[this.value]; + } + + case 6: + { + return BlackLen6TermCodes[this.value]; + } + + case 7: + { + return BlackLen7TermCodes[this.value]; + } + + case 8: + { + return BlackLen8TermCodes[this.value]; + } + + case 9: + { + return BlackLen9TermCodes[this.value]; + } + + case 10: + { + return BlackLen10TermCodes[this.value]; + } + + case 11: + { + return BlackLen11TermCodes[this.value]; + } + + case 12: + { + return BlackLen12TermCodes[this.value]; + } + } + + return 0; + } + + private uint WhiteMakeupCodeRunLength() + { + switch (this.curValueBitsRead) + { + case 5: + { + return WhiteLen5MakeupCodes[this.value]; + } + + case 6: + { + return WhiteLen6MakeupCodes[this.value]; + } + + case 7: + { + return WhiteLen7MakeupCodes[this.value]; + } + + case 8: + { + return WhiteLen8MakeupCodes[this.value]; + } + + case 9: + { + return WhiteLen9MakeupCodes[this.value]; + } + + case 11: + { + return WhiteLen11MakeupCodes[this.value]; + } + + case 12: + { + return WhiteLen12MakeupCodes[this.value]; + } + } + + return 0; + } + + private uint BlackMakeupCodeRunLength() + { + switch (this.curValueBitsRead) + { + case 10: + { + return BlackLen10MakeupCodes[this.value]; + } + + case 11: + { + return BlackLen11MakeupCodes[this.value]; + } + + case 12: + { + return BlackLen12MakeupCodes[this.value]; + } + + case 13: + { + return BlackLen13MakeupCodes[this.value]; + } + } + + return 0; + } + + private bool IsMakeupCode() + { + if (this.IsWhiteRun) + { + return this.IsWhiteMakeupCode(); + } + + return this.IsBlackMakeupCode(); + } + + private bool IsWhiteMakeupCode() + { + switch (this.curValueBitsRead) + { + case 5: + { + return WhiteLen5MakeupCodes.ContainsKey(this.value); + } + + case 6: + { + return WhiteLen6MakeupCodes.ContainsKey(this.value); + } + + case 7: + { + return WhiteLen7MakeupCodes.ContainsKey(this.value); + } + + case 8: + { + return WhiteLen8MakeupCodes.ContainsKey(this.value); + } + + case 9: + { + return WhiteLen9MakeupCodes.ContainsKey(this.value); + } + + case 11: + { + return WhiteLen11MakeupCodes.ContainsKey(this.value); + } + + case 12: + { + if (this.isModifiedHuffmanRle) + { + if (this.value == 1) + { + return true; + } + } + + return WhiteLen12MakeupCodes.ContainsKey(this.value); + } + } + + return false; + } + + private bool IsBlackMakeupCode() + { + switch (this.curValueBitsRead) + { + case 10: + { + return BlackLen10MakeupCodes.ContainsKey(this.value); + } + + case 11: + { + if (this.isModifiedHuffmanRle) + { + if (this.value == 0) + { + return true; + } + } + + return BlackLen11MakeupCodes.ContainsKey(this.value); + } + + case 12: + { + return BlackLen12MakeupCodes.ContainsKey(this.value); + } + + case 13: + { + return BlackLen13MakeupCodes.ContainsKey(this.value); + } + } + + return false; + } + + private bool IsTerminatingCode() + { + if (this.IsWhiteRun) + { + return this.IsWhiteTerminatingCode(); + } + + return this.IsBlackTerminatingCode(); + } + + private bool IsWhiteTerminatingCode() + { + switch (this.curValueBitsRead) + { + case 4: + { + return WhiteLen4TermCodes.ContainsKey(this.value); + } + + case 5: + { + return WhiteLen5TermCodes.ContainsKey(this.value); + } + + case 6: + { + return WhiteLen6TermCodes.ContainsKey(this.value); + } + + case 7: + { + return WhiteLen7TermCodes.ContainsKey(this.value); + } + + case 8: + { + return WhiteLen8TermCodes.ContainsKey(this.value); + } + } + + return false; + } + + private bool IsBlackTerminatingCode() + { + switch (this.curValueBitsRead) + { + case 2: + { + return BlackLen2TermCodes.ContainsKey(this.value); + } + + case 3: + { + return BlackLen3TermCodes.ContainsKey(this.value); + } + + case 4: + { + return BlackLen4TermCodes.ContainsKey(this.value); + } + + case 5: + { + return BlackLen5TermCodes.ContainsKey(this.value); + } + + case 6: + { + return BlackLen6TermCodes.ContainsKey(this.value); + } + + case 7: + { + return BlackLen7TermCodes.ContainsKey(this.value); + } + + case 8: + { + return BlackLen8TermCodes.ContainsKey(this.value); + } + + case 9: + { + return BlackLen9TermCodes.ContainsKey(this.value); + } + + case 10: + { + return BlackLen10TermCodes.ContainsKey(this.value); + } + + case 11: + { + return BlackLen11TermCodes.ContainsKey(this.value); + } + + case 12: + { + return BlackLen12TermCodes.ContainsKey(this.value); + } + } + + return false; + } + + private void Reset(bool resetRunLength = true) + { + this.value = 0; + this.curValueBitsRead = 0; + + if (resetRunLength) + { + this.RunLength = 0; + } + } + + private uint ReadValue(int nBits) + { + Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + + uint v = 0; + int shift = nBits; + while (shift-- > 0) + { + uint bit = this.GetBit(); + v |= bit << shift; + this.curValueBitsRead++; + } + + return v; + } + + private uint GetBit() + { + if (this.bitsRead >= 8) + { + this.LoadNewByte(); + } + + Span dataSpan = this.Data.GetSpan(); + int shift = 8 - this.bitsRead - 1; + var bit = (uint)((dataSpan[(int)this.position] & (1 << shift)) != 0 ? 1 : 0); + this.bitsRead++; + + return bit; + } + + private void LoadNewByte() + { + this.position++; + this.bitsRead = 0; + + if (this.position >= (ulong)this.dataLength) + { + TiffThrowHelper.ThrowImageFormatException("tiff image has invalid t4 compressed data"); + } + } + + private void ReadImageDataFromStream(Stream input, int bytesToRead) + { + Span dataSpan = this.Data.GetSpan(); + input.Read(dataSpan, 0, bytesToRead); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs new file mode 100644 index 0000000000..76f0883643 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is compressed using CCITT T4 compression. + /// + internal class T4TiffCompression : TiffBaseDecompressor + { + private readonly FaxCompressionOptions faxCompressionOptions; + + private readonly byte whiteValue; + + private readonly byte blackValue; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The image width. + /// The number of bits per pixel. + /// Fax compression options. + /// The photometric interpretation. + public T4TiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation) + : base(allocator, width, bitsPerPixel) + { + this.faxCompressionOptions = faxOptions; + + bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero; + this.whiteValue = (byte)(isWhiteZero ? 0 : 1); + this.blackValue = (byte)(isWhiteZero ? 1 : 0); + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, Span buffer) + { + if (this.faxCompressionOptions.HasFlag(FaxCompressionOptions.TwoDimensionalCoding)) + { + TiffThrowHelper.ThrowNotSupported("TIFF CCITT 2D compression is not yet supported"); + } + + var eolPadding = this.faxCompressionOptions.HasFlag(FaxCompressionOptions.EolPadding); + using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding); + + buffer.Clear(); + uint bitsWritten = 0; + while (bitReader.HasMoreData) + { + bitReader.ReadNextRun(); + + if (bitReader.RunLength > 0) + { + if (bitReader.IsWhiteRun) + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue); + bitsWritten += bitReader.RunLength; + } + else + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue); + bitsWritten += bitReader.RunLength; + } + } + + if (bitReader.IsEndOfScanLine) + { + // Write padding bytes, if necessary. + uint pad = 8 - (bitsWritten % 8); + if (pad != 8) + { + BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0); + bitsWritten += pad; + } + } + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffLzwDecoder.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffLzwDecoder.cs new file mode 100644 index 0000000000..68d3a7f2a0 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffLzwDecoder.cs @@ -0,0 +1,257 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /* + This implementation is based on a port of a java tiff decoder by Harald Kuhr: https://github.com/haraldk/TwelveMonkeys + + Original licence: + + BSD 3-Clause License + + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + ** Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + /// + /// Decompresses and decodes data using the dynamic LZW algorithms, see TIFF spec Section 13. + /// + internal sealed class TiffLzwDecoder + { + /// + /// The stream to decode. + /// + private readonly Stream stream; + + /// + /// As soon as we use entry 4094 of the table (maxTableSize - 2), the lzw compressor write out a (12-bit) ClearCode. + /// At this point, the compressor reinitializes the string table and then writes out 9-bit codes again. + /// + private const int ClearCode = 256; + + /// + /// End of Information. + /// + private const int EoiCode = 257; + + /// + /// Minimum code length of 9 bits. + /// + private const int MinBits = 9; + + /// + /// Maximum code length of 12 bits. + /// + private const int MaxBits = 12; + + /// + /// Maximum table size of 4096. + /// + private const int TableSize = 1 << MaxBits; + + private readonly LzwString[] table; + + private int tableLength; + private int bitsPerCode; + private int oldCode = ClearCode; + private int maxCode; + private int bitMask; + private int maxString; + private bool eofReached; + private int nextData; + private int nextBits; + + /// + /// Initializes a new instance of the class + /// and sets the stream, where the compressed data should be read from. + /// + /// The stream to read from. + /// is null. + public TiffLzwDecoder(Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + this.stream = stream; + + // TODO: Investigate a manner by which we can avoid this allocation. + this.table = new LzwString[TableSize]; + for (int i = 0; i < 256; i++) + { + this.table[i] = new LzwString((byte)i); + } + + this.Init(); + } + + private void Init() + { + // Table length is 256 + 2, because of special clear code and end of information code. + this.tableLength = 258; + this.bitsPerCode = MinBits; + this.bitMask = BitmaskFor(this.bitsPerCode); + this.maxCode = this.MaxCode(); + this.maxString = 1; + } + + /// + /// Decodes and decompresses all pixel indices from the stream. + /// + /// The pixel array to decode to. + public void DecodePixels(Span pixels) + { + // Adapted from the pseudo-code example found in the TIFF 6.0 Specification, 1992. + // See Section 13: "LZW Compression"/"LZW Decoding", page 61+ + int code; + int offset = 0; + + while ((code = this.GetNextCode()) != EoiCode) + { + if (code == ClearCode) + { + this.Init(); + code = this.GetNextCode(); + + if (code == EoiCode) + { + break; + } + + if (this.table[code] == null) + { + TiffThrowHelper.ThrowImageFormatException($"Corrupted TIFF LZW: code {code} (table size: {this.tableLength})"); + } + + offset += this.table[code].WriteTo(pixels, offset); + } + else + { + if (this.table[this.oldCode] == null) + { + TiffThrowHelper.ThrowImageFormatException($"Corrupted TIFF LZW: code {this.oldCode} (table size: {this.tableLength})"); + } + + if (this.IsInTable(code)) + { + offset += this.table[code].WriteTo(pixels, offset); + + this.AddStringToTable(this.table[this.oldCode].Concatenate(this.table[code].FirstChar)); + } + else + { + LzwString outString = this.table[this.oldCode].Concatenate(this.table[this.oldCode].FirstChar); + + offset += outString.WriteTo(pixels, offset); + this.AddStringToTable(outString); + } + } + + this.oldCode = code; + + if (offset >= pixels.Length) + { + break; + } + } + } + + private void AddStringToTable(LzwString lzwString) + { + if (this.tableLength > this.table.Length) + { + TiffThrowHelper.ThrowImageFormatException($"TIFF LZW with more than {MaxBits} bits per code encountered (table overflow)"); + } + + this.table[this.tableLength++] = lzwString; + + if (this.tableLength > this.maxCode) + { + this.bitsPerCode++; + + if (this.bitsPerCode > MaxBits) + { + // Continue reading MaxBits (12 bit) length codes. + this.bitsPerCode = MaxBits; + } + + this.bitMask = BitmaskFor(this.bitsPerCode); + this.maxCode = this.MaxCode(); + } + + if (lzwString.Length > this.maxString) + { + this.maxString = lzwString.Length; + } + } + + private int GetNextCode() + { + if (this.eofReached) + { + return EoiCode; + } + + int read = this.stream.ReadByte(); + if (read < 0) + { + this.eofReached = true; + return EoiCode; + } + + this.nextData = (this.nextData << 8) | read; + this.nextBits += 8; + + if (this.nextBits < this.bitsPerCode) + { + read = this.stream.ReadByte(); + if (read < 0) + { + this.eofReached = true; + return EoiCode; + } + + this.nextData = (this.nextData << 8) | read; + this.nextBits += 8; + } + + int code = (this.nextData >> (this.nextBits - this.bitsPerCode)) & this.bitMask; + this.nextBits -= this.bitsPerCode; + + return code; + } + + private bool IsInTable(int code) => code < this.tableLength; + + private int MaxCode() => this.bitMask - 1; + + private static int BitmaskFor(int bits) => (1 << bits) - 1; + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/FaxCompressionOptions.cs b/src/ImageSharp/Formats/Tiff/Compression/FaxCompressionOptions.cs new file mode 100644 index 0000000000..19103de925 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/FaxCompressionOptions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + /// + /// Fax compression options, see TIFF spec page 51f (T4Options). + /// + [Flags] + public enum FaxCompressionOptions : uint + { + /// + /// No options. + /// + None = 0, + + /// + /// If set, 2-dimensional coding is used (otherwise 1-dimensional is assumed). + /// + TwoDimensionalCoding = 1, + + /// + /// If set, uncompressed mode is used. + /// + UncompressedMode = 2, + + /// + /// If set, fill bits have been added as necessary before EOL codes such that + /// EOL always ends on a byte boundary, thus ensuring an EOL-sequence of 1 byte + /// preceded by a zero nibble: xxxx-0000 0000-0001. + /// + EolPadding = 4 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs new file mode 100644 index 0000000000..ae2f17dbb5 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + /// + /// Methods for undoing the horizontal prediction used in combination with deflate and LZW compressed TIFF images. + /// + internal static class HorizontalPredictor + { + /// + /// Inverts the horizontal prediction. + /// + /// Buffer with decompressed pixel data. + /// The width of the image or strip. + /// Bits per pixel. + public static void Undo(Span pixelBytes, int width, int bitsPerPixel) + { + if (bitsPerPixel == 8) + { + Undo8Bit(pixelBytes, width); + } + else if (bitsPerPixel == 24) + { + Undo24Bit(pixelBytes, width); + } + } + + public static void ApplyHorizontalPrediction(Span rows, int width, int bitsPerPixel) + { + if (bitsPerPixel == 8) + { + ApplyHorizontalPrediction8Bit(rows, width); + } + else if (bitsPerPixel == 24) + { + ApplyHorizontalPrediction24Bit(rows, width); + } + } + + /// + /// Applies a horizontal predictor to the rgb row. + /// Make use of the fact that many continuous-tone images rarely vary much in pixel value from one pixel to the next. + /// In such images, if we replace the pixel values by differences between consecutive pixels, many of the differences should be 0, plus + /// or minus 1, and so on.This reduces the apparent information content and allows LZW to encode the data more compactly. + /// + /// The rgb pixel rows. + /// The width. + [MethodImpl(InliningOptions.ShortMethod)] + private static void ApplyHorizontalPrediction24Bit(Span rows, int width) + { + DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equals"); + int height = rows.Length / width; + for (int y = 0; y < height; y++) + { + Span rowSpan = rows.Slice(y * width, width); + Span rowRgb = MemoryMarshal.Cast(rowSpan); + + for (int x = rowRgb.Length - 1; x >= 1; x--) + { + byte r = (byte)(rowRgb[x].R - rowRgb[x - 1].R); + byte g = (byte)(rowRgb[x].G - rowRgb[x - 1].G); + byte b = (byte)(rowRgb[x].B - rowRgb[x - 1].B); + var rgb = new Rgb24(r, g, b); + rowRgb[x].FromRgb24(rgb); + } + } + } + + /// + /// Applies a horizontal predictor to a gray pixel row. + /// + /// The gray pixel rows. + /// The width. + [MethodImpl(InliningOptions.ShortMethod)] + private static void ApplyHorizontalPrediction8Bit(Span rows, int width) + { + DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equals"); + int height = rows.Length / width; + for (int y = 0; y < height; y++) + { + Span rowSpan = rows.Slice(y * width, width); + for (int x = rowSpan.Length - 1; x >= 1; x--) + { + rowSpan[x] -= rowSpan[x - 1]; + } + } + } + + private static void Undo8Bit(Span pixelBytes, int width) + { + int rowBytesCount = width; + int height = pixelBytes.Length / rowBytesCount; + for (int y = 0; y < height; y++) + { + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + + byte pixelValue = rowBytes[0]; + for (int x = 1; x < width; x++) + { + pixelValue += rowBytes[x]; + rowBytes[x] = pixelValue; + } + } + } + + private static void Undo24Bit(Span pixelBytes, int width) + { + int rowBytesCount = width * 3; + int height = pixelBytes.Length / rowBytesCount; + for (int y = 0; y < height; y++) + { + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + Span rowRgb = MemoryMarshal.Cast(rowBytes).Slice(0, width); + ref Rgb24 rowRgbBase = ref MemoryMarshal.GetReference(rowRgb); + byte r = rowRgbBase.R; + byte g = rowRgbBase.G; + byte b = rowRgbBase.B; + + for (int x = 1; x < rowRgb.Length; x++) + { + ref Rgb24 pixel = ref rowRgb[x]; + r += pixel.R; + g += pixel.G; + b += pixel.B; + var rgb = new Rgb24(r, g, b); + pixel.FromRgb24(rgb); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs new file mode 100644 index 0000000000..5bd4cd1f13 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + internal abstract class TiffBaseCompression : IDisposable + { + private bool isDisposed; + + protected TiffBaseCompression(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + { + this.Allocator = allocator; + this.Width = width; + this.BitsPerPixel = bitsPerPixel; + this.Predictor = predictor; + this.BytesPerRow = ((width * bitsPerPixel) + 7) / 8; + } + + /// + /// Gets the image width. + /// + public int Width { get; } + + /// + /// Gets the bits per pixel. + /// + public int BitsPerPixel { get; } + + /// + /// Gets the bytes per row. + /// + public int BytesPerRow { get; } + + /// + /// Gets the predictor to use. Should only be used with deflate or lzw compression. + /// + public TiffPredictor Predictor { get; } + + /// + /// Gets the memory allocator. + /// + protected MemoryAllocator Allocator { get; } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + this.Dispose(true); + } + + protected abstract void Dispose(bool disposing); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompressor.cs new file mode 100644 index 0000000000..c5c5c466dc --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompressor.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + internal abstract class TiffBaseCompressor : TiffBaseCompression + { + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed image to. + /// The memory allocator. + /// The image width. + /// Bits per pixel. + /// The predictor to use (should only be used with deflate or lzw compression). Defaults to none. + protected TiffBaseCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(allocator, width, bitsPerPixel, predictor) + => this.Output = output; + + /// + /// Gets the compression method to use. + /// + public abstract TiffCompression Method { get; } + + /// + /// Gets the output stream to write the compressed image to. + /// + public Stream Output { get; } + + /// + /// Does any initialization required for the compression. + /// + /// The number of rows per strip. + public abstract void Initialize(int rowsPerStrip); + + /// + /// Compresses a strip of the image. + /// + /// Image rows to compress. + /// Image height. + public abstract void CompressStrip(Span rows, int height); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs new file mode 100644 index 0000000000..a289e306a0 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + /// + /// The base tiff decompressor class. + /// + internal abstract class TiffBaseDecompressor : TiffBaseCompression + { + protected TiffBaseDecompressor(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(allocator, width, bitsPerPixel, predictor) + { + } + + /// + /// Decompresses image data into the supplied buffer. + /// + /// The to read image data from. + /// The strip offset of stream. + /// The number of bytes to read from the input stream. + /// The output buffer for uncompressed data. + public void Decompress(BufferedReadStream stream, uint stripOffset, uint stripByteCount, Span buffer) + { + if (stripByteCount > int.MaxValue) + { + TiffThrowHelper.ThrowImageFormatException("The StripByteCount value is too big."); + } + + stream.Seek(stripOffset, SeekOrigin.Begin); + this.Decompress(stream, (int)stripByteCount, buffer); + + if (stripOffset + stripByteCount < stream.Position) + { + TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip."); + } + } + + /// + /// Decompresses image data into the supplied buffer. + /// + /// The to read image data from. + /// The number of bytes to read from the input stream. + /// The output buffer for uncompressed data. + protected abstract void Decompress(BufferedReadStream stream, int byteCount, Span buffer); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs new file mode 100644 index 0000000000..14a0c6e9d7 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + internal static class TiffCompressorFactory + { + public static TiffBaseCompressor Create( + TiffCompression method, + Stream output, + MemoryAllocator allocator, + int width, + int bitsPerPixel, + DeflateCompressionLevel compressionLevel, + TiffPredictor predictor) + { + switch (method) + { + // The following compression types are not implemented in the encoder and will default to no compression instead. + case TiffCompression.ItuTRecT43: + case TiffCompression.ItuTRecT82: + case TiffCompression.Jpeg: + case TiffCompression.OldJpeg: + case TiffCompression.OldDeflate: + case TiffCompression.None: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + + return new NoCompressor(output, allocator, width, bitsPerPixel); + + case TiffCompression.PackBits: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new PackBitsCompressor(output, allocator, width, bitsPerPixel); + + case TiffCompression.Deflate: + return new DeflateCompressor(output, allocator, width, bitsPerPixel, predictor, compressionLevel); + + case TiffCompression.Lzw: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + return new LzwCompressor(output, allocator, width, bitsPerPixel, predictor); + + case TiffCompression.CcittGroup3Fax: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T4BitCompressor(output, allocator, width, bitsPerPixel, false); + + case TiffCompression.Ccitt1D: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T4BitCompressor(output, allocator, width, bitsPerPixel, true); + + default: + throw TiffThrowHelper.NotSupportedCompressor(method.ToString()); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs new file mode 100644 index 0000000000..80bc0af5ab --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + /// + /// Provides enumeration of the various TIFF compression types the decoder can handle. + /// + internal enum TiffDecoderCompressionType + { + /// + /// Image data is stored uncompressed in the TIFF file. + /// + None = 0, + + /// + /// Image data is compressed using PackBits compression. + /// + PackBits = 1, + + /// + /// Image data is compressed using Deflate compression. + /// + Deflate = 2, + + /// + /// Image data is compressed using LZW compression. + /// + Lzw = 3, + + /// + /// Image data is compressed using T4-encoding: CCITT T.4. + /// + T4 = 4, + + /// + /// Image data is compressed using modified huffman compression. + /// + HuffmanRle = 5, + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs new file mode 100644 index 0000000000..a6d44f4d3c --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression +{ + internal static class TiffDecompressorsFactory + { + public static TiffBaseDecompressor Create( + TiffDecoderCompressionType method, + MemoryAllocator allocator, + TiffPhotometricInterpretation photometricInterpretation, + int width, + int bitsPerPixel, + TiffPredictor predictor, + FaxCompressionOptions faxOptions) + { + switch (method) + { + case TiffDecoderCompressionType.None: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); + return new NoneTiffCompression(allocator, width, bitsPerPixel); + + case TiffDecoderCompressionType.PackBits: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); + return new PackBitsTiffCompression(allocator, width, bitsPerPixel); + + case TiffDecoderCompressionType.Deflate: + DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); + return new DeflateTiffCompression(allocator, width, bitsPerPixel, predictor); + + case TiffDecoderCompressionType.Lzw: + DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); + return new LzwTiffCompression(allocator, width, bitsPerPixel, predictor); + + case TiffDecoderCompressionType.T4: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T4TiffCompression(allocator, width, bitsPerPixel, faxOptions, photometricInterpretation); + + case TiffDecoderCompressionType.HuffmanRle: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new ModifiedHuffmanTiffCompression(allocator, width, bitsPerPixel, photometricInterpretation); + + default: + throw TiffThrowHelper.NotSupportedDecompressor(nameof(method)); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/ConfigurationExtensions.cs b/src/ImageSharp/Formats/Tiff/ConfigurationExtensions.cs new file mode 100644 index 0000000000..49f87e0905 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/ConfigurationExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Helper methods for the Configuration. + /// + public static class ConfigurationExtensions + { + /// + /// Registers the tiff format detector, encoder and decoder. + /// + /// The configuration. + public static void AddTiff(this Configuration configuration) + { + configuration.ImageFormatsManager.AddImageFormat(TiffFormat.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new TiffImageFormatDetector()); + configuration.ImageFormatsManager.SetDecoder(TiffFormat.Instance, new TiffDecoder()); + configuration.ImageFormatsManager.SetEncoder(TiffFormat.Instance, new TiffEncoder()); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs new file mode 100644 index 0000000000..031494fc5d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Enumeration representing the compression formats defined by the Tiff file-format. + /// + public enum TiffCompression : ushort + { + /// + /// A invalid compression value. + /// + Invalid = 0, + + /// + /// No compression. + /// + None = 1, + + /// + /// CCITT Group 3 1-Dimensional Modified Huffman run-length encoding. + /// + Ccitt1D = 2, + + /// + /// PackBits compression + /// + PackBits = 32773, + + /// + /// T4-encoding: CCITT T.4 bi-level encoding (see Section 11 of the TIFF 6.0 specification). + /// + CcittGroup3Fax = 3, + + /// + /// T6-encoding: CCITT T.6 bi-level encoding (see Section 11 of the TIFF 6.0 specification). + /// + /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, + /// if this is choosen. + /// + CcittGroup4Fax = 4, + + /// + /// LZW compression (see Section 13 of the TIFF 6.0 specification). + /// + Lzw = 5, + + /// + /// JPEG compression - obsolete (see Section 22 of the TIFF 6.0 specification). + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + OldJpeg = 6, + + /// + /// JPEG compression (see TIFF Specification, supplement 2). + /// + /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, + /// if this is chosen. + /// + Jpeg = 7, + + /// + /// Deflate compression, using zlib data format (see TIFF Specification, supplement 2). + /// + Deflate = 8, + + /// + /// Deflate compression - old. + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + OldDeflate = 32946, + + /// + /// ITU-T Rec. T.82 coding, applying ITU-T Rec. T.85 (JBIG) (see RFC2301). + /// + /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, + /// if this is chosen. + /// + ItuTRecT82 = 9, + + /// + /// ITU-T Rec. T.43 representation, using ITU-T Rec. T.82 (JBIG) (see RFC2301). + /// + /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, + /// if this is chosen. + /// + ItuTRecT43 = 10 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs new file mode 100644 index 0000000000..a30890a69e --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Defines constants defined in the TIFF specification. + /// + internal static class TiffConstants + { + /// + /// Byte order markers for indicating little endian encoding. + /// + public const byte ByteOrderLittleEndian = 0x49; + + /// + /// Byte order markers for indicating big endian encoding. + /// + public const byte ByteOrderBigEndian = 0x4D; + + /// + /// Byte order markers for indicating little endian encoding. + /// + public const ushort ByteOrderLittleEndianShort = 0x4949; + + /// + /// Byte order markers for indicating big endian encoding. + /// + public const ushort ByteOrderBigEndianShort = 0x4D4D; + + /// + /// Magic number used within the image file header to identify a TIFF format file. + /// + public const ushort HeaderMagicNumber = 42; + + /// + /// RowsPerStrip default value, which is effectively infinity. + /// + public const int RowsPerStripInfinity = 2147483647; + + /// + /// Size (in bytes) of the TIFF file header. + /// + public const int SizeOfTiffHeader = 8; + + /// + /// Size (in bytes) of each individual TIFF IFD entry + /// + public const int SizeOfIfdEntry = 12; + + /// + /// Size (in bytes) of the Short and SShort data types + /// + public const int SizeOfShort = 2; + + /// + /// Size (in bytes) of the Long and SLong data types + /// + public const int SizeOfLong = 4; + + /// + /// Size (in bytes) of the Rational and SRational data types + /// + public const int SizeOfRational = 8; + + /// + /// Size (in bytes) of the Float data type + /// + public const int SizeOfFloat = 4; + + /// + /// Size (in bytes) of the Double data type + /// + public const int SizeOfDouble = 8; + + /// + /// The default strip size is 8k. + /// + public const int DefaultStripSize = 8 * 1024; + + /// + /// The bits per sample for 1 bit bicolor images. + /// + public static readonly ushort[] BitsPerSample1Bit = { 1 }; + + /// + /// The bits per sample for images with a 4 color palette. + /// + public static readonly ushort[] BitsPerSample4Bit = { 4 }; + + /// + /// The bits per sample for 8 bit images. + /// + public static readonly ushort[] BitsPerSample8Bit = { 8 }; + + /// + /// The bits per sample for images with 8 bits for each color channel. + /// + public static readonly ushort[] BitsPerSampleRgb8Bit = { 8, 8, 8 }; + + /// + /// The list of mimetypes that equate to a tiff. + /// + public static readonly IEnumerable MimeTypes = new[] { "image/tiff", "image/tiff-fx" }; + + /// + /// The list of file extensions that equate to a tiff. + /// + public static readonly IEnumerable FileExtensions = new[] { "tiff", "tif" }; + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffExtraSamples.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffExtraSamples.cs new file mode 100644 index 0000000000..c10167d250 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffExtraSamples.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Enumeration representing the possible uses of extra components in TIFF format files. + /// + internal enum TiffExtraSamples + { + /// + /// Unspecified data. + /// + Unspecified = 0, + + /// + /// Associated alpha data (with pre-multiplied color). + /// + AssociatedAlpha = 1, + + /// + /// Unassociated alpha data. + /// + UnassociatedAlpha = 2 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs new file mode 100644 index 0000000000..1bb75f8366 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Enumeration representing the fill orders defined by the Tiff file-format. + /// + internal enum TiffFillOrder : ushort + { + /// + /// Pixels with lower column values are stored in the higher-order bits of the byte. + /// + MostSignificantBitFirst = 1, + + /// + /// Pixels with lower column values are stored in the lower-order bits of the byte. + /// + LeastSignificantBitFirst = 2 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs new file mode 100644 index 0000000000..4ed6aafbb2 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Enumeration representing the sub-file types defined by the Tiff file-format. + /// + [Flags] + public enum TiffNewSubfileType : uint + { + /// + /// A full-resolution image. + /// + FullImage = 0, + + /// + /// Reduced-resolution version of another image in this TIFF file. + /// + Preview = 1, + + /// + /// A single page of a multi-page image. + /// + SinglePage = 2, + + /// + /// A transparency mask for another image in this TIFF file. + /// + TransparencyMask = 4, + + /// + /// Alternative reduced-resolution version of another image in this TIFF file (see DNG specification). + /// + AlternativePreview = 65536, + + /// + /// Mixed raster content (see RFC2301). + /// + MixedRasterContent = 8 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffOrientation.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffOrientation.cs new file mode 100644 index 0000000000..a5305d4824 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffOrientation.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Enumeration representing the image orientations defined by the Tiff file-format. + /// + internal enum TiffOrientation + { + /// + /// The 0th row and 0th column represent the visual top and left-hand side of the image respectively. + /// + TopLeft = 1, + + /// + /// The 0th row and 0th column represent the visual top and right-hand side of the image respectively. + /// + TopRight = 2, + + /// + /// The 0th row and 0th column represent the visual bottom and right-hand side of the image respectively. + /// + BottomRight = 3, + + /// + /// The 0th row and 0th column represent the visual bottom and left-hand side of the image respectively. + /// + BottomLeft = 4, + + /// + /// The 0th row and 0th column represent the visual left-hand side and top of the image respectively. + /// + LeftTop = 5, + + /// + /// The 0th row and 0th column represent the visual right-hand side and top of the image respectively. + /// + RightTop = 6, + + /// + /// The 0th row and 0th column represent the visual right-hand side and bottom of the image respectively. + /// + RightBottom = 7, + + /// + /// The 0th row and 0th column represent the visual left-hand side and bottom of the image respectively. + /// + LeftBottom = 8 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs new file mode 100644 index 0000000000..6dab7de6ef --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Enumeration representing the photometric interpretation formats defined by the Tiff file-format. + /// + public enum TiffPhotometricInterpretation : ushort + { + /// + /// Bilevel and grayscale: 0 is imaged as white. The maximum value is imaged as black. + /// + /// Not supported by the TiffEncoder. + /// + WhiteIsZero = 0, + + /// + /// Bilevel and grayscale: 0 is imaged as black. The maximum value is imaged as white. + /// + BlackIsZero = 1, + + /// + /// RGB image. + /// + Rgb = 2, + + /// + /// Palette Color. + /// + PaletteColor = 3, + + /// + /// A transparency mask. + /// + /// Not supported by the TiffEncoder. + /// + TransparencyMask = 4, + + /// + /// Separated: usually CMYK (see Section 16 of the TIFF 6.0 specification). + /// + /// Not supported by the TiffEncoder. + /// + Separated = 5, + + /// + /// YCbCr (see Section 21 of the TIFF 6.0 specification). + /// + /// Not supported by the TiffEncoder. + /// + YCbCr = 6, + + /// + /// 1976 CIE L*a*b* (see Section 23 of the TIFF 6.0 specification). + /// + /// Not supported by the TiffEncoder. + /// + CieLab = 8, + + /// + /// ICC L*a*b* (see TIFF Specification, supplement 1). + /// + /// Not supported by the TiffEncoder. + /// + IccLab = 9, + + /// + /// ITU L*a*b* (see RFC2301). + /// + /// Not supported by the TiffEncoder. + /// + ItuLab = 10, + + /// + /// Color Filter Array (see the DNG specification). + /// + /// Not supported by the TiffEncoder. + /// + ColorFilterArray = 32803, + + /// + /// Linear Raw (see the DNG specification). + /// + /// Not supported by the TiffEncoder. + /// + LinearRaw = 34892 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs new file mode 100644 index 0000000000..ea526ede56 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Enumeration representing how the components of each pixel are stored the Tiff file-format. + /// + public enum TiffPlanarConfiguration : ushort + { + /// + /// Chunky format. + /// The component values for each pixel are stored contiguously. + /// The order of the components within the pixel is specified by + /// PhotometricInterpretation. For example, for RGB data, the data is stored as RGBRGBRGB. + /// + Chunky = 1, + + /// + /// Planar format. + /// The components are stored in separate “component planes.” The + /// values in StripOffsets and StripByteCounts are then arranged as a 2-dimensional + /// array, with SamplesPerPixel rows and StripsPerImage columns. (All of the columns + /// for row 0 are stored first, followed by the columns of row 1, and so on.) + /// PhotometricInterpretation describes the type of data stored in each component + /// plane. For example, RGB data is stored with the Red components in one component + /// plane, the Green in another, and the Blue in another. + /// + Planar = 2 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffPredictor.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffPredictor.cs new file mode 100644 index 0000000000..6bde23cac6 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffPredictor.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// A mathematical operator that is applied to the image data before an encoding scheme is applied. + /// + public enum TiffPredictor : ushort + { + /// + /// No prediction. + /// + None = 1, + + /// + /// Horizontal differencing. + /// + Horizontal = 2, + + /// + /// Floating point horizontal differencing. + /// + /// Note: The Tiff Encoder does not yet support this. If this is chosen, the encoder will fallback to none. + /// + FloatingPoint = 3 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffSampleFormat.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffSampleFormat.cs new file mode 100644 index 0000000000..81899c5fd8 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffSampleFormat.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Specifies how to interpret each data sample in a pixel. + /// + public enum TiffSampleFormat : ushort + { + /// + /// Unsigned integer data. Default value. + /// + UnsignedInteger = 1, + + /// + /// Signed integer data. + /// + SignedInteger = 2, + + /// + /// IEEE floating point data. + /// + Float = 3, + + /// + /// Undefined data format. + /// + Undefined = 4, + + /// + /// The complex int. + /// + ComplexInt = 5, + + /// + /// The complex float. + /// + ComplexFloat = 6 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs new file mode 100644 index 0000000000..ff735de86f --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Enumeration representing the sub-file types defined by the Tiff file-format. + /// + public enum TiffSubfileType : ushort + { + /// + /// Full-resolution image data. + /// + FullImage = 1, + + /// + /// Reduced-resolution image data. + /// + Preview = 2, + + /// + /// A single page of a multi-page image. + /// + SinglePage = 3 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffThresholding.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffThresholding.cs new file mode 100644 index 0000000000..fce0b175c8 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffThresholding.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.Constants +{ + /// + /// Enumeration representing the thresholding applied to image data defined by the Tiff file-format. + /// + internal enum TiffThresholding + { + /// + /// No dithering or halftoning. + /// + None = 1, + + /// + /// An ordered dither or halftone technique. + /// + Ordered = 2, + + /// + /// A randomized process such as error diffusion. + /// + Random = 3 + } +} diff --git a/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs new file mode 100644 index 0000000000..5372384397 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Encapsulates the options for the . + /// + internal interface ITiffDecoderOptions + { + /// + /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. + /// + bool IgnoreMetadata { get; } + } +} diff --git a/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs new file mode 100644 index 0000000000..d56a587df9 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Encapsulates the options for the . + /// + internal interface ITiffEncoderOptions + { + /// + /// Gets the number of bits per pixel. + /// + TiffBitsPerPixel? BitsPerPixel { get; } + + /// + /// Gets the compression type to use. + /// + TiffCompression? Compression { get; } + + /// + /// Gets the compression level 1-9 for the deflate compression mode. + /// Defaults to . + /// + DeflateCompressionLevel? CompressionLevel { get; } + + /// + /// Gets the PhotometricInterpretation to use. Possible options are RGB, RGB with a color palette, gray or BiColor. + /// If no PhotometricInterpretation is specified or it is unsupported by the encoder, RGB will be used. + /// + TiffPhotometricInterpretation? PhotometricInterpretation { get; } + + /// + /// Gets a value indicating which horizontal prediction to use. This can improve the compression ratio with deflate or lzw compression. + /// + TiffPredictor? HorizontalPredictor { get; } + + /// + /// Gets the quantizer for creating a color palette image. + /// + IQuantizer Quantizer { get; } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs new file mode 100644 index 0000000000..8b2c6bd3a0 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The TIFF IFD reader class. + /// + internal class DirectoryReader + { + private readonly Stream stream; + + private uint nextIfdOffset; + + // used for sequential read big values (actual for multiframe big files) + // todo: different tags can link to the same data (stream offset) - investigate + private readonly SortedList lazyLoaders = new SortedList(new DuplicateKeyComparer()); + + public DirectoryReader(Stream stream) => this.stream = stream; + + /// + /// Gets the byte order. + /// + public ByteOrder ByteOrder { get; private set; } + + /// + /// Reads image file directories. + /// + /// Image file directories. + public IEnumerable Read() + { + this.ByteOrder = ReadByteOrder(this.stream); + this.nextIfdOffset = new HeaderReader(this.stream, this.ByteOrder).ReadFileHeader(); + return this.ReadIfds(); + } + + private static ByteOrder ReadByteOrder(Stream stream) + { + var headerBytes = new byte[2]; + stream.Read(headerBytes, 0, 2); + if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) + { + return ByteOrder.LittleEndian; + } + else if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian) + { + return ByteOrder.BigEndian; + } + + throw TiffThrowHelper.ThrowInvalidHeader(); + } + + private IEnumerable ReadIfds() + { + var readers = new List(); + while (this.nextIfdOffset != 0 && this.nextIfdOffset < this.stream.Length) + { + var reader = new EntryReader(this.stream, this.ByteOrder, this.nextIfdOffset, this.lazyLoaders); + reader.ReadTags(); + + this.nextIfdOffset = reader.NextIfdOffset; + + readers.Add(reader); + } + + // Sequential reading big values. + foreach (Action loader in this.lazyLoaders.Values) + { + loader(); + } + + var list = new List(); + foreach (EntryReader reader in readers) + { + var profile = new ExifProfile(reader.Values, reader.InvalidTags); + list.Add(profile); + } + + return list; + } + + /// + /// used for possibility add a duplicate offsets (but tags don't duplicate). + /// + /// The type of the key. + private class DuplicateKeyComparer : IComparer + where TKey : IComparable + { + public int Compare(TKey x, TKey y) + { + int result = x.CompareTo(y); + + // Handle equality as being greater. + return (result == 0) ? 1 : result; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs new file mode 100644 index 0000000000..123a64cc1d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.IO; + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + internal class EntryReader : BaseExifReader + { + private readonly uint startOffset; + + private readonly SortedList lazyLoaders; + + public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList lazyLoaders) + : base(stream) + { + this.IsBigEndian = byteOrder == ByteOrder.BigEndian; + this.startOffset = ifdOffset; + this.lazyLoaders = lazyLoaders; + } + + public List Values { get; } = new List(); + + public uint NextIfdOffset { get; private set; } + + public void ReadTags() + { + this.ReadValues(this.Values, this.startOffset); + this.NextIfdOffset = this.ReadUInt32(); + + this.ReadSubIfd(this.Values); + } + + protected override void RegisterExtLoader(uint offset, Action reader) => + this.lazyLoaders.Add(offset, reader); + } + + internal class HeaderReader : BaseExifReader + { + public HeaderReader(Stream stream, ByteOrder byteOrder) + : base(stream) => + this.IsBigEndian = byteOrder == ByteOrder.BigEndian; + + public uint FirstIfdOffset { get; private set; } + + public uint ReadFileHeader() + { + ushort magic = this.ReadUInt16(); + if (magic != TiffConstants.HeaderMagicNumber) + { + TiffThrowHelper.ThrowInvalidHeader(); + } + + this.FirstIfdOffset = this.ReadUInt32(); + return this.FirstIfdOffset; + } + + protected override void RegisterExtLoader(uint offset, Action reader) => throw new NotSupportedException(); + } +} diff --git a/src/ImageSharp/Formats/Tiff/MetadataExtensions.cs b/src/ImageSharp/Formats/Tiff/MetadataExtensions.cs new file mode 100644 index 0000000000..b9da86fc44 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/MetadataExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Metadata; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class MetadataExtensions + { + /// + /// Gets the tiff format specific metadata for the image. + /// + /// The metadata this method extends. + /// The . + public static TiffMetadata GetTiffMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance); + + /// + /// Gets the tiff format specific metadata for the image frame. + /// + /// The metadata this method extends. + /// The . + public static TiffFrameMetadata GetTiffMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance); + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor{TPixel}.cs new file mode 100644 index 0000000000..6724adec03 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor{TPixel}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation (optimized for bilevel images). + /// + /// The pixel format. + internal class BlackIsZero1TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + int offset = 0; + + Color black = Color.Black; + Color white = Color.White; + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x += 8) + { + byte b = data[offset++]; + int maxShift = Math.Min(left + width - x, 8); + + for (int shift = 0; shift < maxShift; shift++) + { + int bit = (b >> (7 - shift)) & 1; + + color.FromRgba32(bit == 0 ? black : white); + + pixels[x + shift, y] = color; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor{TPixel}.cs new file mode 100644 index 0000000000..cf59c1222d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor{TPixel}.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation (optimized for 4-bit grayscale images). + /// + internal class BlackIsZero4TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + int offset = 0; + bool isOddWidth = (width & 1) == 1; + + var l8 = default(L8); + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width - 1;) + { + byte byteData = data[offset++]; + + byte intensity1 = (byte)(((byteData & 0xF0) >> 4) * 17); + l8.PackedValue = intensity1; + color.FromL8(l8); + + pixels[x++, y] = color; + + byte intensity2 = (byte)((byteData & 0x0F) * 17); + l8.PackedValue = intensity2; + color.FromL8(l8); + + pixels[x++, y] = color; + } + + if (isOddWidth) + { + byte byteData = data[offset++]; + + byte intensity1 = (byte)(((byteData & 0xF0) >> 4) * 17); + l8.PackedValue = intensity1; + color.FromL8(l8); + + pixels[left + width - 1, y] = color; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs new file mode 100644 index 0000000000..096f0449bf --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation (optimized for 8-bit grayscale images). + /// + internal class BlackIsZero8TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + int offset = 0; + + var l8 = default(L8); + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + byte intensity = data[offset++]; + + l8.PackedValue = intensity; + color.FromL8(l8); + + pixels[x, y] = color; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs new file mode 100644 index 0000000000..83cef8e758 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation (for all bit depths). + /// + internal class BlackIsZeroTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ushort bitsPerSample0; + + private readonly float factor; + + public BlackIsZeroTiffColor(ushort[] bitsPerSample) + { + this.bitsPerSample0 = bitsPerSample[0]; + this.factor = (1 << this.bitsPerSample0) - 1.0f; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + var bitReader = new BitReader(data); + + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + int value = bitReader.ReadBits(this.bitsPerSample0); + float intensity = value / this.factor; + + color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); + pixels[x, y] = color; + } + + bitReader.NextRow(); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs new file mode 100644 index 0000000000..7ed25f8221 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'PaletteTiffColor' photometric interpretation (for all bit depths). + /// + internal class PaletteTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ushort bitsPerSample0; + + private readonly TPixel[] palette; + + /// The number of bits per sample for each pixel. + /// The RGB color lookup table to use for decoding the image. + public PaletteTiffColor(ushort[] bitsPerSample, ushort[] colorMap) + { + this.bitsPerSample0 = bitsPerSample[0]; + int colorCount = 1 << this.bitsPerSample0; + this.palette = GeneratePalette(colorMap, colorCount); + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var bitReader = new BitReader(data); + + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + int index = bitReader.ReadBits(this.bitsPerSample0); + pixels[x, y] = this.palette[index]; + } + + bitReader.NextRow(); + } + } + + private static TPixel[] GeneratePalette(ushort[] colorMap, int colorCount) + { + var palette = new TPixel[colorCount]; + + const int rOffset = 0; + int gOffset = colorCount; + int bOffset = colorCount * 2; + + for (int i = 0; i < palette.Length; i++) + { + float r = colorMap[rOffset + i] / 65535F; + float g = colorMap[gOffset + i] / 65535F; + float b = colorMap[bOffset + i] / 65535F; + palette[i].FromVector4(new Vector4(r, g, b, 1.0f)); + } + + return palette; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs new file mode 100644 index 0000000000..e45863a57f --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation (optimized for 8-bit full color images). + /// + internal class Rgb888TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + int offset = 0; + + var rgba = default(Rgba32); + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y); + + for (int x = left; x < left + width; x++) + { + byte r = data[offset++]; + byte g = data[offset++]; + byte b = data[offset++]; + + rgba.PackedValue = (uint)(r | (g << 8) | (b << 16) | (0xff << 24)); + color.FromRgba32(rgba); + + pixelRow[x] = color; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000000..e45dd44bdc --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout (for all bit depths). + /// + internal class RgbPlanarTiffColor + where TPixel : unmanaged, IPixel + { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly ushort bitsPerSampleR; + + private readonly ushort bitsPerSampleG; + + private readonly ushort bitsPerSampleB; + + public RgbPlanarTiffColor(ushort[] bitsPerSample) + /* : base(bitsPerSample, null) */ + { + this.bitsPerSampleR = bitsPerSample[0]; + this.bitsPerSampleG = bitsPerSample[1]; + this.bitsPerSampleB = bitsPerSample[2]; + + this.rFactor = (1 << this.bitsPerSampleR) - 1.0f; + this.gFactor = (1 << this.bitsPerSampleG) - 1.0f; + this.bFactor = (1 << this.bitsPerSampleB) - 1.0f; + } + + /// + /// Decodes pixel data using the current photometric interpretation. + /// + /// The buffers to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public void Decode(IManagedByteBuffer[] data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + var rBitReader = new BitReader(data[0].GetSpan()); + var gBitReader = new BitReader(data[1].GetSpan()); + var bBitReader = new BitReader(data[2].GetSpan()); + + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + float r = rBitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; + float g = gBitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; + float b = bBitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; + + color.FromVector4(new Vector4(r, g, b, 1.0f)); + pixels[x, y] = color; + } + + rBitReader.NextRow(); + gBitReader.NextRow(); + bBitReader.NextRow(); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs new file mode 100644 index 0000000000..816ba67b76 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation (for all bit depths). + /// + internal class RgbTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly ushort bitsPerSampleR; + + private readonly ushort bitsPerSampleG; + + private readonly ushort bitsPerSampleB; + + public RgbTiffColor(ushort[] bitsPerSample) + { + this.bitsPerSampleR = bitsPerSample[0]; + this.bitsPerSampleG = bitsPerSample[1]; + this.bitsPerSampleB = bitsPerSample[2]; + + this.rFactor = (1 << this.bitsPerSampleR) - 1.0f; + this.gFactor = (1 << this.bitsPerSampleG) - 1.0f; + this.bFactor = (1 << this.bitsPerSampleB) - 1.0f; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + var bitReader = new BitReader(data); + + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; + float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; + float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; + + color.FromVector4(new Vector4(r, g, b, 1.0f)); + pixels[x, y] = color; + } + + bitReader.NextRow(); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBaseColorDecoder{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBaseColorDecoder{TPixel}.cs new file mode 100644 index 0000000000..c08b26ef13 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBaseColorDecoder{TPixel}.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// The base class for photometric interpretation decoders. + /// + /// The pixel format. + internal abstract class TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + /// Decodes source raw pixel data using the current photometric interpretation. + /// + /// The buffer to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public abstract void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height); + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs new file mode 100644 index 0000000000..0a7941dfbc --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + internal static class TiffColorDecoderFactory + where TPixel : unmanaged, IPixel + { + public static TiffBaseColorDecoder Create(TiffColorType colorType, ushort[] bitsPerSample, ushort[] colorMap) + { + switch (colorType) + { + case TiffColorType.WhiteIsZero: + DebugGuard.IsTrue(bitsPerSample.Length == 1, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZeroTiffColor(bitsPerSample); + + case TiffColorType.WhiteIsZero1: + DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 1, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero1TiffColor(); + + case TiffColorType.WhiteIsZero4: + DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 4, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero4TiffColor(); + + case TiffColorType.WhiteIsZero8: + DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 8, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero8TiffColor(); + + case TiffColorType.BlackIsZero: + DebugGuard.IsTrue(bitsPerSample.Length == 1, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZeroTiffColor(bitsPerSample); + + case TiffColorType.BlackIsZero1: + DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 1, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero1TiffColor(); + + case TiffColorType.BlackIsZero4: + DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 4, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero4TiffColor(); + + case TiffColorType.BlackIsZero8: + DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 8, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero8TiffColor(); + + case TiffColorType.Rgb: + DebugGuard.NotNull(bitsPerSample, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgb888: + DebugGuard.IsTrue( + bitsPerSample.Length == 3 + && bitsPerSample[0] == 8 + && bitsPerSample[1] == 8 + && bitsPerSample[2] == 8, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb888TiffColor(); + + case TiffColorType.PaletteColor: + DebugGuard.NotNull(bitsPerSample, "bitsPerSample"); + DebugGuard.NotNull(colorMap, "colorMap"); + return new PaletteTiffColor(bitsPerSample, colorMap); + + default: + throw TiffThrowHelper.InvalidColorType(colorType.ToString()); + } + } + + public static RgbPlanarTiffColor CreatePlanar(TiffColorType colorType, ushort[] bitsPerSample, ushort[] colorMap) + { + switch (colorType) + { + case TiffColorType.RgbPlanar: + DebugGuard.NotNull(bitsPerSample, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbPlanarTiffColor(bitsPerSample); + + default: + throw TiffThrowHelper.InvalidColorType(colorType.ToString()); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs new file mode 100644 index 0000000000..484d231633 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Provides enumeration of the various TIFF photometric interpretation implementation types. + /// + internal enum TiffColorType + { + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. + /// + BlackIsZero, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for bilevel images. + /// + BlackIsZero1, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 4-bit images. + /// + BlackIsZero4, + + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 8-bit images. + /// + BlackIsZero8, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. + /// + WhiteIsZero, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for bilevel images. + /// + WhiteIsZero1, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 4-bit images. + /// + WhiteIsZero4, + + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 8-bit images. + /// + WhiteIsZero8, + + /// + /// Palette-color. + /// + PaletteColor, + + /// + /// RGB Full Color. + /// + Rgb, + + /// + /// RGB Full Color. Optimized implementation for 8-bit images. + /// + Rgb888, + + /// + /// RGB Full Color. Planar configuration of data. + /// + RgbPlanar, + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor{TPixel}.cs new file mode 100644 index 0000000000..4654142571 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor{TPixel}.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation (optimized for bilevel images). + /// + internal class WhiteIsZero1TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + int offset = 0; + + Color black = Color.Black; + Color white = Color.White; + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x += 8) + { + byte b = data[offset++]; + int maxShift = Math.Min(left + width - x, 8); + + for (int shift = 0; shift < maxShift; shift++) + { + int bit = (b >> (7 - shift)) & 1; + + color.FromRgba32(bit == 0 ? white : black); + + pixels[x + shift, y] = color; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor{TPixel}.cs new file mode 100644 index 0000000000..dae89db28d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor{TPixel}.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation (optimized for 4-bit grayscale images). + /// + internal class WhiteIsZero4TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + int offset = 0; + bool isOddWidth = (width & 1) == 1; + + var l8 = default(L8); + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width - 1;) + { + byte byteData = data[offset++]; + + byte intensity1 = (byte)((15 - ((byteData & 0xF0) >> 4)) * 17); + l8.PackedValue = intensity1; + color.FromL8(l8); + + pixels[x++, y] = color; + + byte intensity2 = (byte)((15 - (byteData & 0x0F)) * 17); + l8.PackedValue = intensity2; + color.FromL8(l8); + + pixels[x++, y] = color; + } + + if (isOddWidth) + { + byte byteData = data[offset++]; + + byte intensity1 = (byte)((15 - ((byteData & 0xF0) >> 4)) * 17); + l8.PackedValue = intensity1; + color.FromL8(l8); + + pixels[left + width - 1, y] = color; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs new file mode 100644 index 0000000000..1b141f9f6a --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation (optimized for 8-bit grayscale images). + /// + internal class WhiteIsZero8TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + int offset = 0; + + var l8 = default(L8); + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + byte intensity = (byte)(255 - data[offset++]); + + l8.PackedValue = intensity; + color.FromL8(l8); + + pixels[x, y] = color; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs new file mode 100644 index 0000000000..697fe2f073 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation (for all bit depths). + /// + internal class WhiteIsZeroTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ushort bitsPerSample0; + + private readonly float factor; + + public WhiteIsZeroTiffColor(ushort[] bitsPerSample) + { + this.bitsPerSample0 = bitsPerSample[0]; + this.factor = (float)Math.Pow(2, this.bitsPerSample0) - 1.0f; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + var bitReader = new BitReader(data); + + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + int value = bitReader.ReadBits(this.bitsPerSample0); + float intensity = 1.0f - (value / this.factor); + + color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); + pixels[x, y] = color; + } + + bitReader.NextRow(); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md new file mode 100644 index 0000000000..5b116b819a --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -0,0 +1,253 @@ +# ImageSharp TIFF codec + +## References +- TIFF + - [TIFF 6.0 Specification](http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf),(http://www.npes.org/pdf/TIFF-v6.pdf) + - [TIFF Supplement 1](http://partners.adobe.com/public/developer/en/tiff/TIFFPM6.pdf) + - [TIFF Supplement 2](http://partners.adobe.com/public/developer/en/tiff/TIFFphotoshop.pdf) + - [TIFF Supplement 3](http://chriscox.org/TIFFTN3d1.pdf) + - [TIFF-F/FX Extension (RFC2301)](http://www.ietf.org/rfc/rfc2301.txt) + - [TIFF/EP Extension (Wikipedia)](https://en.wikipedia.org/wiki/TIFF/EP) + - [Adobe TIFF Pages](http://partners.adobe.com/public/developer/tiff/index.html) + - [Unofficial TIFF FAQ](http://www.awaresystems.be/imaging/tiff/faq.html) + - [CCITT T.4 Compression](https://www.itu.int/rec/T-REC-T.4-198811-S/_page.print) + - [CCITT T.6 Compression](https://www.itu.int/rec/T-REC-T.6/en) + +- DNG + - [Adobe DNG Pages](https://helpx.adobe.com/photoshop/digital-negative.html) + +- Metadata (EXIF) + - [EXIF 2.3 Specification](http://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf) + +- Metadata (XMP) + - [Adobe XMP Pages](http://www.adobe.com/products/xmp.html) + - [Adobe XMP Developer Center](http://www.adobe.com/devnet/xmp.html) + +## Implementation Status + +- The Decoder and Encoder currently only supports a single frame per image. +- Some compression formats are not yet supported. See the list below. + +### Deviations from the TIFF spec (to be fixed) + +- Decoder + - A Baseline TIFF reader must skip over extra components (e.g. RGB with 4 samples per pixels) + - NB: Need to handle this for both planar and chunky data + - If the SampleFormat field is present and not 1 - fail gracefully if you cannot handle this + - Compression=None should treat 16/32-BitsPerSample for all samples as SHORT/LONG (for byte order and padding rows) + - Check Planar format data - is this encoded as strips in order RGBRGBRGB or RRRGGGBBB? + +### Compression Formats + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|--------------------------| +|None | Y | Y | | +|Ccitt1D | Y | Y | | +|PackBits | Y | Y | | +|CcittGroup3Fax | Y | Y | | +|CcittGroup4Fax | | | | +|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | +|Old Jpeg | | | We should not even try to support this | +|Jpeg (Technote 2) | | | | +|Deflate (Technote 2) | Y | Y | Based on PNG Deflate. | +|Old Deflate (Technote 2) | | Y | | + +### Photometric Interpretation Formats + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|--------------------------| +|WhiteIsZero | Y | Y | General + 1/4/8-bit optimised implementations | +|BlackIsZero | Y | Y | General + 1/4/8-bit optimised implementations | +|Rgb (Chunky) | Y | Y | General + Rgb888 optimised implementation | +|Rgb (Planar) | | Y | General implementation only | +|PaletteColor | Y | Y | General implementation only | +|TransparencyMask | | | | +|Separated (TIFF Extension) | | | | +|YCbCr (TIFF Extension) | | | | +|CieLab (TIFF Extension) | | | | +|IccLab (TechNote 1) | | | | + +### Baseline TIFF Tags + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|--------------------------| +|NewSubfileType | | | | +|SubfileType | | | | +|ImageWidth | Y | Y | | +|ImageLength | Y | Y | | +|BitsPerSample | Y | Y | | +|Compression | Y | Y | | +|PhotometricInterpretation | Y | Y | | +|Thresholding | | | | +|CellWidth | | | | +|CellLength | | | | +|FillOrder | | - | Ignore. In practice is very uncommon, and is not recommended. | +|ImageDescription | Y | Y | | +|Make | Y | Y | | +|Model | Y | Y | | +|StripOffsets | Y | Y | | +|Orientation | | - | Ignore. Many readers ignore this tag. | +|SamplesPerPixel | Y | - | Currently ignored, as can be inferred from count of BitsPerSample | +|RowsPerStrip | Y | Y | | +|StripByteCounts | Y | Y | | +|MinSampleValue | | | | +|MaxSampleValue | | | | +|XResolution | Y | Y | | +|YResolution | Y | Y | | +|PlanarConfiguration | | Y | Encoding support only chunky. | +|FreeOffsets | | | | +|FreeByteCounts | | | | +|GrayResponseUnit | | | | +|GrayResponseCurve | | | | +|ResolutionUnit | Y | Y | | +|Software | Y | Y | | +|DateTime | Y | Y | | +|Artist | Y | Y | | +|HostComputer | Y | Y | | +|ColorMap | Y | Y | | +|ExtraSamples | | - | | +|Copyright | Y | Y | | + +### Extension TIFF Tags + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|--------------------------| +|NewSubfileType | | | | +|DocumentName | Y | Y | | +|PageName | | | | +|XPosition | | | | +|YPosition | | | | +|T4Options | | Y | | +|T6Options | | | | +|PageNumber | | | | +|TransferFunction | | | | +|Predictor | Y | Y | only Horizontal | +|WhitePoint | | | | +|PrimaryChromaticities | | | | +|HalftoneHints | | | | +|TileWidth | | - | | +|TileLength | | - | | +|TileOffsets | | - | | +|TileByteCounts | | - | | +|BadFaxLines | | | | +|CleanFaxData | | | | +|ConsecutiveBadFaxLines | | | | +|SubIFDs | | - | | +|InkSet | | | | +|InkNames | | | | +|NumberOfInks | | | | +|DotRange | | | | +|TargetPrinter | | | | +|SampleFormat | | - | | +|SMinSampleValue | | | | +|SMaxSampleValue | | | | +|TransferRange | | | | +|ClipPath | | | | +|XClipPathUnits | | | | +|YClipPathUnits | | | | +|Indexed | | | | +|JPEGTables | | | | +|OPIProxy | | | | +|GlobalParametersIFD | | | | +|ProfileType | | | | +|FaxProfile | | | | +|CodingMethods | | | | +|VersionYear | | | | +|ModeNumber | | | | +|Decode | | | | +|DefaultImageColor | | | | +|JPEGProc | | | | +|JPEGInterchangeFormat | | | | +|JPEGInterchangeFormatLength| | | | +|JPEGRestartInterval | | | | +|JPEGLosslessPredictors | | | | +|JPEGPointTransforms | | | | +|JPEGQTables | | | | +|JPEGDCTables | | | | +|JPEGACTables | | | | +|YCbCrCoefficients | | | | +|YCbCrSubSampling | | | | +|YCbCrPositioning | | | | +|ReferenceBlackWhite | | | | +|StripRowCounts | - | - | See RFC 2301 (File Format for Internet Fax). | +|XMP | Y | Y | | +|ImageID | | | | +|ImageLayer | | | | + +### Private TIFF Tags + +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|--------------------------| +|Wang Annotation | | | | +|MD FileTag | | | | +|MD ScalePixel | | | | +|MD ColorTable | | | | +|MD LabName | | | | +|MD SampleInfo | | | | +|MD PrepDate | | | | +|MD PrepTime | | | | +|MD FileUnits | | | | +|ModelPixelScaleTag | | | | +|IPTC | Y | Y | | +|INGR Packet Data Tag | | | | +|INGR Flag Registers | | | | +|IrasB Transformation Matrix| | | | +|ModelTiepointTag | | | | +|ModelTransformationTag | | | | +|Photoshop | | | | +|Exif IFD | | - | 0x8769 SubExif | +|ICC Profile | Y | Y | | +|GeoKeyDirectoryTag | | | | +|GeoDoubleParamsTag | | | | +|GeoAsciiParamsTag | | | | +|GPS IFD | | | | +|HylaFAX FaxRecvParams | | | | +|HylaFAX FaxSubAddress | | | | +|HylaFAX FaxRecvTime | | | | +|ImageSourceData | | | | +|Interoperability IFD | | | | +|GDAL_METADATA | | | | +|GDAL_NODATA | | | | +|Oce Scanjob Description | | | | +|Oce Application Selector | | | | +|Oce Identification Number | | | | +|Oce ImageLogic Characteristics| | | | +|DNGVersion | | | | +|DNGBackwardVersion | | | | +|UniqueCameraModel | | | | +|LocalizedCameraModel | | | | +|CFAPlaneColor | | | | +|CFALayout | | | | +|LinearizationTable | | | | +|BlackLevelRepeatDim | | | | +|BlackLevel | | | | +|BlackLevelDeltaH | | | | +|BlackLevelDeltaV | | | | +|WhiteLevel | | | | +|DefaultScale | | | | +|DefaultCropOrigin | | | | +|DefaultCropSize | | | | +|ColorMatrix1 | | | | +|ColorMatrix2 | | | | +|CameraCalibration1 | | | | +|CameraCalibration2 | | | | +|ReductionMatrix1 | | | | +|ReductionMatrix2 | | | | +|AnalogBalance | | | | +|AsShotNeutral | | | | +|AsShotWhiteXY | | | | +|BaselineExposure | | | | +|BaselineNoise | | | | +|BaselineSharpness | | | | +|BayerGreenSplit | | | | +|LinearResponseLimit | | | | +|CameraSerialNumber | | | | +|LensInfo | | | | +|ChromaBlurRadius | | | | +|AntiAliasStrength | | | | +|DNGPrivateData | | | | +|MakerNoteSafety | | | | +|CalibrationIlluminant1 | | | | +|CalibrationIlluminant2 | | | | +|BestQualityScale | | | | +|Alias Layer Metadata | | | | diff --git a/src/ImageSharp/Formats/Tiff/T-REC-T.4-198811-S!!PDF-E.pdf b/src/ImageSharp/Formats/Tiff/T-REC-T.4-198811-S!!PDF-E.pdf new file mode 100644 index 0000000000..40724dd1e3 Binary files /dev/null and b/src/ImageSharp/Formats/Tiff/T-REC-T.4-198811-S!!PDF-E.pdf differ diff --git a/src/ImageSharp/Formats/Tiff/T-REC-T.6-198811-I!!PDF-E.pdf b/src/ImageSharp/Formats/Tiff/T-REC-T.6-198811-I!!PDF-E.pdf new file mode 100644 index 0000000000..32fa877b13 Binary files /dev/null and b/src/ImageSharp/Formats/Tiff/T-REC-T.6-198811-I!!PDF-E.pdf differ diff --git a/src/ImageSharp/Formats/Tiff/TIFF-v6.pdf b/src/ImageSharp/Formats/Tiff/TIFF-v6.pdf new file mode 100644 index 0000000000..99117063a0 Binary files /dev/null and b/src/ImageSharp/Formats/Tiff/TIFF-v6.pdf differ diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs new file mode 100644 index 0000000000..35a9a590bb --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Enumerates the available bits per pixel for the tiff format. + /// + public enum TiffBitsPerPixel + { + /// + /// 1 bit per pixel, for bi-color image. + /// + Bit1 = 1, + + /// + /// 4 bits per pixel, for images with a color palette. + /// + Bit4 = 4, + + /// + /// 8 bits per pixel, grayscale or color palette images. + /// + Bit8 = 8, + + /// + /// 24 bits per pixel. One byte for each color channel. + /// + Bit24 = 24, + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs new file mode 100644 index 0000000000..bc74cbc5fb --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The number of bits per component. + /// + public enum TiffBitsPerSample + { + /// + /// The Bits per samples is not known. + /// + Unknown = 0, + + /// + /// One bit per sample for bicolor images. + /// + Bit1 = 1, + + /// + /// Four bits per sample for grayscale images with 16 different levels of gray or paletted images with a palette of 16 colors. + /// + Bit4 = 4, + + /// + /// Eight bits per sample for grayscale images with 256 different levels of gray or paletted images with a palette of 256 colors. + /// + Bit8 = 8, + + /// + /// 24 bits per sample, each color channel has 8 Bits. + /// + Bit24 = 24, + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs new file mode 100644 index 0000000000..5c4c374bef --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Constants; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + internal static class TiffBitsPerSampleExtensions + { + /// + /// Gets the bits per channel array for a given BitsPerSample value, e,g, for RGB888: [8, 8, 8] + /// + /// The tiff bits per sample. + /// Bits per sample array. + public static ushort[] Bits(this TiffBitsPerSample tiffBitsPerSample) + { + switch (tiffBitsPerSample) + { + case TiffBitsPerSample.Bit1: + return TiffConstants.BitsPerSample1Bit; + case TiffBitsPerSample.Bit4: + return TiffConstants.BitsPerSample4Bit; + case TiffBitsPerSample.Bit8: + return TiffConstants.BitsPerSample8Bit; + case TiffBitsPerSample.Bit24: + return TiffConstants.BitsPerSampleRgb8Bit; + + default: + return Array.Empty(); + } + } + + /// + /// Maps an array of bits per sample to a concrete enum value. + /// + /// The bits per sample array. + /// TiffBitsPerSample enum value. + public static TiffBitsPerSample GetBitsPerSample(this ushort[] bitsPerSample) + { + switch (bitsPerSample.Length) + { + case 3: + if (bitsPerSample[0] == TiffConstants.BitsPerSampleRgb8Bit[0] && + bitsPerSample[1] == TiffConstants.BitsPerSampleRgb8Bit[1] && + bitsPerSample[2] == TiffConstants.BitsPerSampleRgb8Bit[2]) + { + return TiffBitsPerSample.Bit24; + } + + break; + + case 1: + if (bitsPerSample[0] == TiffConstants.BitsPerSample1Bit[0]) + { + return TiffBitsPerSample.Bit1; + } + + if (bitsPerSample[0] == TiffConstants.BitsPerSample4Bit[0]) + { + return TiffBitsPerSample.Bit4; + } + + if (bitsPerSample[0] == TiffConstants.BitsPerSample8Bit[0]) + { + return TiffBitsPerSample.Bit8; + } + + break; + } + + return TiffBitsPerSample.Unknown; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffConfigurationModule.cs b/src/ImageSharp/Formats/Tiff/TiffConfigurationModule.cs new file mode 100644 index 0000000000..e96dba2077 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffConfigurationModule.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Registers the image encoders, decoders and mime type detectors for the TIFF format. + /// + public sealed class TiffConfigurationModule : IConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetEncoder(TiffFormat.Instance, new TiffEncoder()); + configuration.ImageFormatsManager.SetDecoder(TiffFormat.Instance, new TiffDecoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new TiffImageFormatDetector()); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs new file mode 100644 index 0000000000..9d52e34dfe --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Image decoder for generating an image out of a TIFF stream. + /// + public class TiffDecoder : IImageDecoder, ITiffDecoderOptions, IImageInfoDetector + { + /// + /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. + /// + public bool IgnoreMetadata { get; set; } + + /// + public Image Decode(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, "stream"); + + var decoder = new TiffDecoderCore(configuration, this); + return decoder.Decode(configuration, stream); + } + + /// + public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); + + /// + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + var decoder = new TiffDecoderCore(configuration, this); + return decoder.DecodeAsync(configuration, stream, cancellationToken); + } + + /// + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + + /// + public IImageInfo Identify(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + var decoder = new TiffDecoderCore(configuration, this); + return decoder.Identify(configuration, stream); + } + + /// + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(stream, nameof(stream)); + + var decoder = new TiffDecoderCore(configuration, this); + return decoder.IdentifyAsync(configuration, stream, cancellationToken); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs new file mode 100644 index 0000000000..b7b7640072 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -0,0 +1,339 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Performs the tiff decoding operation. + /// + internal class TiffDecoderCore : IImageDecoderInternals + { + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. + /// + private readonly bool ignoreMetadata; + + /// + /// The stream to decode from. + /// + private BufferedReadStream inputStream; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The decoder options. + public TiffDecoderCore(Configuration configuration, ITiffDecoderOptions options) + { + options ??= new TiffDecoder(); + + this.Configuration = configuration ?? Configuration.Default; + this.ignoreMetadata = options.IgnoreMetadata; + this.memoryAllocator = this.Configuration.MemoryAllocator; + } + + /// + /// Gets or sets the number of bits per component of the pixel format used to decode the image. + /// + public TiffBitsPerSample BitsPerSample { get; set; } + + /// + /// Gets or sets the bits per pixel. + /// + public int BitsPerPixel { get; set; } + + /// + /// Gets or sets the lookup table for RGB palette colored images. + /// + public ushort[] ColorMap { get; set; } + + /// + /// Gets or sets the photometric interpretation implementation to use when decoding the image. + /// + public TiffColorType ColorType { get; set; } + + /// + /// Gets or sets the compression used, when the image was encoded. + /// + public TiffDecoderCompressionType CompressionType { get; set; } + + /// + /// Gets or sets the Fax specific compression options. + /// + public FaxCompressionOptions FaxCompressionOptions { get; set; } + + /// + /// Gets or sets the planar configuration type to use when decoding the image. + /// + public TiffPlanarConfiguration PlanarConfiguration { get; set; } + + /// + /// Gets or sets the photometric interpretation. + /// + public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } + + /// + /// Gets or sets the horizontal predictor. + /// + public TiffPredictor Predictor { get; set; } + + /// + public Configuration Configuration { get; } + + /// + public Size Dimensions { get; private set; } + + /// + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + this.inputStream = stream; + var reader = new DirectoryReader(stream); + + IEnumerable directories = reader.Read(); + + var frames = new List>(); + foreach (ExifProfile ifd in directories) + { + ImageFrame frame = this.DecodeFrame(ifd); + frames.Add(frame); + } + + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder); + + // TODO: Tiff frames can have different sizes + ImageFrame root = frames[0]; + this.Dimensions = root.Size(); + foreach (ImageFrame frame in frames) + { + if (frame.Size() != root.Size()) + { + TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported"); + } + } + + return new Image(this.Configuration, metadata, frames); + } + + /// + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.inputStream = stream; + var reader = new DirectoryReader(stream); + IEnumerable directories = reader.Read(); + + ExifProfile rootFrameExifProfile = directories.First(); + var rootMetadata = TiffFrameMetadata.Parse(rootFrameExifProfile); + + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(reader.ByteOrder, rootFrameExifProfile); + int width = GetImageWidth(rootFrameExifProfile); + int height = GetImageHeight(rootFrameExifProfile); + + return new ImageInfo(new PixelTypeInfo((int)rootMetadata.BitsPerPixel), width, height, metadata); + } + + /// + /// Decodes the image data from a specified IFD. + /// + /// The pixel format. + /// The IFD tags. + /// + /// The tiff frame. + /// + private ImageFrame DecodeFrame(ExifProfile tags) + where TPixel : unmanaged, IPixel + { + ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ? + new ImageFrameMetadata() : + new ImageFrameMetadata { ExifProfile = tags, XmpProfile = tags.GetValue(ExifTag.XMP)?.Value }; + + TiffFrameMetadata tiffFrameMetaData = imageFrameMetaData.GetTiffMetadata(); + TiffFrameMetadata.Parse(tiffFrameMetaData, tags); + + this.VerifyAndParse(tags, tiffFrameMetaData); + + int width = GetImageWidth(tags); + int height = GetImageHeight(tags); + var frame = new ImageFrame(this.Configuration, width, height, imageFrameMetaData); + + int rowsPerStrip = tags.GetValue(ExifTag.RowsPerStrip) != null ? (int)tags.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; + Number[] stripOffsets = tags.GetValue(ExifTag.StripOffsets)?.Value; + Number[] stripByteCounts = tags.GetValue(ExifTag.StripByteCounts)?.Value; + + if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) + { + this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts); + } + else + { + this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts); + } + + return frame; + } + + /// + /// Calculates the size (in bytes) for a pixel buffer using the determined color format. + /// + /// The width for the desired pixel buffer. + /// The height for the desired pixel buffer. + /// The index of the plane for planar image configuration (or zero for chunky). + /// The size (in bytes) of the required pixel buffer. + private int CalculateStripBufferSize(int width, int height, int plane = -1) + { + int bitsPerPixel; + + if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) + { + DebugGuard.IsTrue(plane == -1, "Excepted Chunky planar."); + bitsPerPixel = this.BitsPerPixel; + } + else + { + bitsPerPixel = this.BitsPerSample.Bits()[plane]; + } + + int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; + return bytesPerRow * height; + } + + /// + /// Decodes the image data for strip encoded data. + /// + /// The pixel format. + /// The image frame to decode data into. + /// The number of rows per strip of data. + /// An array of byte offsets to each strip in the image. + /// An array of the size of each strip (in bytes). + private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts) + where TPixel : unmanaged, IPixel + { + int stripsPerPixel = this.BitsPerSample.Bits().Length; + int stripsPerPlane = stripOffsets.Length / stripsPerPixel; + int bitsPerPixel = this.BitsPerPixel; + + Buffer2D pixels = frame.PixelBuffer; + + var stripBuffers = new IManagedByteBuffer[stripsPerPixel]; + + try + { + for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) + { + int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex); + stripBuffers[stripIndex] = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize); + } + + using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions); + + RgbPlanarTiffColor colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample.Bits(), this.ColorMap); + + for (int i = 0; i < stripsPerPlane; i++) + { + int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; + + for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) + { + int stripIndex = (i * stripsPerPixel) + planeIndex; + + decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffers[planeIndex].GetSpan()); + } + + colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); + } + } + finally + { + foreach (IManagedByteBuffer buf in stripBuffers) + { + buf?.Dispose(); + } + } + } + + private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts) + where TPixel : unmanaged, IPixel + { + // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. + if (rowsPerStrip == TiffConstants.RowsPerStripInfinity) + { + rowsPerStrip = frame.Height; + } + + int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); + int bitsPerPixel = this.BitsPerPixel; + + using IManagedByteBuffer stripBuffer = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize, AllocationOptions.Clean); + + Buffer2D pixels = frame.PixelBuffer; + + using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions); + + TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample.Bits(), this.ColorMap); + + for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) + { + int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; + int top = rowsPerStrip * stripIndex; + if (top + stripHeight > frame.Height) + { + // Make sure we ignore any strips that are not needed for the image (if too many are present) + break; + } + + decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffer.GetSpan()); + + colorDecoder.Decode(stripBuffer.GetSpan(), pixels, 0, top, frame.Width, stripHeight); + } + } + + /// + /// Gets the width of the image frame. + /// + /// The image frame exif profile. + /// The image width. + private static int GetImageWidth(ExifProfile exifProfile) + { + IExifValue width = exifProfile.GetValue(ExifTag.ImageWidth); + if (width == null) + { + TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageWidth"); + } + + return (int)width.Value; + } + + /// + /// Gets the height of the image frame. + /// + /// The image frame exif profile. + /// The image height. + private static int GetImageHeight(ExifProfile exifProfile) + { + IExifValue height = exifProfile.GetValue(ExifTag.ImageLength); + if (height == null) + { + TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength"); + } + + return (int)height.Value; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs new file mode 100644 index 0000000000..6f8a81a827 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Linq; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The decoder metadata creator. + /// + internal static class TiffDecoderMetadataCreator + { + public static ImageMetadata Create(List> frames, bool ignoreMetadata, ByteOrder byteOrder) + where TPixel : unmanaged, IPixel + { + if (frames.Count < 1) + { + TiffThrowHelper.ThrowImageFormatException("Expected at least one frame."); + } + + var imageMetaData = new ImageMetadata(); + ExifProfile exifProfileRootFrame = frames[0].Metadata.ExifProfile; + + SetResolution(imageMetaData, exifProfileRootFrame); + + TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); + tiffMetadata.ByteOrder = byteOrder; + + if (!ignoreMetadata) + { + for (int i = 0; i < frames.Count; i++) + { + ImageFrame frame = frames[i]; + ImageFrameMetadata frameMetaData = frame.Metadata; + if (TryGetIptc(frameMetaData.ExifProfile.Values, out byte[] iptcBytes)) + { + frameMetaData.IptcProfile = new IptcProfile(iptcBytes); + } + + IExifValue iccProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.IccProfile); + if (iccProfileBytes != null) + { + frameMetaData.IccProfile = new IccProfile(iccProfileBytes.Value); + } + } + } + + return imageMetaData; + } + + public static ImageMetadata Create(ByteOrder byteOrder, ExifProfile exifProfile) + { + var imageMetaData = new ImageMetadata(); + SetResolution(imageMetaData, exifProfile); + + TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); + tiffMetadata.ByteOrder = byteOrder; + + return imageMetaData; + } + + private static void SetResolution(ImageMetadata imageMetaData, ExifProfile exifProfile) + { + imageMetaData.ResolutionUnits = exifProfile != null ? UnitConverter.ExifProfileToResolutionUnit(exifProfile) : PixelResolutionUnit.PixelsPerInch; + double? horizontalResolution = exifProfile?.GetValue(ExifTag.XResolution)?.Value.ToDouble(); + if (horizontalResolution != null) + { + imageMetaData.HorizontalResolution = horizontalResolution.Value; + } + + double? verticalResolution = exifProfile?.GetValue(ExifTag.YResolution)?.Value.ToDouble(); + if (verticalResolution != null) + { + imageMetaData.VerticalResolution = verticalResolution.Value; + } + } + + private static bool TryGetIptc(IReadOnlyList exifValues, out byte[] iptcBytes) + { + iptcBytes = null; + IExifValue iptc = exifValues.FirstOrDefault(f => f.Tag == ExifTag.IPTC); + + if (iptc != null) + { + if (iptc.DataType == ExifDataType.Byte || iptc.DataType == ExifDataType.Undefined) + { + iptcBytes = (byte[])iptc.GetValue(); + return true; + } + + // Some Encoders write the data type of IPTC as long. + if (iptc.DataType == ExifDataType.Long) + { + uint[] iptcValues = (uint[])iptc.GetValue(); + iptcBytes = new byte[iptcValues.Length * 4]; + Buffer.BlockCopy(iptcValues, 0, iptcBytes, 0, iptcValues.Length * 4); + if (iptcBytes[0] == 0x1c) + { + return true; + } + else if (iptcBytes[3] != 0x1c) + { + return false; + } + + // Probably wrong endianess, swap byte order. + Span iptcBytesSpan = iptcBytes.AsSpan(); + Span buffer = stackalloc byte[4]; + for (int i = 0; i < iptcBytes.Length; i += 4) + { + iptcBytesSpan.Slice(i, 4).CopyTo(buffer); + iptcBytes[i] = buffer[3]; + iptcBytes[i + 1] = buffer[2]; + iptcBytes[i + 2] = buffer[1]; + iptcBytes[i + 3] = buffer[0]; + } + + return true; + } + } + + return false; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs new file mode 100644 index 0000000000..b5f3e7cf1e --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -0,0 +1,281 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Linq; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The decoder options parser. + /// + internal static class TiffDecoderOptionsParser + { + private const TiffPlanarConfiguration DefaultPlanarConfiguration = TiffPlanarConfiguration.Chunky; + + /// + /// Determines the TIFF compression and color types, and reads any associated parameters. + /// + /// The options. + /// The exif profile of the frame to decode. + /// The IFD entries container to read the image format information for current frame. + public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata) + { + if (exifProfile.GetValue(ExifTag.TileOffsets)?.Value != null) + { + TiffThrowHelper.ThrowNotSupported("Tiled images are not supported."); + } + + if (exifProfile.GetValue(ExifTag.ExtraSamples)?.Value != null) + { + TiffThrowHelper.ThrowNotSupported("ExtraSamples is not supported."); + } + + TiffFillOrder fillOrder = (TiffFillOrder?)exifProfile.GetValue(ExifTag.FillOrder)?.Value ?? TiffFillOrder.MostSignificantBitFirst; + if (fillOrder != TiffFillOrder.MostSignificantBitFirst) + { + TiffThrowHelper.ThrowNotSupported("The lower-order bits of the byte FillOrder is not supported."); + } + + if (frameMetadata.Predictor == TiffPredictor.FloatingPoint) + { + TiffThrowHelper.ThrowNotSupported("TIFF images with FloatingPoint horizontal predictor are not supported."); + } + + TiffSampleFormat[] sampleFormat = exifProfile.GetValue(ExifTag.SampleFormat)?.Value?.Select(a => (TiffSampleFormat)a).ToArray(); + if (sampleFormat != null) + { + foreach (TiffSampleFormat format in sampleFormat) + { + if (format != TiffSampleFormat.UnsignedInteger) + { + TiffThrowHelper.ThrowNotSupported("ImageSharp only supports the UnsignedInteger SampleFormat."); + } + } + } + + if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null) + { + TiffThrowHelper.ThrowNotSupported("Variable-sized strips are not supported."); + } + + VerifyRequiredFieldsArePresent(exifProfile, frameMetadata); + + options.PlanarConfiguration = (TiffPlanarConfiguration?)exifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value ?? DefaultPlanarConfiguration; + options.Predictor = frameMetadata.Predictor ?? TiffPredictor.None; + options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb; + options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24; + options.BitsPerSample = GetBitsPerSample(frameMetadata.BitsPerPixel); + + options.ParseColorType(exifProfile); + options.ParseCompression(frameMetadata.Compression, exifProfile); + } + + private static void VerifyRequiredFieldsArePresent(ExifProfile exifProfile, TiffFrameMetadata frameMetadata) + { + if (exifProfile.GetValue(ExifTag.StripOffsets) == null) + { + TiffThrowHelper.ThrowImageFormatException("StripOffsets are missing and are required for decoding the TIFF image!"); + } + + if (exifProfile.GetValue(ExifTag.StripByteCounts) == null) + { + TiffThrowHelper.ThrowImageFormatException("StripByteCounts are missing and are required for decoding the TIFF image!"); + } + + if (frameMetadata.BitsPerPixel == null) + { + TiffThrowHelper.ThrowNotSupported("The TIFF BitsPerSample entry is missing which is required to decode the image!"); + } + } + + private static void ParseColorType(this TiffDecoderCore options, ExifProfile exifProfile) + { + switch (options.PhotometricInterpretation) + { + case TiffPhotometricInterpretation.WhiteIsZero: + { + if (options.BitsPerSample.Bits().Length != 1) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + switch (options.BitsPerSample) + { + case TiffBitsPerSample.Bit8: + { + options.ColorType = TiffColorType.WhiteIsZero8; + break; + } + + case TiffBitsPerSample.Bit4: + { + options.ColorType = TiffColorType.WhiteIsZero4; + break; + } + + case TiffBitsPerSample.Bit1: + { + options.ColorType = TiffColorType.WhiteIsZero1; + break; + } + + default: + { + options.ColorType = TiffColorType.WhiteIsZero; + break; + } + } + + break; + } + + case TiffPhotometricInterpretation.BlackIsZero: + { + if (options.BitsPerSample.Bits().Length != 1) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + switch (options.BitsPerSample) + { + case TiffBitsPerSample.Bit8: + { + options.ColorType = TiffColorType.BlackIsZero8; + break; + } + + case TiffBitsPerSample.Bit4: + { + options.ColorType = TiffColorType.BlackIsZero4; + break; + } + + case TiffBitsPerSample.Bit1: + { + options.ColorType = TiffColorType.BlackIsZero1; + break; + } + + default: + { + options.ColorType = TiffColorType.BlackIsZero; + break; + } + } + + break; + } + + case TiffPhotometricInterpretation.Rgb: + { + if (options.BitsPerSample.Bits().Length != 3) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky) + { + options.ColorType = options.BitsPerSample == TiffBitsPerSample.Bit24 ? TiffColorType.Rgb888 : TiffColorType.Rgb; + } + else + { + options.ColorType = TiffColorType.RgbPlanar; + } + + break; + } + + case TiffPhotometricInterpretation.PaletteColor: + { + options.ColorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value; + if (options.ColorMap != null) + { + if (options.BitsPerSample.Bits().Length != 1) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + options.ColorType = TiffColorType.PaletteColor; + } + else + { + TiffThrowHelper.ThrowNotSupported("The TIFF ColorMap entry is missing for a palette color image."); + } + + break; + } + + default: + { + TiffThrowHelper.ThrowNotSupported($"The specified TIFF photometric interpretation is not supported: {options.PhotometricInterpretation}"); + } + + break; + } + } + + private static void ParseCompression(this TiffDecoderCore options, TiffCompression? compression, ExifProfile exifProfile) + { + switch (compression) + { + case TiffCompression.None: + { + options.CompressionType = TiffDecoderCompressionType.None; + break; + } + + case TiffCompression.PackBits: + { + options.CompressionType = TiffDecoderCompressionType.PackBits; + break; + } + + case TiffCompression.Deflate: + case TiffCompression.OldDeflate: + { + options.CompressionType = TiffDecoderCompressionType.Deflate; + break; + } + + case TiffCompression.Lzw: + { + options.CompressionType = TiffDecoderCompressionType.Lzw; + break; + } + + case TiffCompression.CcittGroup3Fax: + { + options.CompressionType = TiffDecoderCompressionType.T4; + options.FaxCompressionOptions = exifProfile.GetValue(ExifTag.T4Options) != null ? (FaxCompressionOptions)exifProfile.GetValue(ExifTag.T4Options).Value : FaxCompressionOptions.None; + + break; + } + + case TiffCompression.Ccitt1D: + { + options.CompressionType = TiffDecoderCompressionType.HuffmanRle; + break; + } + + default: + { + TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported"); + break; + } + } + } + + private static TiffBitsPerSample GetBitsPerSample(TiffBitsPerPixel? bitsPerPixel) => bitsPerPixel switch + { + TiffBitsPerPixel.Bit1 => TiffBitsPerSample.Bit1, + TiffBitsPerPixel.Bit4 => TiffBitsPerSample.Bit4, + TiffBitsPerPixel.Bit8 => TiffBitsPerSample.Bit8, + TiffBitsPerPixel.Bit24 => TiffBitsPerSample.Bit24, + _ => TiffBitsPerSample.Bit24, + }; + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs new file mode 100644 index 0000000000..7d5ccdb941 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Encoder for writing the data image to a stream in TIFF format. + /// + public class TiffEncoder : IImageEncoder, ITiffEncoderOptions + { + /// + public TiffBitsPerPixel? BitsPerPixel { get; set; } + + /// + public TiffCompression? Compression { get; set; } + + /// + public DeflateCompressionLevel? CompressionLevel { get; set; } + + /// + public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; } + + /// + public TiffPredictor? HorizontalPredictor { get; set; } + + /// + public IQuantizer Quantizer { get; set; } + + /// + public void Encode(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + var encode = new TiffEncoderCore(this, image.GetMemoryAllocator()); + encode.Encode(image, stream); + } + + /// + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + var encoder = new TiffEncoderCore(this, image.GetMemoryAllocator()); + return encoder.EncodeAsync(image, stream, cancellationToken); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs new file mode 100644 index 0000000000..74c516f63b --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -0,0 +1,381 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Tiff.Writers; +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 +{ + /// + /// Performs the TIFF encoding operation. + /// + internal sealed class TiffEncoderCore : IImageEncoderInternals + { + private static readonly ushort ByteOrderMarker = BitConverter.IsLittleEndian + ? TiffConstants.ByteOrderLittleEndianShort + : TiffConstants.ByteOrderBigEndianShort; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// A scratch buffer to reduce allocations. + /// + private readonly byte[] buffer = new byte[4]; + + /// + /// The global configuration. + /// + private Configuration configuration; + + /// + /// The quantizer for creating color palette image. + /// + private readonly IQuantizer quantizer; + + /// + /// Sets the deflate compression level. + /// + private readonly DeflateCompressionLevel compressionLevel; + + /// + /// The default predictor is None. + /// + private const TiffPredictor DefaultPredictor = TiffPredictor.None; + + /// + /// The default bits per pixel is Bit24. + /// + private const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24; + + /// + /// The default compression is None. + /// + private const TiffCompression DefaultCompression = TiffCompression.None; + + /// + /// The default photometric interpretation is Rgb. + /// + private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + + /// + /// Initializes a new instance of the class. + /// + /// The options for the encoder. + /// The memory allocator. + public TiffEncoderCore(ITiffEncoderOptions options, MemoryAllocator memoryAllocator) + { + this.memoryAllocator = memoryAllocator; + this.PhotometricInterpretation = options.PhotometricInterpretation; + this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; + this.BitsPerPixel = options.BitsPerPixel; + this.HorizontalPredictor = options.HorizontalPredictor; + this.CompressionType = options.Compression; + this.compressionLevel = options.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression; + } + + /// + /// Gets the photometric interpretation implementation to use when encoding the image. + /// + internal TiffPhotometricInterpretation? PhotometricInterpretation { get; private set; } + + /// + /// Gets or sets the compression implementation to use when encoding the image. + /// + internal TiffCompression? CompressionType { get; set; } + + /// + /// Gets or sets a value indicating which horizontal predictor to use. This can improve the compression ratio with deflate compression. + /// + internal TiffPredictor? HorizontalPredictor { get; set; } + + /// + /// Gets the bits per pixel. + /// + internal TiffBitsPerPixel? BitsPerPixel { get; private set; } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + this.configuration = image.GetConfiguration(); + + ImageFrameMetadata rootFrameMetaData = image.Frames.RootFrame.Metadata; + TiffFrameMetadata rootFrameTiffMetaData = rootFrameMetaData.GetTiffMetadata(); + + // Determine the correct values to encode with. + // EncoderOptions > Metadata > Default. + TiffBitsPerPixel? bitsPerPixel = this.BitsPerPixel ?? rootFrameTiffMetaData.BitsPerPixel; + + TiffPhotometricInterpretation? photometricInterpretation = this.PhotometricInterpretation ?? rootFrameTiffMetaData.PhotometricInterpretation; + + TiffPredictor predictor = + this.HorizontalPredictor + ?? rootFrameTiffMetaData.Predictor + ?? DefaultPredictor; + + TiffCompression compression = + this.CompressionType + ?? rootFrameTiffMetaData.Compression + ?? DefaultCompression; + + // Make sure, the Encoder options makes sense in combination with each other. + this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor); + + using (var writer = new TiffStreamWriter(stream)) + { + long firstIfdMarker = this.WriteHeader(writer); + + // TODO: multiframing is not supported + this.WriteImage(writer, image, firstIfdMarker); + } + } + + /// + /// Writes the TIFF file header. + /// + /// The to write data to. + /// + /// The marker to write the first IFD offset. + /// + public long WriteHeader(TiffStreamWriter writer) + { + writer.Write(ByteOrderMarker); + writer.Write(TiffConstants.HeaderMagicNumber); + return writer.PlaceMarker(); + } + + /// + /// Writes all data required to define an image. + /// + /// The pixel format. + /// The to write data to. + /// The to encode from. + /// The marker to write this IFD offset. + private void WriteImage(TiffStreamWriter writer, Image image, long ifdOffset) + where TPixel : unmanaged, IPixel + { + var entriesCollector = new TiffEncoderEntriesCollector(); + + using TiffBaseCompressor compressor = TiffCompressorFactory.Create( + this.CompressionType ?? TiffCompression.None, + writer.BaseStream, + this.memoryAllocator, + image.Width, + (int)this.BitsPerPixel, + this.compressionLevel, + this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); + + using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create( + this.PhotometricInterpretation, + image.Frames.RootFrame, + this.quantizer, + this.memoryAllocator, + this.configuration, + entriesCollector, + (int)this.BitsPerPixel); + + int rowsPerStrip = this.CalcRowsPerStrip(image.Frames.RootFrame.Height, colorWriter.BytesPerRow); + + colorWriter.Write(compressor, rowsPerStrip); + + entriesCollector.ProcessImageFormat(this); + entriesCollector.ProcessGeneral(image); + + writer.WriteMarker(ifdOffset, (uint)writer.Position); + long nextIfdMarker = this.WriteIfd(writer, entriesCollector.Entries); + } + + /// + /// Calculates the number of rows written per strip. + /// + /// The height of the image. + /// The number of bytes per row. + /// Number of rows per strip. + private int CalcRowsPerStrip(int height, int bytesPerRow) + { + DebugGuard.MustBeGreaterThan(height, 0, nameof(height)); + DebugGuard.MustBeGreaterThan(bytesPerRow, 0, nameof(bytesPerRow)); + + int rowsPerStrip = TiffConstants.DefaultStripSize / bytesPerRow; + + if (rowsPerStrip > 0) + { + if (rowsPerStrip < height) + { + return rowsPerStrip; + } + + return height; + } + + return 1; + } + + /// + /// Writes a TIFF IFD block. + /// + /// The to write data to. + /// The IFD entries to write to the file. + /// The marker to write the next IFD offset (if present). + private long WriteIfd(TiffStreamWriter writer, List entries) + { + if (entries.Count == 0) + { + TiffThrowHelper.ThrowArgumentException("There must be at least one entry per IFD."); + } + + uint dataOffset = (uint)writer.Position + (uint)(6 + (entries.Count * 12)); + var largeDataBlocks = new List(); + + entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag); + + writer.Write((ushort)entries.Count); + + foreach (IExifValue entry in entries) + { + writer.Write((ushort)entry.Tag); + writer.Write((ushort)entry.DataType); + writer.Write(ExifWriter.GetNumberOfComponents(entry)); + + uint length = ExifWriter.GetLength(entry); + if (length <= 4) + { + int sz = ExifWriter.WriteValue(entry, this.buffer, 0); + DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written"); + writer.WritePadded(this.buffer.AsSpan(0, sz)); + } + else + { + var raw = new byte[length]; + int sz = ExifWriter.WriteValue(entry, raw, 0); + DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written"); + largeDataBlocks.Add(raw); + writer.Write(dataOffset); + dataOffset += (uint)(raw.Length + (raw.Length % 2)); + } + } + + long nextIfdMarker = writer.PlaceMarker(); + + foreach (byte[] dataBlock in largeDataBlocks) + { + writer.Write(dataBlock); + + if (dataBlock.Length % 2 == 1) + { + writer.Write(0); + } + } + + return nextIfdMarker; + } + + private void SanitizeAndSetEncoderOptions(TiffBitsPerPixel? bitsPerPixel, int inputBitsPerPixel, TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffPredictor predictor) + { + // BitsPerPixel should be the primary source of truth for the encoder options. + if (bitsPerPixel.HasValue) + { + switch (bitsPerPixel) + { + case TiffBitsPerPixel.Bit1: + if (compression == TiffCompression.Ccitt1D || compression == TiffCompression.CcittGroup3Fax || compression == TiffCompression.CcittGroup4Fax) + { + // The normal PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero. + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None); + break; + } + + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, TiffPredictor.None); + break; + case TiffBitsPerPixel.Bit4: + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, TiffPredictor.None); + break; + case TiffBitsPerPixel.Bit8: + this.SetEncoderOptions(bitsPerPixel, photometricInterpretation ?? TiffPhotometricInterpretation.BlackIsZero, compression, predictor); + break; + default: + this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor); + break; + } + + return; + } + + // If no photometric interpretation was chosen, the input image bit per pixel should be preserved. + if (!photometricInterpretation.HasValue) + { + // At the moment only 8 and 32 bits per pixel can be preserved by the tiff encoder. + if (inputBitsPerPixel == 8) + { + this.SetEncoderOptions(TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); + return; + } + + this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor); + return; + } + + switch (photometricInterpretation) + { + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + if (this.CompressionType == TiffCompression.Ccitt1D || + this.CompressionType == TiffCompression.CcittGroup3Fax || + this.CompressionType == TiffCompression.CcittGroup4Fax) + { + this.SetEncoderOptions(TiffBitsPerPixel.Bit1, photometricInterpretation, compression, TiffPredictor.None); + return; + } + else + { + this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor); + return; + } + + case TiffPhotometricInterpretation.PaletteColor: + this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor); + return; + + case TiffPhotometricInterpretation.Rgb: + this.SetEncoderOptions(TiffBitsPerPixel.Bit24, photometricInterpretation, compression, predictor); + return; + } + + this.SetEncoderOptions(DefaultBitsPerPixel, DefaultPhotometricInterpretation, compression, predictor); + } + + private void SetEncoderOptions(TiffBitsPerPixel? bitsPerPixel, TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffPredictor predictor) + { + this.BitsPerPixel = bitsPerPixel; + this.PhotometricInterpretation = photometricInterpretation; + this.CompressionType = compression; + this.HorizontalPredictor = predictor; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs new file mode 100644 index 0000000000..09605bc690 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -0,0 +1,378 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + internal class TiffEncoderEntriesCollector + { + private const string SoftwareValue = "ImageSharp"; + + public List Entries { get; } = new List(); + + public void ProcessGeneral(Image image) + where TPixel : unmanaged, IPixel + => new GeneralProcessor(this).Process(image); + + public void ProcessImageFormat(TiffEncoderCore encoder) + => new ImageFormatProcessor(this).Process(encoder); + + public void AddOrReplace(IExifValue entry) + { + int index = this.Entries.FindIndex(t => t.Tag == entry.Tag); + if (index >= 0) + { + this.Entries[index] = entry; + } + else + { + this.Entries.Add(entry); + } + } + + private void Add(IExifValue entry) => this.Entries.Add(entry); + + private class GeneralProcessor + { + private readonly TiffEncoderEntriesCollector collector; + + public GeneralProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + + public void Process(Image image) + where TPixel : unmanaged, IPixel + { + ImageFrame rootFrame = image.Frames.RootFrame; + ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); + byte[] rootFrameXmpBytes = rootFrame.Metadata.XmpProfile; + + var width = new ExifLong(ExifTagValue.ImageWidth) + { + Value = (uint)image.Width + }; + + var height = new ExifLong(ExifTagValue.ImageLength) + { + Value = (uint)image.Height + }; + + var software = new ExifString(ExifTagValue.Software) + { + Value = SoftwareValue + }; + + this.collector.Add(width); + this.collector.Add(height); + + this.ProcessResolution(image.Metadata, rootFrameExifProfile); + this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpBytes); + this.ProcessMetadata(rootFrameExifProfile); + + if (!this.collector.Entries.Exists(t => t.Tag == ExifTag.Software)) + { + this.collector.Add(software); + } + } + + private static bool IsPureMetadata(ExifTag tag) + { + switch ((ExifTagValue)(ushort)tag) + { + case ExifTagValue.DocumentName: + case ExifTagValue.ImageDescription: + case ExifTagValue.Make: + case ExifTagValue.Model: + case ExifTagValue.Software: + case ExifTagValue.DateTime: + case ExifTagValue.Artist: + case ExifTagValue.HostComputer: + case ExifTagValue.TargetPrinter: + case ExifTagValue.XMP: + case ExifTagValue.Rating: + case ExifTagValue.RatingPercent: + case ExifTagValue.ImageID: + case ExifTagValue.Copyright: + case ExifTagValue.MDLabName: + case ExifTagValue.MDSampleInfo: + case ExifTagValue.MDPrepDate: + case ExifTagValue.MDPrepTime: + case ExifTagValue.MDFileUnits: + case ExifTagValue.SEMInfo: + case ExifTagValue.XPTitle: + case ExifTagValue.XPComment: + case ExifTagValue.XPAuthor: + case ExifTagValue.XPKeywords: + case ExifTagValue.XPSubject: + return true; + default: + return false; + } + } + + private void ProcessResolution(ImageMetadata imageMetadata, ExifProfile exifProfile) + { + UnitConverter.SetResolutionValues( + exifProfile, + imageMetadata.ResolutionUnits, + imageMetadata.HorizontalResolution, + imageMetadata.VerticalResolution); + + this.collector.Add(exifProfile.GetValue(ExifTag.ResolutionUnit).DeepClone()); + + IExifValue xResolution = exifProfile.GetValue(ExifTag.XResolution)?.DeepClone(); + IExifValue yResolution = exifProfile.GetValue(ExifTag.YResolution)?.DeepClone(); + + if (xResolution != null && yResolution != null) + { + this.collector.Add(xResolution); + this.collector.Add(yResolution); + } + } + + private void ProcessMetadata(ExifProfile exifProfile) + { + foreach (IExifValue entry in exifProfile.Values) + { + // todo: skip subIfd + if (entry.DataType == ExifDataType.Ifd) + { + continue; + } + + switch ((ExifTagValue)(ushort)entry.Tag) + { + case ExifTagValue.SubIFDOffset: + case ExifTagValue.GPSIFDOffset: + case ExifTagValue.SubIFDs: + case ExifTagValue.XMP: + case ExifTagValue.IPTC: + case ExifTagValue.IccProfile: + continue; + } + + switch (ExifTags.GetPart(entry.Tag)) + { + case ExifParts.ExifTags: + case ExifParts.GpsTags: + break; + + case ExifParts.IfdTags: + if (!IsPureMetadata(entry.Tag)) + { + continue; + } + + break; + } + + if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag)) + { + this.collector.AddOrReplace(entry.DeepClone()); + } + } + } + + private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, byte[] xmpProfile) + { + if (exifProfile != null && exifProfile.Parts != ExifParts.None) + { + foreach (IExifValue entry in exifProfile.Values) + { + if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null) + { + ExifParts entryPart = ExifTags.GetPart(entry.Tag); + if (entryPart != ExifParts.None && exifProfile.Parts.HasFlag(entryPart)) + { + this.collector.AddOrReplace(entry.DeepClone()); + } + } + } + } + else + { + exifProfile.RemoveValue(ExifTag.SubIFDOffset); + } + + if (imageMetadata.IptcProfile != null) + { + imageMetadata.IptcProfile.UpdateData(); + var iptc = new ExifByteArray(ExifTagValue.IPTC, ExifDataType.Byte) + { + Value = imageMetadata.IptcProfile.Data + }; + + this.collector.Add(iptc); + } + else + { + exifProfile.RemoveValue(ExifTag.IPTC); + } + + if (imageMetadata.IccProfile != null) + { + var icc = new ExifByteArray(ExifTagValue.IccProfile, ExifDataType.Undefined) + { + Value = imageMetadata.IccProfile.ToByteArray() + }; + + this.collector.Add(icc); + } + else + { + exifProfile.RemoveValue(ExifTag.IccProfile); + } + + TiffMetadata tiffMetadata = imageMetadata.GetTiffMetadata(); + if (xmpProfile != null) + { + var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte) + { + Value = xmpProfile + }; + + this.collector.Add(xmp); + } + else + { + exifProfile.RemoveValue(ExifTag.XMP); + } + } + } + + private class ImageFormatProcessor + { + private readonly TiffEncoderEntriesCollector collector; + + public ImageFormatProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + + public void Process(TiffEncoderCore encoder) + { + var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel) + { + Value = GetSamplesPerPixel(encoder) + }; + + ushort[] bitsPerSampleValue = GetBitsPerSampleValue(encoder); + var bitPerSample = new ExifShortArray(ExifTagValue.BitsPerSample) + { + Value = bitsPerSampleValue + }; + + ushort compressionType = GetCompressionType(encoder); + var compression = new ExifShort(ExifTagValue.Compression) + { + Value = compressionType + }; + + var photometricInterpretation = new ExifShort(ExifTagValue.PhotometricInterpretation) + { + Value = (ushort)encoder.PhotometricInterpretation + }; + + this.collector.AddOrReplace(samplesPerPixel); + this.collector.AddOrReplace(bitPerSample); + this.collector.AddOrReplace(compression); + this.collector.AddOrReplace(photometricInterpretation); + + if (encoder.HorizontalPredictor == TiffPredictor.Horizontal) + { + if (encoder.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb || + encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor || + encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero) + { + var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; + + this.collector.AddOrReplace(predictor); + } + } + } + + private static uint GetSamplesPerPixel(TiffEncoderCore encoder) + { + switch (encoder.PhotometricInterpretation) + { + case TiffPhotometricInterpretation.PaletteColor: + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + return 1; + case TiffPhotometricInterpretation.Rgb: + default: + return 3; + } + } + + private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder) + { + switch (encoder.PhotometricInterpretation) + { + case TiffPhotometricInterpretation.PaletteColor: + if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit4) + { + return TiffConstants.BitsPerSample4Bit; + } + else + { + return TiffConstants.BitsPerSample8Bit; + } + + case TiffPhotometricInterpretation.Rgb: + return TiffConstants.BitsPerSampleRgb8Bit; + + case TiffPhotometricInterpretation.WhiteIsZero: + if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit1) + { + return TiffConstants.BitsPerSample1Bit; + } + + return TiffConstants.BitsPerSample8Bit; + + case TiffPhotometricInterpretation.BlackIsZero: + if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit1) + { + return TiffConstants.BitsPerSample1Bit; + } + + return TiffConstants.BitsPerSample8Bit; + + default: + return TiffConstants.BitsPerSampleRgb8Bit; + } + } + + private static ushort GetCompressionType(TiffEncoderCore encoder) + { + switch (encoder.CompressionType) + { + case TiffCompression.Deflate: + // Deflate is allowed for all modes. + return (ushort)TiffCompression.Deflate; + case TiffCompression.PackBits: + // PackBits is allowed for all modes. + return (ushort)TiffCompression.PackBits; + case TiffCompression.Lzw: + if (encoder.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb || + encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor || + encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero) + { + return (ushort)TiffCompression.Lzw; + } + + break; + + case TiffCompression.CcittGroup3Fax: + return (ushort)TiffCompression.CcittGroup3Fax; + + case TiffCompression.Ccitt1D: + return (ushort)TiffCompression.Ccitt1D; + } + + return (ushort)TiffCompression.None; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffFormat.cs b/src/ImageSharp/Formats/Tiff/TiffFormat.cs new file mode 100644 index 0000000000..2217ffb7f7 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffFormat.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +using SixLabors.ImageSharp.Formats.Tiff.Constants; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Encapsulates the means to encode and decode Tiff images. + /// + public sealed class TiffFormat : IImageFormat + { + private TiffFormat() + { + } + + /// + /// Gets the current instance. + /// + public static TiffFormat Instance { get; } = new TiffFormat(); + + /// + public string Name => "TIFF"; + + /// + public string DefaultMimeType => "image/tiff"; + + /// + public IEnumerable MimeTypes => TiffConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => TiffConstants.FileExtensions; + + /// + public TiffMetadata CreateDefaultFormatMetadata() => new TiffMetadata(); + + /// + public TiffFrameMetadata CreateDefaultFormatFrameMetadata() => new TiffFrameMetadata(); + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs new file mode 100644 index 0000000000..25a0578e91 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Provides Tiff specific metadata information for the frame. + /// + public class TiffFrameMetadata : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public TiffFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The other tiff frame metadata. + private TiffFrameMetadata(TiffFrameMetadata other) + { + this.BitsPerPixel = other.BitsPerPixel; + this.Compression = other.Compression; + this.PhotometricInterpretation = other.PhotometricInterpretation; + this.Predictor = other.Predictor; + } + + /// + /// Gets or sets the bits per pixel. + /// + public TiffBitsPerPixel? BitsPerPixel { get; set; } + + /// + /// Gets or sets the compression scheme used on the image data. + /// + public TiffCompression? Compression { get; set; } + + /// + /// Gets or sets the color space of the image data. + /// + public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; } + + /// + /// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied. + /// + public TiffPredictor? Predictor { get; set; } + + /// + /// Returns a new instance parsed from the given Exif profile. + /// + /// The Exif profile containing tiff frame directory tags to parse. + /// If null, a new instance is created and parsed instead. + /// The . + internal static TiffFrameMetadata Parse(ExifProfile profile) + { + var meta = new TiffFrameMetadata(); + Parse(meta, profile); + return meta; + } + + /// + /// Parses the given Exif profile to populate the properties of the tiff frame meta data.. + /// + /// The tiff frame meta data. + /// The Exif profile containing tiff frame directory tags. + internal static void Parse(TiffFrameMetadata meta, ExifProfile profile) + { + if (profile != null) + { + ushort[] bitsPerSample = profile.GetValue(ExifTag.BitsPerSample)?.Value; + meta.BitsPerPixel = BitsPerPixelFromBitsPerSample(bitsPerSample); + meta.Compression = (TiffCompression?)profile.GetValue(ExifTag.Compression)?.Value; + meta.PhotometricInterpretation = + (TiffPhotometricInterpretation?)profile.GetValue(ExifTag.PhotometricInterpretation)?.Value; + meta.Predictor = (TiffPredictor?)profile.GetValue(ExifTag.Predictor)?.Value; + + profile.RemoveValue(ExifTag.BitsPerSample); + profile.RemoveValue(ExifTag.Compression); + profile.RemoveValue(ExifTag.PhotometricInterpretation); + profile.RemoveValue(ExifTag.Predictor); + } + } + + /// + /// Gets the bits per pixel for the given bits per sample. + /// + /// The tiff bits per sample. + /// Bits per pixel. + private static TiffBitsPerPixel? BitsPerPixelFromBitsPerSample(ushort[] bitsPerSample) + { + if (bitsPerSample == null) + { + return null; + } + + int bitsPerPixel = 0; + foreach (ushort bits in bitsPerSample) + { + bitsPerPixel += bits; + } + + return (TiffBitsPerPixel)bitsPerPixel; + } + + /// + public IDeepCloneable DeepClone() => new TiffFrameMetadata(this); + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs b/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs new file mode 100644 index 0000000000..f7e6f7a997 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Detects tiff file headers + /// + public sealed class TiffImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 4; + + /// + public IImageFormat DetectFormat(ReadOnlySpan header) + { + if (this.IsSupportedFileFormat(header)) + { + return TiffFormat.Instance; + } + + return null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + return header.Length >= this.HeaderSize && + ((header[0] == 0x49 && header[1] == 0x49 && header[2] == 0x2A && header[3] == 0x00) || // Little-endian + (header[0] == 0x4D && header[1] == 0x4D && header[2] == 0x00 && header[3] == 0x2A)); // Big-endian + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs new file mode 100644 index 0000000000..cf1ab37545 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Provides Tiff specific metadata information for the image. + /// + public class TiffMetadata : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public TiffMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private TiffMetadata(TiffMetadata other) => this.ByteOrder = other.ByteOrder; + + /// + /// Gets or sets the byte order. + /// + public ByteOrder ByteOrder { get; set; } + + /// + public IDeepCloneable DeepClone() => new TiffMetadata(this); + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs b/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs new file mode 100644 index 0000000000..3c541a786d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Cold path optimizations for throwing tiff format based exceptions. + /// + internal static class TiffThrowHelper + { + /// + /// Cold path optimization for throwing -s + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowImageFormatException(string errorMessage) => throw new ImageFormatException(errorMessage); + + [MethodImpl(InliningOptions.ColdPath)] + public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); + + [MethodImpl(InliningOptions.ColdPath)] + public static Exception NotSupportedCompressor(string compressionType) => throw new NotSupportedException($"Not supported encoder compression method: {compressionType}"); + + [MethodImpl(InliningOptions.ColdPath)] + public static Exception InvalidColorType(string colorType) => throw new NotSupportedException($"Invalid color type: {colorType}"); + + [MethodImpl(InliningOptions.ColdPath)] + public static Exception ThrowInvalidHeader() => throw new ImageFormatException("Invalid TIFF file header."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNotSupported(string message) => throw new NotSupportedException(message); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowArgumentException(string message) => throw new ArgumentException(message); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Utils/BitReader.cs b/src/ImageSharp/Formats/Tiff/Utils/BitReader.cs new file mode 100644 index 0000000000..40e67c1b0a --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Utils/BitReader.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Tiff.Utils +{ + /// + /// Utility class to read a sequence of bits from an array + /// + internal ref struct BitReader + { + private readonly ReadOnlySpan array; + private int offset; + private int bitOffset; + + /// + /// Initializes a new instance of the struct. + /// + /// The array to read data from. + public BitReader(ReadOnlySpan array) + { + this.array = array; + this.offset = 0; + this.bitOffset = 0; + } + + /// + /// Reads the specified number of bits from the array. + /// + /// The number of bits to read. + /// The value read from the array. + public int ReadBits(uint bits) + { + int value = 0; + + for (uint i = 0; i < bits; i++) + { + int bit = (this.array[this.offset] >> (7 - this.bitOffset)) & 0x01; + value = (value << 1) | bit; + + this.bitOffset++; + + if (this.bitOffset == 8) + { + this.bitOffset = 0; + this.offset++; + } + } + + return value; + } + + /// + /// Moves the reader to the next row of byte-aligned data. + /// + public void NextRow() + { + if (this.bitOffset > 0) + { + this.bitOffset = 0; + this.offset++; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs new file mode 100644 index 0000000000..232daa18d6 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers +{ + internal abstract class TiffBaseColorWriter : IDisposable + where TPixel : unmanaged, IPixel + { + private bool isDisposed; + + protected TiffBaseColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) + { + this.Image = image; + this.MemoryAllocator = memoryAllocator; + this.Configuration = configuration; + this.EntriesCollector = entriesCollector; + } + + /// + /// Gets the bits per pixel. + /// + public abstract int BitsPerPixel { get; } + + /// + /// Gets the bytes per row. + /// + public int BytesPerRow => ((this.Image.Width * this.BitsPerPixel) + 7) / 8; + + protected ImageFrame Image { get; } + + protected MemoryAllocator MemoryAllocator { get; } + + protected Configuration Configuration { get; } + + protected TiffEncoderEntriesCollector EntriesCollector { get; } + + public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip) + { + DebugGuard.IsTrue(this.BytesPerRow == compressor.BytesPerRow, "bytes per row of the compressor does not match tiff color writer"); + int stripsCount = (this.Image.Height + rowsPerStrip - 1) / rowsPerStrip; + + uint[] stripOffsets = new uint[stripsCount]; + uint[] stripByteCounts = new uint[stripsCount]; + + int stripIndex = 0; + compressor.Initialize(rowsPerStrip); + for (int y = 0; y < this.Image.Height; y += rowsPerStrip) + { + long offset = compressor.Output.Position; + + int height = Math.Min(rowsPerStrip, this.Image.Height - y); + this.EncodeStrip(y, height, compressor); + + long endOffset = compressor.Output.Position; + stripOffsets[stripIndex] = (uint)offset; + stripByteCounts[stripIndex] = (uint)(endOffset - offset); + stripIndex++; + } + + DebugGuard.IsTrue(stripIndex == stripsCount, "stripIndex and stripsCount should match"); + this.AddStripTags(rowsPerStrip, stripOffsets, stripByteCounts); + } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + this.Dispose(true); + } + + protected static Span GetStripPixels(Buffer2D buffer2D, int y, int height) + where T : struct + => buffer2D.GetSingleSpan().Slice(y * buffer2D.Width, height * buffer2D.Width); + + protected abstract void EncodeStrip(int y, int height, TiffBaseCompressor compressor); + + /// + /// Adds image format information to the specified IFD. + /// + /// The rows per strip. + /// The strip offsets. + /// The strip byte counts. + private void AddStripTags(int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts) + { + this.EntriesCollector.AddOrReplace(new ExifLong(ExifTagValue.RowsPerStrip) + { + Value = (uint)rowsPerStrip + }); + + this.EntriesCollector.AddOrReplace(new ExifLongArray(ExifTagValue.StripOffsets) + { + Value = stripOffsets + }); + + this.EntriesCollector.AddOrReplace(new ExifLongArray(ExifTagValue.StripByteCounts) + { + Value = stripByteCounts + }); + } + + protected abstract void Dispose(bool disposing); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs new file mode 100644 index 0000000000..be5c837eae --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; + +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers +{ + internal sealed class TiffBiColorWriter : TiffBaseColorWriter + where TPixel : unmanaged, IPixel + { + private readonly Image imageBlackWhite; + + private IMemoryOwner pixelsAsGray; + + private IMemoryOwner bitStrip; + + public TiffBiColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) + : base(image, memoryAllocator, configuration, entriesCollector) + { + // Convert image to black and white. + this.imageBlackWhite = new Image(configuration, new ImageMetadata(), new[] { image.Clone() }); + this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg)); + } + + /// + public override int BitsPerPixel => 1; + + /// + protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) + { + this.pixelsAsGray ??= this.MemoryAllocator.Allocate(height * this.Image.Width); + + Span pixelAsGraySpan = this.pixelsAsGray.Slice(0, height * this.Image.Width); + + Span pixelsBlackWhite = GetStripPixels(this.imageBlackWhite.GetRootFramePixelBuffer(), y, height); + + PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhite, pixelAsGraySpan, pixelsBlackWhite.Length); + + if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D) + { + // Special case for T4BitCompressor. + compressor.CompressStrip(pixelAsGraySpan, height); + } + else + { + // Write uncompressed image. + int bytesPerStrip = this.BytesPerRow * height; + this.bitStrip ??= this.MemoryAllocator.AllocateManagedByteBuffer(bytesPerStrip); + + Span rows = this.bitStrip.Slice(0, bytesPerStrip); + rows.Clear(); + + int grayPixelIndex = 0; + for (int s = 0; s < height; s++) + { + int bitIndex = 0; + int byteIndex = 0; + Span outputRow = rows.Slice(s * this.BytesPerRow); + for (int x = 0; x < this.Image.Width; x++) + { + int shift = 7 - bitIndex; + if (pixelAsGraySpan[grayPixelIndex++] == 255) + { + outputRow[byteIndex] |= (byte)(1 << shift); + } + + bitIndex++; + if (bitIndex == 8) + { + byteIndex++; + bitIndex = 0; + } + } + } + + compressor.CompressStrip(rows, height); + } + } + + /// + protected override void Dispose(bool disposing) + { + this.imageBlackWhite?.Dispose(); + this.pixelsAsGray?.Dispose(); + this.bitStrip?.Dispose(); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs new file mode 100644 index 0000000000..e53f4b420d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers +{ + internal static class TiffColorWriterFactory + { + public static TiffBaseColorWriter Create( + TiffPhotometricInterpretation? photometricInterpretation, + ImageFrame image, + IQuantizer quantizer, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector, + int bitsPerPixel) + where TPixel : unmanaged, IPixel + { + switch (photometricInterpretation) + { + case TiffPhotometricInterpretation.PaletteColor: + return new TiffPaletteWriter(image, quantizer, memoryAllocator, configuration, entriesCollector, bitsPerPixel); + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + if (bitsPerPixel == 1) + { + return new TiffBiColorWriter(image, memoryAllocator, configuration, entriesCollector); + } + + return new TiffGrayWriter(image, memoryAllocator, configuration, entriesCollector); + default: + return new TiffRgbWriter(image, memoryAllocator, configuration, entriesCollector); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs new file mode 100644 index 0000000000..4df57f7e85 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers +{ + /// + /// The base class for composite color types: 8-bit gray, 24-bit RGB (4-bit gray, 16-bit (565/555) RGB, 32-bit RGB, CMYK, YCbCr). + /// + internal abstract class TiffCompositeColorWriter : TiffBaseColorWriter + where TPixel : unmanaged, IPixel + { + private IManagedByteBuffer rowBuffer; + + protected TiffCompositeColorWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) + : base(image, memoryAllocator, configuration, entriesCollector) + { + } + + protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) + { + if (this.rowBuffer == null) + { + this.rowBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(this.BytesPerRow * height); + } + + this.rowBuffer.Clear(); + + Span rowSpan = this.rowBuffer.GetSpan().Slice(0, this.BytesPerRow * height); + + Span pixels = GetStripPixels(this.Image.PixelBuffer, y, height); + + this.EncodePixels(pixels, rowSpan); + compressor.CompressStrip(rowSpan, height); + } + + protected abstract void EncodePixels(Span pixels, Span buffer); + + /// + protected override void Dispose(bool disposing) => this.rowBuffer?.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs new file mode 100644 index 0000000000..117960ba7a --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers +{ + internal sealed class TiffGrayWriter : TiffCompositeColorWriter + where TPixel : unmanaged, IPixel + { + public TiffGrayWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) + : base(image, memoryAllocator, configuration, entriesCollector) + { + } + + /// + public override int BitsPerPixel => 8; + + /// + protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs new file mode 100644 index 0000000000..712578f81a --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs @@ -0,0 +1,127 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.InteropServices; + +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Tiff.Compression; +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.Writers +{ + internal sealed class TiffPaletteWriter : TiffBaseColorWriter + where TPixel : unmanaged, IPixel + { + private readonly int maxColors; + private readonly int colorPaletteSize; + private readonly int colorPaletteBytes; + private readonly IndexedImageFrame quantizedImage; + + public TiffPaletteWriter( + ImageFrame image, + IQuantizer quantizer, + MemoryAllocator memoryAllocator, + Configuration configuration, + TiffEncoderEntriesCollector entriesCollector, + int bitsPerPixel) + : base(image, memoryAllocator, configuration, entriesCollector) + { + DebugGuard.NotNull(quantizer, nameof(quantizer)); + DebugGuard.NotNull(configuration, nameof(configuration)); + DebugGuard.NotNull(entriesCollector, nameof(entriesCollector)); + DebugGuard.MustBeBetweenOrEqualTo(bitsPerPixel, 4, 8, nameof(bitsPerPixel)); + + this.BitsPerPixel = bitsPerPixel; + this.maxColors = this.BitsPerPixel == 4 ? 16 : 256; + this.colorPaletteSize = this.maxColors * 3; + this.colorPaletteBytes = this.colorPaletteSize * 2; + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.Configuration, new QuantizerOptions() + { + MaxColors = this.maxColors + }); + this.quantizedImage = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); + + this.AddColorMapTag(); + } + + /// + public override int BitsPerPixel { get; } + + /// + protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) + { + Span pixels = GetStripPixels(((IPixelSource)this.quantizedImage).PixelBuffer, y, height); + if (this.BitsPerPixel == 4) + { + using IMemoryOwner rows4bitBuffer = this.MemoryAllocator.Allocate(pixels.Length / 2); + Span rows4bit = rows4bitBuffer.GetSpan(); + int idx = 0; + for (int i = 0; i < rows4bit.Length; i++) + { + rows4bit[i] = (byte)((pixels[idx] << 4) | (pixels[idx + 1] & 0xF)); + idx += 2; + } + + compressor.CompressStrip(rows4bit, height); + } + else + { + compressor.CompressStrip(pixels, height); + } + } + + /// + protected override void Dispose(bool disposing) => this.quantizedImage?.Dispose(); + + private void AddColorMapTag() + { + using IMemoryOwner colorPaletteBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(this.colorPaletteBytes); + Span colorPalette = colorPaletteBuffer.GetSpan(); + + ReadOnlySpan quantizedColors = this.quantizedImage.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); + + // It can happen that the quantized colors are less than the expected maximum per channel. + var diffToMaxColors = this.maxColors - quantizedColors.Length; + + // 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[this.colorPaletteSize]; + int paletteIdx = 0; + for (int i = 0; i < quantizedColors.Length; i++) + { + palette[paletteIdx++] = quantizedColorRgb48[i].R; + } + + paletteIdx += diffToMaxColors; + + for (int i = 0; i < quantizedColors.Length; i++) + { + palette[paletteIdx++] = quantizedColorRgb48[i].G; + } + + paletteIdx += diffToMaxColors; + + for (int i = 0; i < quantizedColors.Length; i++) + { + palette[paletteIdx++] = quantizedColorRgb48[i].B; + } + + var colorMap = new ExifShortArray(ExifTagValue.ColorMap) + { + Value = palette + }; + + this.EntriesCollector.AddOrReplace(colorMap); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs new file mode 100644 index 0000000000..a3050f8a2f --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers +{ + internal sealed class TiffRgbWriter : TiffCompositeColorWriter + where TPixel : unmanaged, IPixel + { + public TiffRgbWriter(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) + : base(image, memoryAllocator, configuration, entriesCollector) + { + } + + /// + public override int BitsPerPixel => 24; + + /// + protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs new file mode 100644 index 0000000000..05a1ca7a24 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff.Writers +{ + /// + /// Utility class for writing TIFF data to a . + /// + internal sealed class TiffStreamWriter : IDisposable + { + private static readonly byte[] PaddingBytes = new byte[4]; + + /// + /// A scratch buffer to reduce allocations. + /// + private readonly byte[] buffer = new byte[4]; + + /// + /// Initializes a new instance of the class. + /// + /// The output stream. + public TiffStreamWriter(Stream output) => this.BaseStream = output; + + /// + /// Gets a value indicating whether the architecture is little-endian. + /// + public bool IsLittleEndian => BitConverter.IsLittleEndian; + + /// + /// Gets the current position within the stream. + /// + public long Position => this.BaseStream.Position; + + /// + /// Gets the base stream. + /// + public Stream BaseStream { get; } + + /// + /// Writes an empty four bytes to the stream, returning the offset to be written later. + /// + /// The offset to be written later. + public long PlaceMarker() + { + long offset = this.BaseStream.Position; + this.Write(0u); + return offset; + } + + /// + /// Writes an array of bytes to the current stream. + /// + /// The bytes to write. + public void Write(byte[] value) => this.BaseStream.Write(value, 0, value.Length); + + /// + /// Writes the specified value. + /// + /// The bytes to write. + public void Write(ReadOnlySpan value) => this.BaseStream.Write(value); + + /// + /// Writes a byte to the current stream. + /// + /// The byte to write. + public void Write(byte value) => this.BaseStream.WriteByte(value); + + /// + /// Writes a two-byte unsigned integer to the current stream. + /// + /// The two-byte unsigned integer to write. + public void Write(ushort value) + { + if (this.IsLittleEndian) + { + BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, value); + } + else + { + BinaryPrimitives.WriteUInt16BigEndian(this.buffer, value); + } + + this.BaseStream.Write(this.buffer.AsSpan(0, 2)); + } + + /// + /// Writes a four-byte unsigned integer to the current stream. + /// + /// The four-byte unsigned integer to write. + public void Write(uint value) + { + if (this.IsLittleEndian) + { + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, value); + } + else + { + BinaryPrimitives.WriteUInt32BigEndian(this.buffer, value); + } + + this.BaseStream.Write(this.buffer.AsSpan(0, 4)); + } + + /// + /// Writes an array of bytes to the current stream, padded to four-bytes. + /// + /// The bytes to write. + public void WritePadded(Span value) + { + this.BaseStream.Write(value); + + if (value.Length % 4 != 0) + { + this.BaseStream.Write(PaddingBytes, 0, 4 - (value.Length % 4)); + } + } + + /// + /// Writes a four-byte unsigned integer to the specified marker in the stream. + /// + /// The offset returned when placing the marker + /// The four-byte unsigned integer to write. + public void WriteMarker(long offset, uint value) + { + long currentOffset = this.BaseStream.Position; + this.BaseStream.Seek(offset, SeekOrigin.Begin); + this.Write(value); + this.BaseStream.Seek(currentOffset, SeekOrigin.Begin); + } + + /// + /// Disposes instance, ensuring any unwritten data is flushed. + /// + public void Dispose() => this.BaseStream.Flush(); + } +} diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index 2021f1249b..1819fd2bc5 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -1,8 +1,12 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; namespace SixLabors.ImageSharp.Metadata { @@ -35,8 +39,34 @@ internal ImageFrameMetadata(ImageFrameMetadata other) { this.formatMetadata.Add(meta.Key, meta.Value.DeepClone()); } + + this.ExifProfile = other.ExifProfile?.DeepClone(); + this.IccProfile = other.IccProfile?.DeepClone(); + this.IptcProfile = other.IptcProfile?.DeepClone(); + this.XmpProfile = other.XmpProfile != null ? new byte[other.XmpProfile.Length] : null; + other.XmpProfile?.AsSpan().CopyTo(this.XmpProfile.AsSpan()); } + /// + /// Gets or sets the Exif profile. + /// + public ExifProfile ExifProfile { get; set; } + + /// + /// Gets or sets the XMP profile. + /// + internal byte[] XmpProfile { get; set; } + + /// + /// Gets or sets the list of ICC profiles. + /// + public IccProfile IccProfile { get; set; } + + /// + /// Gets or sets the iptc profile. + /// + public IptcProfile IptcProfile { get; set; } + /// public ImageFrameMetadata DeepClone() => new ImageFrameMetadata(this); @@ -63,4 +93,4 @@ public TFormatFrameMetadata GetFormatMetadata /// A 64-bit double precision floating point value. /// - DoubleFloat = 12 + DoubleFloat = 12, + + /// + /// Reference to an IFD (32-bit (4-byte) unsigned integer). + /// + Ifd = 13 } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs index dc12f3819b..0a9c879ce6 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs @@ -24,12 +24,12 @@ public enum ExifParts /// /// ExifTags /// - ExifTags = 4, + ExifTags = 2, /// /// GPSTags /// - GpsTags = 8, + GpsTags = 4, /// /// All diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index 55af45fb46..9265314edb 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; + using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif @@ -52,6 +53,18 @@ public ExifProfile(byte[] data) this.InvalidTags = Array.Empty(); } + /// + /// Initializes a new instance of the class. + /// + /// The values. + /// The invalid tags. + internal ExifProfile(List values, IReadOnlyList invalidTags) + { + this.Parts = ExifParts.All; + this.values = values; + this.InvalidTags = invalidTags; + } + /// /// Initializes a new instance of the class /// by making a copy from another EXIF profile. @@ -154,7 +167,7 @@ public IExifValue GetValue(ExifTag tag) /// /// The tag of the EXIF value. /// - /// The . + /// True, if the value was removed, otherwise false. /// public bool RemoveValue(ExifTag tag) { diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 749c691865..6e671b3eca 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -5,27 +5,100 @@ using System.Buffers.Binary; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; using System.Runtime.CompilerServices; using System.Text; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { + internal class ExifReader : BaseExifReader + { + private readonly List loaders = new List(); + + public ExifReader(byte[] exifData) + : base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData)))) + { + } + + /// + /// Reads and returns the collection of EXIF values. + /// + /// + /// The . + /// + public List ReadValues() + { + var values = new List(); + + // II == 0x4949 + this.IsBigEndian = this.ReadUInt16() != 0x4949; + + if (this.ReadUInt16() != 0x002A) + { + return values; + } + + uint ifdOffset = this.ReadUInt32(); + this.ReadValues(values, ifdOffset); + + uint thumbnailOffset = this.ReadUInt32(); + this.GetThumbnail(thumbnailOffset); + + this.ReadSubIfd(values); + + foreach (Action loader in this.loaders) + { + loader(); + } + + return values; + } + + protected override void RegisterExtLoader(uint offset, Action loader) => this.loaders.Add(loader); + + private void GetThumbnail(uint offset) + { + if (offset == 0) + { + return; + } + + var values = new List(); + this.ReadValues(values, offset); + + foreach (ExifValue value in values) + { + if (value == ExifTag.JPEGInterchangeFormat) + { + this.ThumbnailOffset = ((ExifLong)value).Value; + } + else if (value == ExifTag.JPEGInterchangeFormatLength) + { + this.ThumbnailLength = ((ExifLong)value).Value; + } + } + } + } + /// - /// Reads and parses EXIF data from a byte array. + /// Reads and parses EXIF data from a stream. /// - internal sealed class ExifReader + internal abstract class BaseExifReader { + private readonly byte[] offsetBuffer = new byte[4]; + private readonly byte[] buf4 = new byte[4]; + private readonly byte[] buf2 = new byte[2]; + + private readonly Stream data; private List invalidTags; - private readonly byte[] exifData; - private int position; - private bool isBigEndian; + private uint exifOffset; + private uint gpsOffset; - public ExifReader(byte[] exifData) - { - this.exifData = exifData ?? throw new ArgumentNullException(nameof(exifData)); - } + protected BaseExifReader(Stream stream) => + this.data = stream ?? throw new ArgumentNullException(nameof(stream)); private delegate TDataType ConverterMethod(ReadOnlySpan data); @@ -35,66 +108,51 @@ public ExifReader(byte[] exifData) public IReadOnlyList InvalidTags => this.invalidTags ?? (IReadOnlyList)Array.Empty(); /// - /// Gets the thumbnail length in the byte stream. + /// Gets or sets the thumbnail length in the byte stream. /// - public uint ThumbnailLength { get; private set; } + public uint ThumbnailLength { get; protected set; } /// - /// Gets the thumbnail offset position in the byte stream. + /// Gets or sets the thumbnail offset position in the byte stream. /// - public uint ThumbnailOffset { get; private set; } + public uint ThumbnailOffset { get; protected set; } - /// - /// Gets the remaining length. - /// - private int RemainingLength - { - get - { - if (this.position >= this.exifData.Length) - { - return 0; - } + public bool IsBigEndian { get; protected set; } - return this.exifData.Length - this.position; - } - } + protected abstract void RegisterExtLoader(uint offset, Action loader); /// - /// Reads and returns the collection of EXIF values. + /// Reads the values to the values collection. /// - /// - /// The . - /// - public List ReadValues() + /// The values. + /// The IFD offset. + protected void ReadValues(List values, uint offset) { - var values = new List(); - - // II == 0x4949 - this.isBigEndian = this.ReadUInt16() != 0x4949; - - if (this.ReadUInt16() != 0x002A) + if (offset > this.data.Length) { - return values; + return; } - uint ifdOffset = this.ReadUInt32(); - this.AddValues(values, ifdOffset); + this.Seek(offset); + int count = this.ReadUInt16(); - uint thumbnailOffset = this.ReadUInt32(); - this.GetThumbnail(thumbnailOffset); + for (int i = 0; i < count; i++) + { + this.ReadValue(values); + } + } + protected void ReadSubIfd(List values) + { if (this.exifOffset != 0) { - this.AddValues(values, this.exifOffset); + this.ReadValues(values, this.exifOffset); } if (this.gpsOffset != 0) { - this.AddValues(values, this.gpsOffset); + this.ReadValues(values, this.gpsOffset); } - - return values; } private static TDataType[] ToArray(ExifDataType dataType, ReadOnlySpan data, ConverterMethod converter) @@ -128,58 +186,6 @@ private string ConvertToString(ReadOnlySpan buffer) return Encoding.UTF8.GetString(buffer); } - /// - /// Adds the collection of EXIF values to the reader. - /// - /// The values. - /// The index. - private void AddValues(List values, uint index) - { - if (index > (uint)this.exifData.Length) - { - return; - } - - this.position = (int)index; - int count = this.ReadUInt16(); - - for (int i = 0; i < count; i++) - { - if (!this.TryReadValue(out ExifValue value)) - { - continue; - } - - bool duplicate = false; - foreach (IExifValue val in values) - { - if (val == value) - { - duplicate = true; - break; - } - } - - if (duplicate) - { - continue; - } - - if (value == ExifTag.SubIFDOffset) - { - this.exifOffset = ((ExifLong)value).Value; - } - else if (value == ExifTag.GPSIFDOffset) - { - this.gpsOffset = ((ExifLong)value).Value; - } - else - { - values.Add(value); - } - } - } - private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, uint numberOfComponents) { if (buffer.Length == 0) @@ -275,28 +281,28 @@ private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, ui } } - private bool TryReadValue(out ExifValue exifValue) + private void ReadValue(List values) { - exifValue = default; - // 2 | 2 | 4 | 4 // tag | type | count | value offset - if (this.RemainingLength < 12) + if ((this.data.Length - this.data.Position) < 12) { - return false; + return; } var tag = (ExifTagValue)this.ReadUInt16(); ExifDataType dataType = EnumUtils.Parse(this.ReadUInt16(), ExifDataType.Unknown); + uint numberOfComponents = this.ReadUInt32(); + + this.TryReadSpan(this.offsetBuffer); + // Ensure that the data type is valid if (dataType == ExifDataType.Unknown) { - return false; + return; } - uint numberOfComponents = this.ReadUInt32(); - // Issue #132: ExifDataType == Undefined is treated like a byte array. // If numberOfComponents == 0 this value can only be handled as an inline value and must fallback to 4 (bytes) if (dataType == ExifDataType.Undefined && numberOfComponents == 0) @@ -304,110 +310,103 @@ private bool TryReadValue(out ExifValue exifValue) numberOfComponents = 4; } - uint size = numberOfComponents * ExifDataTypes.GetSize(dataType); + ExifValue exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); - this.TryReadSpan(4, out ReadOnlySpan offsetBuffer); + if (exifValue is null) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } - object value; + uint size = numberOfComponents * ExifDataTypes.GetSize(dataType); if (size > 4) { - int oldIndex = this.position; - uint newIndex = this.ConvertToUInt32(offsetBuffer); + uint newOffset = this.ConvertToUInt32(this.offsetBuffer); - // Ensure that the new index does not overrun the data - if (newIndex > int.MaxValue) + // Ensure that the new index does not overrun the data. + if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length) { this.AddInvalidTag(new UnkownExifTag(tag)); - return false; + return; } - this.position = (int)newIndex; - - if (this.RemainingLength < size) + this.RegisterExtLoader(newOffset, () => { - this.AddInvalidTag(new UnkownExifTag(tag)); - - this.position = oldIndex; - return false; - } - - this.TryReadSpan((int)size, out ReadOnlySpan dataBuffer); - - value = this.ConvertValue(dataType, dataBuffer, numberOfComponents); - this.position = oldIndex; + byte[] dataBuffer = new byte[size]; + this.Seek(newOffset); + if (this.TryReadSpan(dataBuffer)) + { + object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents); + this.Add(values, exifValue, value); + } + }); } else { - value = this.ConvertValue(dataType, offsetBuffer, numberOfComponents); - } + object value = this.ConvertValue(dataType, this.offsetBuffer, numberOfComponents); - exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); + this.Add(values, exifValue, value); + } + } - if (exifValue is null) + private void Add(IList values, IExifValue exif, object value) + { + if (!exif.TrySetValue(value)) { - this.AddInvalidTag(new UnkownExifTag(tag)); - return false; + return; } - if (!exifValue.TrySetValue(value)) + foreach (IExifValue val in values) { - return false; + // Sometimes duplicates appear, can compare val.Tag == exif.Tag + if (val == exif) + { + Debug.WriteLine($"Duplicate Exif tag: tag={exif.Tag}, dataType={exif.DataType}"); + return; + } } - return true; + if (exif.Tag == ExifTag.SubIFDOffset) + { + this.exifOffset = (uint)value; + } + else if (exif.Tag == ExifTag.GPSIFDOffset) + { + this.gpsOffset = (uint)value; + } + else + { + values.Add(exif); + } } private void AddInvalidTag(ExifTag tag) - => (this.invalidTags ?? (this.invalidTags = new List())).Add(tag); + => (this.invalidTags ??= new List()).Add(tag); + + private void Seek(long pos) + => this.data.Seek(pos, SeekOrigin.Begin); - private bool TryReadSpan(int length, out ReadOnlySpan span) + private bool TryReadSpan(Span span) { - if (this.RemainingLength < length) + int length = span.Length; + if ((this.data.Length - this.data.Position) < length) { - span = default; - return false; } - span = new ReadOnlySpan(this.exifData, this.position, length); - - this.position += length; - - return true; + int read = this.data.Read(span); + return read == length; } - private uint ReadUInt32() - { - // Known as Long in Exif Specification - return this.TryReadSpan(4, out ReadOnlySpan span) - ? this.ConvertToUInt32(span) + // Known as Long in Exif Specification. + protected uint ReadUInt32() => + this.TryReadSpan(this.buf4) + ? this.ConvertToUInt32(this.buf4) : default; - } - private ushort ReadUInt16() - { - return this.TryReadSpan(2, out ReadOnlySpan span) - ? this.ConvertToShort(span) + protected ushort ReadUInt16() => this.TryReadSpan(this.buf2) + ? this.ConvertToShort(this.buf2) : default; - } - - private void GetThumbnail(uint offset) - { - var values = new List(); - this.AddValues(values, offset); - - foreach (ExifValue value in values) - { - if (value == ExifTag.JPEGInterchangeFormat) - { - this.ThumbnailOffset = ((ExifLong)value).Value; - } - else if (value == ExifTag.JPEGInterchangeFormatLength) - { - this.ThumbnailLength = ((ExifLong)value).Value; - } - } - } private double ConvertToDouble(ReadOnlySpan buffer) { @@ -416,7 +415,7 @@ private double ConvertToDouble(ReadOnlySpan buffer) return default; } - long intValue = this.isBigEndian + long intValue = this.IsBigEndian ? BinaryPrimitives.ReadInt64BigEndian(buffer) : BinaryPrimitives.ReadInt64LittleEndian(buffer); @@ -425,13 +424,13 @@ private double ConvertToDouble(ReadOnlySpan buffer) private uint ConvertToUInt32(ReadOnlySpan buffer) { - // Known as Long in Exif Specification + // Known as Long in Exif Specification. if (buffer.Length < 4) { return default; } - return this.isBigEndian + return this.IsBigEndian ? BinaryPrimitives.ReadUInt32BigEndian(buffer) : BinaryPrimitives.ReadUInt32LittleEndian(buffer); } @@ -443,7 +442,7 @@ private ushort ConvertToShort(ReadOnlySpan buffer) return default; } - return this.isBigEndian + return this.IsBigEndian ? BinaryPrimitives.ReadUInt16BigEndian(buffer) : BinaryPrimitives.ReadUInt16LittleEndian(buffer); } @@ -455,7 +454,7 @@ private float ConvertToSingle(ReadOnlySpan buffer) return default; } - int intValue = this.isBigEndian + int intValue = this.IsBigEndian ? BinaryPrimitives.ReadInt32BigEndian(buffer) : BinaryPrimitives.ReadInt32LittleEndian(buffer); @@ -484,7 +483,7 @@ private int ConvertToInt32(ReadOnlySpan buffer) // SignedLong in Exif Spec return default; } - return this.isBigEndian + return this.IsBigEndian ? BinaryPrimitives.ReadInt32BigEndian(buffer) : BinaryPrimitives.ReadInt32LittleEndian(buffer); } @@ -509,7 +508,7 @@ private short ConvertToSignedShort(ReadOnlySpan buffer) return default; } - return this.isBigEndian + return this.IsBigEndian ? BinaryPrimitives.ReadInt16BigEndian(buffer) : BinaryPrimitives.ReadInt16LittleEndian(buffer); } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index a240c13925..e7a01b070f 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -260,9 +260,9 @@ private uint GetLength(IList values) return length; } - private static uint GetLength(IExifValue value) => GetNumberOfComponents(value) * ExifDataTypes.GetSize(value.DataType); + internal static uint GetLength(IExifValue value) => GetNumberOfComponents(value) * ExifDataTypes.GetSize(value.DataType); - private static uint GetNumberOfComponents(IExifValue exifValue) + internal static uint GetNumberOfComponents(IExifValue exifValue) { object value = exifValue.GetValue(); @@ -279,17 +279,17 @@ private static uint GetNumberOfComponents(IExifValue exifValue) return 1; } - private int WriteArray(IExifValue value, Span destination, int offset) + private static int WriteArray(IExifValue value, Span destination, int offset) { if (value.DataType == ExifDataType.Ascii) { - return this.WriteValue(ExifDataType.Ascii, value.GetValue(), destination, offset); + return WriteValue(ExifDataType.Ascii, value.GetValue(), destination, offset); } int newOffset = offset; foreach (object obj in (Array)value.GetValue()) { - newOffset = this.WriteValue(value.DataType, obj, destination, newOffset); + newOffset = WriteValue(value.DataType, obj, destination, newOffset); } return newOffset; @@ -310,7 +310,7 @@ private int WriteData(uint startIndex, List values, Span desti if (GetLength(value) > 4) { WriteUInt32((uint)(newOffset - startIndex), destination, this.dataOffsets[i++]); - newOffset = this.WriteValue(value, destination, newOffset); + newOffset = WriteValue(value, destination, newOffset); } } @@ -341,7 +341,7 @@ private int WriteHeaders(List values, Span destination, int of } else { - this.WriteValue(value, destination, newOffset); + WriteValue(value, destination, newOffset); } newOffset += 4; @@ -362,7 +362,7 @@ private static void WriteSignedRational(Span destination, in SignedRationa BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(4, 4), value.Denominator); } - private int WriteValue(ExifDataType dataType, object value, Span destination, int offset) + private static int WriteValue(ExifDataType dataType, object value, Span destination, int offset) { switch (dataType) { @@ -410,14 +410,14 @@ private int WriteValue(ExifDataType dataType, object value, Span destinati } } - private int WriteValue(IExifValue value, Span destination, int offset) + internal static int WriteValue(IExifValue value, Span destination, int offset) { if (value.IsArray && value.DataType != ExifDataType.Ascii) { - return this.WriteArray(value, destination, offset); + return WriteArray(value, destination, offset); } - return this.WriteValue(value.DataType, value.GetValue(), destination, offset); + return WriteValue(value.DataType, value.GetValue(), destination, offset); } } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs index e20867b43e..fdde66c513 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs @@ -21,6 +21,16 @@ public abstract partial class ExifTag /// public static ExifTag XMP => new ExifTag(ExifTagValue.XMP); + /// + /// Gets the IPTC exif tag. + /// + public static ExifTag IPTC => new ExifTag(ExifTagValue.IPTC); + + /// + /// Gets the IccProfile exif tag. + /// + public static ExifTag IccProfile => new ExifTag(ExifTagValue.IccProfile); + /// /// Gets the CFAPattern2 exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs index 7e73b75aad..680136b039 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs @@ -16,6 +16,11 @@ public abstract partial class ExifTag /// public static ExifTag ImageLength { get; } = new ExifTag(ExifTagValue.ImageLength); + /// + /// Gets the RowsPerStrip exif tag. + /// + public static ExifTag RowsPerStrip { get; } = new ExifTag(ExifTagValue.RowsPerStrip); + /// /// Gets the TileWidth exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs index e9440119ef..e4ea6d0de6 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs @@ -11,6 +11,11 @@ public abstract partial class ExifTag /// public static ExifTag StripOffsets { get; } = new ExifTag(ExifTagValue.StripOffsets); + /// + /// Gets the StripByteCounts exif tag. + /// + public static ExifTag StripByteCounts { get; } = new ExifTag(ExifTagValue.StripByteCounts); + /// /// Gets the TileByteCounts exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs index f520455311..d27c26ee51 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs @@ -56,6 +56,11 @@ public abstract partial class ExifTag /// public static ExifTag PlanarConfiguration { get; } = new ExifTag(ExifTagValue.PlanarConfiguration); + /// + /// Gets the Predictor exif tag. + /// + public static ExifTag Predictor { get; } = new ExifTag(ExifTagValue.Predictor); + /// /// Gets the GrayResponseUnit exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs index ce0bb36f0d..6ed9131c83 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs @@ -46,11 +46,6 @@ public abstract partial class ExifTag /// public static ExifTag TransferFunction { get; } = new ExifTag(ExifTagValue.TransferFunction); - /// - /// Gets the Predictor exif tag. - /// - public static ExifTag Predictor { get; } = new ExifTag(ExifTagValue.Predictor); - /// /// Gets the HalftoneHints exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs index e07a32598d..3d13a82dcc 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs @@ -24,7 +24,16 @@ internal enum ExifTagValue GPSIFDOffset = 0x8825, /// - /// SubfileType + /// Indicates the identification of the Interoperability rule. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/interoperability/interoperabilityindex.html + /// + [ExifTagDescription("R98", "Indicates a file conforming to R98 file specification of Recommended Exif Interoperability Rules (ExifR98) or to DCF basic file stipulated by Design Rule for Camera File System.")] + [ExifTagDescription("THM", "Indicates a file conforming to DCF thumbnail file stipulated by Design rule for Camera File System.")] + InteroperabilityIndex = 0x0001, + + /// + /// A general indication of the kind of data contained in this subfile. + /// See Section 8: Baseline Fields. /// [ExifTagDescription(0U, "Full-resolution Image")] [ExifTagDescription(1U, "Reduced-resolution image")] @@ -38,7 +47,8 @@ internal enum ExifTagValue SubfileType = 0x00FE, /// - /// OldSubfileType + /// A general indication of the kind of data contained in this subfile. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)1, "Full-resolution Image")] [ExifTagDescription((ushort)2, "Reduced-resolution image")] @@ -46,22 +56,26 @@ internal enum ExifTagValue OldSubfileType = 0x00FF, /// - /// ImageWidth + /// The number of columns in the image, i.e., the number of pixels per row. + /// See Section 8: Baseline Fields. /// ImageWidth = 0x0100, /// - /// ImageLength + /// The number of rows of pixels in the image. + /// See Section 8: Baseline Fields. /// ImageLength = 0x0101, /// - /// BitsPerSample + /// Number of bits per component. + /// See Section 8: Baseline Fields. /// BitsPerSample = 0x0102, /// - /// Compression + /// Compression scheme used on the image data. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)1, "Uncompressed")] [ExifTagDescription((ushort)2, "CCITT 1D")] @@ -107,7 +121,8 @@ internal enum ExifTagValue Compression = 0x0103, /// - /// PhotometricInterpretation + /// The color space of the image data. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)0, "WhiteIsZero")] [ExifTagDescription((ushort)1, "BlackIsZero")] @@ -126,7 +141,8 @@ internal enum ExifTagValue PhotometricInterpretation = 0x0106, /// - /// Thresholding + /// For black and white TIFF files that represent shades of gray, the technique used to convert from gray to black and white pixels. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)1, "No dithering or halftoning")] [ExifTagDescription((ushort)2, "Ordered dither or halftone")] @@ -134,49 +150,58 @@ internal enum ExifTagValue Thresholding = 0x0107, /// - /// CellWidth + /// The width of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file. + /// See Section 8: Baseline Fields. /// CellWidth = 0x0108, /// - /// CellLength + /// The length of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file. + /// See Section 8: Baseline Fields. /// CellLength = 0x0109, /// - /// FillOrder + /// The logical order of bits within a byte. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)1, "Normal")] [ExifTagDescription((ushort)2, "Reversed")] FillOrder = 0x010A, /// - /// DocumentName + /// The name of the document from which this image was scanned. + /// See Section 12: Document Storage and Retrieval. /// DocumentName = 0x010D, /// - /// ImageDescription + /// A string that describes the subject of the image. + /// See Section 8: Baseline Fields. /// ImageDescription = 0x010E, /// - /// Make + /// The scanner manufacturer. + /// See Section 8: Baseline Fields. /// Make = 0x010F, /// - /// Model + /// The scanner model name or number. + /// See Section 8: Baseline Fields. /// Model = 0x0110, /// - /// StripOffsets + /// For each strip, the byte offset of that strip. + /// See Section 8: Baseline Fields. /// StripOffsets = 0x0111, /// - /// Orientation + /// The orientation of the image with respect to the rows and columns. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)1, "Horizontal (normal)")] [ExifTagDescription((ushort)2, "Mirror horizontal")] @@ -189,74 +214,88 @@ internal enum ExifTagValue Orientation = 0x0112, /// - /// SamplesPerPixel + /// The number of components per pixel. + /// See Section 8: Baseline Fields. /// SamplesPerPixel = 0x0115, /// - /// RowsPerStrip + /// The number of rows per strip. + /// See Section 8: Baseline Fields. /// RowsPerStrip = 0x0116, /// - /// StripByteCounts + /// For each strip, the number of bytes in the strip after compression. + /// See Section 8: Baseline Fields. /// StripByteCounts = 0x0117, /// - /// MinSampleValue + /// The minimum component value used. + /// See Section 8: Baseline Fields. /// MinSampleValue = 0x0118, /// - /// MaxSampleValue + /// The maximum component value used. + /// See Section 8: Baseline Fields. /// MaxSampleValue = 0x0119, /// - /// XResolution + /// The number of pixels per ResolutionUnit in the ImageWidth direction. + /// See Section 8: Baseline Fields. /// XResolution = 0x011A, /// - /// YResolution + /// The number of pixels per ResolutionUnit in the direction. + /// See Section 8: Baseline Fields. /// YResolution = 0x011B, /// - /// PlanarConfiguration + /// How the components of each pixel are stored. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)1, "Chunky")] [ExifTagDescription((ushort)2, "Planar")] PlanarConfiguration = 0x011C, /// - /// PageName + /// The name of the page from which this image was scanned. + /// See Section 12: Document Storage and Retrieval. /// PageName = 0x011D, /// - /// XPosition + /// X position of the image. + /// See Section 12: Document Storage and Retrieval. /// XPosition = 0x011E, /// - /// YPosition + /// Y position of the image. + /// See Section 12: Document Storage and Retrieval. /// YPosition = 0x011F, /// - /// FreeOffsets + /// For each string of contiguous unused bytes in a TIFF file, the byte offset of the string. + /// See Section 8: Baseline Fields. /// FreeOffsets = 0x0120, /// - /// FreeByteCounts + /// For each string of contiguous unused bytes in a TIFF file, the number of bytes in the string. + /// See Section 8: Baseline Fields. /// FreeByteCounts = 0x0121, /// - /// GrayResponseUnit + /// The precision of the information contained in the GrayResponseCurve. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)1, "0.1")] [ExifTagDescription((ushort)2, "0.001")] @@ -266,12 +305,13 @@ internal enum ExifTagValue GrayResponseUnit = 0x0122, /// - /// GrayResponseCurve + /// For grayscale data, the optical density of each possible pixel value. + /// See Section 8: Baseline Fields. /// GrayResponseCurve = 0x0123, /// - /// T4Options + /// Options for Group 3 Fax compression. /// [ExifTagDescription(0U, "2-Dimensional encoding")] [ExifTagDescription(1U, "Uncompressed")] @@ -279,13 +319,14 @@ internal enum ExifTagValue T4Options = 0x0124, /// - /// T6Options + /// Options for Group 4 Fax compression. /// [ExifTagDescription(1U, "Uncompressed")] T6Options = 0x0125, /// - /// ResolutionUnit + /// The unit of measurement for XResolution and YResolution. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)1, "None")] [ExifTagDescription((ushort)2, "Inches")] @@ -293,7 +334,8 @@ internal enum ExifTagValue ResolutionUnit = 0x0128, /// - /// PageNumber + /// The page number of the page from which this image was scanned. + /// See Section 12: Document Storage and Retrieval. /// PageNumber = 0x0129, @@ -308,22 +350,26 @@ internal enum ExifTagValue TransferFunction = 0x012D, /// - /// Software + /// Name and version number of the software package(s) used to create the image. + /// See Section 8: Baseline Fields. /// Software = 0x0131, /// - /// DateTime + /// Date and time of image creation. + /// See Section 8: Baseline Fields. /// DateTime = 0x0132, /// - /// Artist + /// Person who created the image. + /// See Section 8: Baseline Fields. /// Artist = 0x013B, /// - /// HostComputer + /// The computer and/or operating system in use at the time of image creation. + /// See Section 8: Baseline Fields. /// HostComputer = 0x013C, @@ -343,7 +389,8 @@ internal enum ExifTagValue PrimaryChromaticities = 0x013F, /// - /// ColorMap + /// A color map for palette color images. + /// See Section 8: Baseline Fields. /// ColorMap = 0x0140, @@ -390,6 +437,14 @@ internal enum ExifTagValue /// ConsecutiveBadFaxLines = 0x0148, + /// + /// Offset to child IFDs. + /// See TIFF Supplement 1: Adobe Pagemaker 6.0. + /// Each value is an offset (from the beginning of the TIFF file, as always) to a child IFD. Child images provide extra information for the parent image - such as a subsampled version of the parent image. + /// TIFF data type is Long or 13, IFD. The IFD type is identical to LONG, except that it is only used to point to other valid IFDs. + /// + SubIFDs = 0x014A, + /// /// InkSet /// @@ -418,7 +473,8 @@ internal enum ExifTagValue TargetPrinter = 0x0151, /// - /// ExtraSamples + /// Description of extra components. + /// See Section 8: Baseline Fields. /// [ExifTagDescription((ushort)0, "Unspecified")] [ExifTagDescription((ushort)1, "Associated Alpha")] @@ -485,6 +541,14 @@ internal enum ExifTagValue [ExifTagDescription((ushort)1, "Higher resolution image exists")] OPIProxy = 0x015F, + /// + /// Used in the TIFF-FX standard to point to an IFD containing tags that are globally applicable to the complete TIFF file. + /// See RFC2301: TIFF-F/FX Specification. + /// It is recommended that a TIFF writer place this field in the first IFD, where a TIFF reader would find it quickly. + /// Each field in the GlobalParametersIFD is a TIFF field that is legal in any IFD. Required baseline fields should not be located in the GlobalParametersIFD, but should be in each image IFD. If a conflict exists between fields in the GlobalParametersIFD and in the image IFDs, then the data in the image IFD shall prevail. + /// + GlobalParametersIFD = 0x0190, + /// /// ProfileType /// @@ -637,6 +701,12 @@ internal enum ExifTagValue /// ImageID = 0x800D, + /// + /// Annotation data, as used in 'Imaging for Windows'. + /// See Other Private TIFF tags: http://www.awaresystems.be/imaging/tiff/tifftags/private.html + /// + WangAnnotation = 0x80A4, + /// /// CFARepeatPatternDim /// @@ -653,7 +723,8 @@ internal enum ExifTagValue BatteryLevel = 0x828F, /// - /// Copyright + /// Copyright notice. + /// See Section 8: Baseline Fields. /// Copyright = 0x8298, @@ -668,38 +739,70 @@ internal enum ExifTagValue FNumber = 0x829D, /// - /// MDFileTag + /// Specifies the pixel data format encoding in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html /// + [ExifTagDescription((ushort)2, "Squary root data format")] + [ExifTagDescription((ushort)128, "Linear data format")] MDFileTag = 0x82A5, /// - /// MDScalePixel + /// Specifies a scale factor in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// The scale factor is to be applies to each pixel before presenting it to the user. /// MDScalePixel = 0x82A6, /// - /// MDLabName + /// Used to specify the conversion from 16bit to 8bit in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// Since the display is only 9bit, the 16bit data must be converted before display. + /// 8bit value = (16bit value - low range ) * 255 / (high range - low range) + /// Count: n. + /// + [ExifTagDescription((ushort)0, "lowest possible")] + [ExifTagDescription((ushort)1, "low range")] + [ExifTagDescription("n-2", "high range")] + [ExifTagDescription("n-1", "highest possible")] + MDColorTable = 0x82A7, + + /// + /// Name of the lab that scanned this file, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html /// MDLabName = 0x82A8, /// - /// MDSampleInfo + /// Information about the sample, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// This information is entered by the person that scanned the file. + /// Note that the word 'sample' as used here, refers to the scanned sample, not an image channel. /// MDSampleInfo = 0x82A9, /// - /// MDPrepDate + /// Date the sample was prepared, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// The format of this data is YY/MM/DD. + /// Note that the word 'sample' as used here, refers to the scanned sample, not an image channel. /// MDPrepDate = 0x82AA, /// - /// MDPrepTime + /// Time the sample was prepared, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html + /// Format of this data is HH:MM using the 24-hour clock. + /// Note that the word 'sample' as used here, refers to the scanned sample, not an image channel. /// MDPrepTime = 0x82AB, /// - /// MDFileUnits + /// Units for data in this file, as used in the Molecular Dynamics GEL file format. + /// See Molecular Dynamics GEL File Format and Private Tags: https://www.awaresystems.be/imaging/tiff/tifftags/docs/gel.html /// + [ExifTagDescription("O.D.", "Densitometer")] + [ExifTagDescription("Counts", "PhosphorImager")] + [ExifTagDescription("RFU", "FluorImager")] MDFileUnits = 0x82AC, /// @@ -707,6 +810,12 @@ internal enum ExifTagValue /// PixelScale = 0x830E, + /// + /// IPTC (International Press Telecommunications Council) metadata. + /// See IPTC 4.1 specification. + /// + IPTC = 0x83BB, + /// /// IntergraphPacketData /// @@ -737,6 +846,40 @@ internal enum ExifTagValue /// ModelTransform = 0x85D8, + /// + /// Collection of Photoshop 'Image Resource Blocks' (Embedded Metadata). + /// See Extracting the Thumbnail from the PhotoShop private TIFF Tag: https://www.awaresystems.be/imaging/tiff/tifftags/docs/photoshopthumbnail.html + /// + Photoshop = 0x8649, + + /// + /// ICC profile data. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/iccprofile.html + /// + IccProfile = 0x8773, + + /// + /// Used in interchangeable GeoTIFF files. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/geokeydirectorytag.html + /// This tag is also know as 'ProjectionInfoTag' and 'CoordSystemInfoTag' + /// This tag may be used to store the GeoKey Directory, which defines and references the "GeoKeys". + /// + GeoKeyDirectoryTag = 0x87AF, + + /// + /// Used in interchangeable GeoTIFF files. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/geodoubleparamstag.html + /// This tag is used to store all of the DOUBLE valued GeoKeys, referenced by the GeoKeyDirectoryTag. The meaning of any value of this double array is determined from the GeoKeyDirectoryTag reference pointing to it. FLOAT values should first be converted to DOUBLE and stored here. + /// + GeoDoubleParamsTag = 0x87B0, + + /// + /// Used in interchangeable GeoTIFF files. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/geoasciiparamstag.html + /// This tag is used to store all of the ASCII valued GeoKeys, referenced by the GeoKeyDirectoryTag. Since keys use offsets into tags, any special comments may be placed at the beginning of this tag. For the most part, the only keys that are ASCII valued are "Citation" keys, giving documentation and references for obscure projections, datums, etc. + /// + GeoAsciiParamsTag = 0x87B1, + /// /// ImageLayer /// @@ -1184,6 +1327,14 @@ internal enum ExifTagValue /// RelatedSoundFile = 0xA004, + /// + /// A pointer to the Exif-related Interoperability IFD. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/interoperability.html + /// Interoperability IFD is composed of tags which stores the information to ensure the Interoperability. + /// The Interoperability structure of Interoperability IFD is same as TIFF defined IFD structure but does not contain the image data characteristically compared with normal TIFF IFD. + /// + InteroperabilityIFD = 0xA005, + /// /// FlashEnergy /// @@ -1539,5 +1690,41 @@ internal enum ExifTagValue /// GPSDifferential /// GPSDifferential = 0x001E, + + /// + /// Used in the Oce scanning process. + /// Identifies the scanticket used in the scanning process. + /// Includes a trailing zero. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/oce.html + /// + OceScanjobDescription = 0xC427, + + /// + /// Used in the Oce scanning process. + /// Identifies the application to process the TIFF file that results from scanning. + /// Includes a trailing zero. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/oce.html + /// + OceApplicationSelector = 0xC428, + + /// + /// Used in the Oce scanning process. + /// This is the user's answer to an optional question embedded in the Oce scanticket, and presented to that user before scanning. It can serve in further determination of the workflow. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/oce.html + /// + OceIdentificationNumber = 0xC429, + + /// + /// Used in the Oce scanning process. + /// This tag encodes the imageprocessing done by the Oce ImageLogic module in the scanner to ensure optimal quality for certain workflows. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/oce.html + /// + OceImageLogicCharacteristics = 0xC42A, + + /// + /// Alias Sketchbook Pro layer usage description. + /// See https://www.awaresystems.be/imaging/tiff/tifftags/docs/alias.html + /// + AliasLayerMetadata = 0xC660, } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs index 28002f0b75..2d3a93aed3 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs @@ -36,6 +36,90 @@ public override ExifDataType DataType } } + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int val: + return this.SetSingle(val); + case uint val: + return this.SetSingle(val); + case short val: + return this.SetSingle(val); + case ushort val: + return this.SetSingle(val); + case int[] array: + return this.SetArray(array); + case uint[] array: + return this.SetArray(array); + case short[] array: + return this.SetArray(array); + case ushort[] array: + return this.SetArray(array); + } + + return false; + } + public override IExifValue DeepClone() => new ExifNumberArray(this); + + private bool SetSingle(Number value) + { + this.Value = new[] { value }; + return true; + } + + private bool SetArray(int[] values) + { + var numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(uint[] values) + { + var numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(short[] values) + { + var numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(ushort[] values) + { + var numbers = new Number[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = values[i]; + } + + this.Value = numbers; + return true; + } } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index 2d8aa92601..af1eee2dc8 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -9,10 +9,10 @@ internal static partial class ExifValues public static ExifValue Create(ExifTag tag) => (ExifValue)CreateValue((ExifTagValue)(ushort)tag); - public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, uint numberOfComponents) - { - bool isArray = numberOfComponents != 1; + public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, uint numberOfComponents) => Create(tag, dataType, numberOfComponents != 1); + public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, bool isArray) + { switch (dataType) { case ExifDataType.Byte: return isArray ? (ExifValue)new ExifByteArray(tag, dataType) : new ExifByte(tag, dataType); @@ -93,6 +93,7 @@ private static object CreateValue(ExifTagValue tag) case ExifTagValue.ImageWidth: return new ExifNumber(ExifTag.ImageWidth); case ExifTagValue.ImageLength: return new ExifNumber(ExifTag.ImageLength); + case ExifTagValue.RowsPerStrip: return new ExifNumber(ExifTag.RowsPerStrip); case ExifTagValue.TileWidth: return new ExifNumber(ExifTag.TileWidth); case ExifTagValue.TileLength: return new ExifNumber(ExifTag.TileLength); case ExifTagValue.BadFaxLines: return new ExifNumber(ExifTag.BadFaxLines); @@ -100,6 +101,7 @@ private static object CreateValue(ExifTagValue tag) case ExifTagValue.PixelXDimension: return new ExifNumber(ExifTag.PixelXDimension); case ExifTagValue.PixelYDimension: return new ExifNumber(ExifTag.PixelYDimension); + case ExifTagValue.StripByteCounts: return new ExifNumberArray(ExifTag.StripByteCounts); case ExifTagValue.StripOffsets: return new ExifNumberArray(ExifTag.StripOffsets); case ExifTagValue.TileByteCounts: return new ExifNumberArray(ExifTag.TileByteCounts); case ExifTagValue.ImageLayer: return new ExifNumberArray(ExifTag.ImageLayer); @@ -158,6 +160,7 @@ private static object CreateValue(ExifTagValue tag) case ExifTagValue.Orientation: return new ExifShort(ExifTag.Orientation); case ExifTagValue.SamplesPerPixel: return new ExifShort(ExifTag.SamplesPerPixel); case ExifTagValue.PlanarConfiguration: return new ExifShort(ExifTag.PlanarConfiguration); + case ExifTagValue.Predictor: return new ExifShort(ExifTag.Predictor); case ExifTagValue.GrayResponseUnit: return new ExifShort(ExifTag.GrayResponseUnit); case ExifTagValue.ResolutionUnit: return new ExifShort(ExifTag.ResolutionUnit); case ExifTagValue.CleanFaxData: return new ExifShort(ExifTag.CleanFaxData); @@ -203,7 +206,6 @@ private static object CreateValue(ExifTagValue tag) case ExifTagValue.ExtraSamples: return new ExifShortArray(ExifTag.ExtraSamples); case ExifTagValue.PageNumber: return new ExifShortArray(ExifTag.PageNumber); case ExifTagValue.TransferFunction: return new ExifShortArray(ExifTag.TransferFunction); - case ExifTagValue.Predictor: return new ExifShortArray(ExifTag.Predictor); case ExifTagValue.HalftoneHints: return new ExifShortArray(ExifTag.HalftoneHints); case ExifTagValue.SampleFormat: return new ExifShortArray(ExifTag.SampleFormat); case ExifTagValue.TransferRange: return new ExifShortArray(ExifTag.TransferRange); diff --git a/tests/ImageSharp.Benchmarks/Codecs/DecodeTiff.cs b/tests/ImageSharp.Benchmarks/Codecs/DecodeTiff.cs new file mode 100644 index 0000000000..b388b5d70a --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/DecodeTiff.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// Enable this for using larger Tiff files. Those files are very large (> 700MB) and therefor not part of the git repo. +// Use the scripts gen_big.ps1 and gen_medium.ps1 in tests\Images\Input\Tiff\Benchmarks to generate those images. +//// #define BIG_TESTS + +using System.IO; + +using BenchmarkDotNet.Attributes; + +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +using SDImage = System.Drawing.Image; +using SDSize = System.Drawing.Size; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs +{ + [MarkdownExporter] + [HtmlExporter] + [Config(typeof(Config.ShortMultiFramework))] + public class DecodeTiff + { + private string prevImage = null; + + private byte[] data; + + private Configuration configuration; + +#if BIG_TESTS + private static readonly int BufferSize = 1024 * 68; + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Path.Combine(TestImages.Tiff.Benchmark_Path, this.TestImage)); + + [Params( + TestImages.Tiff.Benchmark_BwFax3, + //// TestImages.Tiff.Benchmark_RgbFax4, // fax4 is not supported yet. + TestImages.Tiff.Benchmark_GrayscaleUncompressed, + TestImages.Tiff.Benchmark_PaletteUncompressed, + TestImages.Tiff.Benchmark_RgbDeflate, + TestImages.Tiff.Benchmark_RgbLzw, + TestImages.Tiff.Benchmark_RgbPackbits, + TestImages.Tiff.Benchmark_RgbUncompressed)] + public string TestImage { get; set; } + +#else + private static readonly int BufferSize = Configuration.Default.StreamProcessingBufferSize; + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params( + TestImages.Tiff.CcittFax3AllTermCodes, + TestImages.Tiff.HuffmanRleAllMakeupCodes, + TestImages.Tiff.Calliphora_GrayscaleUncompressed, + TestImages.Tiff.Calliphora_RgbPaletteLzw_Predictor, + TestImages.Tiff.Calliphora_RgbDeflate_Predictor, + TestImages.Tiff.Calliphora_RgbLzwPredictor, + TestImages.Tiff.Calliphora_RgbPackbits, + TestImages.Tiff.Calliphora_RgbUncompressed)] + public string TestImage { get; set; } +#endif + + [GlobalSetup] + public void Config() + { + if (this.configuration == null) + { + this.configuration = new Configuration(); + this.configuration.AddTiff(); + this.configuration.StreamProcessingBufferSize = BufferSize; + } + } + + [IterationSetup] + public void ReadImages() + { + if (this.prevImage != this.TestImage) + { + this.data = File.ReadAllBytes(this.TestImageFullPath); + this.prevImage = this.TestImage; + } + } + + [Benchmark(Baseline = true, Description = "System.Drawing Tiff")] + public SDSize TiffSystemDrawing() + { + using (var memoryStream = new MemoryStream(this.data)) + using (var image = SDImage.FromStream(memoryStream)) + { + return image.Size; + } + } + + [Benchmark(Description = "ImageSharp Tiff")] + public Size TiffCore() + { + using (var ms = new MemoryStream(this.data)) + using (var image = Image.Load(this.configuration, ms)) + { + return image.Size(); + } + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs index bcb015e570..0e320e4a7f 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs @@ -40,17 +40,17 @@ public void Cleanup() } [Benchmark(Baseline = true, Description = "Magick Tga")] - public void BmpImageMagick() + public void TgaMagick() { using var memoryStream = new MemoryStream(); this.tgaMagick.Write(memoryStream, MagickFormat.Tga); } [Benchmark(Description = "ImageSharp Tga")] - public void BmpImageSharp() + public void TgaImageSharp() { using var memoryStream = new MemoryStream(); - this.tgaCore.SaveAsBmp(memoryStream); + this.tgaCore.SaveAsTga(memoryStream); } } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeTiff.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeTiff.cs new file mode 100644 index 0000000000..7154b2310b --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeTiff.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Drawing.Imaging; +using System.IO; + +using BenchmarkDotNet.Attributes; + +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs +{ + [MarkdownExporter] + [HtmlExporter] + [Config(typeof(Config.ShortMultiFramework))] + public class EncodeTiff + { + private System.Drawing.Image drawing; + private Image core; + + private Configuration configuration; + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params(TestImages.Tiff.Calliphora_RgbUncompressed)] + public string TestImage { get; set; } + + [Params( + TiffCompression.None, + TiffCompression.Deflate, + TiffCompression.Lzw, + TiffCompression.PackBits, + TiffCompression.CcittGroup3Fax, + TiffCompression.Ccitt1D)] + public TiffCompression Compression { get; set; } + + [GlobalSetup] + public void ReadImages() + { + if (this.core == null) + { + this.configuration = new Configuration(); + this.configuration.AddTiff(); + + this.core = Image.Load(this.configuration, this.TestImageFullPath); + this.drawing = System.Drawing.Image.FromFile(this.TestImageFullPath); + } + } + + [GlobalCleanup] + public void Cleanup() + { + this.core.Dispose(); + this.drawing.Dispose(); + } + + [Benchmark(Baseline = true, Description = "System.Drawing Tiff")] + public void SystemDrawing() + { + ImageCodecInfo codec = FindCodecForType("image/tiff"); + using var parameters = new EncoderParameters(1) + { + Param = {[0] = new EncoderParameter(Encoder.Compression, (long)Cast(this.Compression))} + }; + + using var memoryStream = new MemoryStream(); + this.drawing.Save(memoryStream, codec, parameters); + } + + [Benchmark(Description = "ImageSharp Tiff")] + public void TiffCore() + { + TiffPhotometricInterpretation photometricInterpretation = TiffPhotometricInterpretation.Rgb; + + // Workaround for 1-bit bug + if (this.Compression == TiffCompression.CcittGroup3Fax || this.Compression == TiffCompression.Ccitt1D) + { + photometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero; + } + + var encoder = new TiffEncoder() { Compression = this.Compression, PhotometricInterpretation = photometricInterpretation }; + using var memoryStream = new MemoryStream(); + this.core.SaveAsTiff(memoryStream, encoder); + } + + private static ImageCodecInfo FindCodecForType(string mimeType) + { + ImageCodecInfo[] imgEncoders = ImageCodecInfo.GetImageEncoders(); + + for (int i = 0; i < imgEncoders.GetLength(0); i++) + { + if (imgEncoders[i].MimeType == mimeType) + { + return imgEncoders[i]; + } + } + + return null; + } + + private static EncoderValue Cast(TiffCompression compression) + { + switch (compression) + { + case TiffCompression.None: + return EncoderValue.CompressionNone; + + case TiffCompression.CcittGroup3Fax: + return EncoderValue.CompressionCCITT3; + + case TiffCompression.Ccitt1D: + return EncoderValue.CompressionRle; + + case TiffCompression.Lzw: + return EncoderValue.CompressionLZW; + + default: + throw new System.NotSupportedException(compression.ToString()); + } + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Config.cs b/tests/ImageSharp.Benchmarks/Config.cs index 0c40b482ad..63064eec5b 100644 --- a/tests/ImageSharp.Benchmarks/Config.cs +++ b/tests/ImageSharp.Benchmarks/Config.cs @@ -9,6 +9,7 @@ using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Reports; namespace SixLabors.ImageSharp.Benchmarks { @@ -25,6 +26,7 @@ public Config() } #endif + this.SummaryStyle = SummaryStyle.Default.WithMaxParameterColumnWidth(50); } public class MultiFramework : Config diff --git a/tests/ImageSharp.Benchmarks/General/Adler32Benchmark.cs b/tests/ImageSharp.Benchmarks/General/Adler32Benchmark.cs index 2d0fce0a01..416a62833c 100644 --- a/tests/ImageSharp.Benchmarks/General/Adler32Benchmark.cs +++ b/tests/ImageSharp.Benchmarks/General/Adler32Benchmark.cs @@ -3,7 +3,7 @@ using System; using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Formats.Png.Zlib; +using SixLabors.ImageSharp.Compression.Zlib; using SharpAdler32 = ICSharpCode.SharpZipLib.Checksum.Adler32; namespace SixLabors.ImageSharp.Benchmarks.General diff --git a/tests/ImageSharp.Benchmarks/General/Crc32Benchmark.cs b/tests/ImageSharp.Benchmarks/General/Crc32Benchmark.cs index 0153ca8b1e..8a90b44cfb 100644 --- a/tests/ImageSharp.Benchmarks/General/Crc32Benchmark.cs +++ b/tests/ImageSharp.Benchmarks/General/Crc32Benchmark.cs @@ -3,7 +3,7 @@ using System; using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Formats.Png.Zlib; +using SixLabors.ImageSharp.Compression.Zlib; using SharpCrc32 = ICSharpCode.SharpZipLib.Checksum.Crc32; namespace SixLabors.ImageSharp.Benchmarks.General diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs index 44a6b17a66..ea5e8b0870 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs @@ -18,12 +18,14 @@ public class BmpMetadataTests [Fact] public void CloneIsDeep() { - var meta = new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel24 }; + var meta = new BmpMetadata { BitsPerPixel = BmpBitsPerPixel.Pixel24, InfoHeaderType = BmpInfoHeaderType.Os2Version2 }; var clone = (BmpMetadata)meta.DeepClone(); clone.BitsPerPixel = BmpBitsPerPixel.Pixel32; + clone.InfoHeaderType = BmpInfoHeaderType.WinVersion2; Assert.False(meta.BitsPerPixel.Equals(clone.BitsPerPixel)); + Assert.False(meta.InfoHeaderType.Equals(clone.InfoHeaderType)); } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs b/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs index aadd30f2bc..0886bd84dc 100644 --- a/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; -using SixLabors.ImageSharp.Formats.Png.Zlib; +using SixLabors.ImageSharp.Compression.Zlib; using Xunit; using SharpAdler32 = ICSharpCode.SharpZipLib.Checksum.Adler32; diff --git a/tests/ImageSharp.Tests/Formats/Png/Crc32Tests.cs b/tests/ImageSharp.Tests/Formats/Png/Crc32Tests.cs index 1ae21e7715..6bdad6ed4d 100644 --- a/tests/ImageSharp.Tests/Formats/Png/Crc32Tests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/Crc32Tests.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; -using SixLabors.ImageSharp.Formats.Png.Zlib; +using SixLabors.ImageSharp.Compression.Zlib; using Xunit; using SharpCrc32 = ICSharpCode.SharpZipLib.Checksum.Crc32; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs new file mode 100644 index 0000000000..782a504d58 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression +{ + [Trait("Format", "Tiff")] + public class DeflateTiffCompressionTests + { + [Theory] + [InlineData(new byte[] { })] + [InlineData(new byte[] { 42 })] // One byte + [InlineData(new byte[] { 42, 16, 128, 53, 96, 218, 7, 64, 3, 4, 97 })] // Random bytes + [InlineData(new byte[] { 1, 2, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 3, 4 })] // Repeated bytes + [InlineData(new byte[] { 1, 2, 42, 53, 42, 53, 42, 53, 42, 53, 42, 53, 3, 4 })] // Repeated sequence + public void Compress_Decompress_Roundtrip_Works(byte[] data) + { + using (BufferedReadStream stream = CreateCompressedStream(data)) + { + var buffer = new byte[data.Length]; + + using var decompressor = new DeflateTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffPredictor.None); + + decompressor.Decompress(stream, 0, (uint)stream.Length, buffer); + + Assert.Equal(data, buffer); + } + } + + private static BufferedReadStream CreateCompressedStream(byte[] data) + { + Stream compressedStream = new MemoryStream(); + + using (Stream uncompressedStream = new MemoryStream(data), + deflateStream = new ZlibDeflateStream(Configuration.Default.MemoryAllocator, compressedStream, DeflateCompressionLevel.Level6)) + { + uncompressedStream.CopyTo(deflateStream); + } + + compressedStream.Seek(0, SeekOrigin.Begin); + return new BufferedReadStream(Configuration.Default, compressedStream); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs new file mode 100644 index 0000000000..bf585e9c8f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.IO; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression +{ + [Trait("Format", "Tiff")] + public class LzwTiffCompressionTests + { + [Theory] + [InlineData(new byte[] { 1, 2, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 3, 4 }, new byte[] { 128, 0, 64, 66, 168, 36, 22, 12, 3, 2, 64, 64, 0, 0 })] // Repeated bytes + + public void Compress_Works(byte[] inputData, byte[] expectedCompressedData) + { + var compressedData = new byte[expectedCompressedData.Length]; + Stream streamData = CreateCompressedStream(inputData); + streamData.Read(compressedData, 0, expectedCompressedData.Length); + + Assert.Equal(expectedCompressedData, compressedData); + } + + [Theory] + [InlineData(new byte[] { })] + [InlineData(new byte[] { 42 })] // One byte + [InlineData(new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 })] + [InlineData(new byte[] { 42, 16, 128, 53, 96, 218, 7, 64, 3, 4, 97 })] // Random bytes + [InlineData(new byte[] { 1, 2, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 3, 4 })] // Repeated bytes + [InlineData(new byte[] { 1, 2, 42, 53, 42, 53, 42, 53, 42, 53, 42, 53, 3, 4 })] // Repeated sequence + + public void Compress_Decompress_Roundtrip_Works(byte[] data) + { + using BufferedReadStream stream = CreateCompressedStream(data); + var buffer = new byte[data.Length]; + + using var decompressor = new LzwTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffPredictor.None); + decompressor.Decompress(stream, 0, (uint)stream.Length, buffer); + + Assert.Equal(data, buffer); + } + + private static BufferedReadStream CreateCompressedStream(byte[] inputData) + { + Stream compressedStream = new MemoryStream(); + + using (var encoder = new TiffLzwEncoder(Configuration.Default.MemoryAllocator)) + { + encoder.Encode(inputData, compressedStream); + } + + compressedStream.Seek(0, SeekOrigin.Begin); + + return new BufferedReadStream(Configuration.Default, compressedStream); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs new file mode 100644 index 0000000000..82ecb315b0 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; +using SixLabors.ImageSharp.IO; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression +{ + [Trait("Format", "Tiff")] + public class NoneTiffCompressionTests + { + [Theory] + [InlineData(new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 }, 8, new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 })] + [InlineData(new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 }, 5, new byte[] { 10, 15, 20, 25, 30 })] + public void Decompress_ReadsData(byte[] inputData, uint byteCount, byte[] expectedResult) + { + var stream = new BufferedReadStream(Configuration.Default, new MemoryStream(inputData)); + var buffer = new byte[expectedResult.Length]; + + new NoneTiffCompression(default, default, default).Decompress(stream, 0, byteCount, buffer); + + Assert.Equal(expectedResult, buffer); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs new file mode 100644 index 0000000000..b67cb83254 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression +{ + [Trait("Format", "Tiff")] + public class PackBitsTiffCompressionTests + { + [Theory] + [InlineData(new byte[] { }, new byte[] { })] + [InlineData(new byte[] { 0x00, 0x2A }, new byte[] { 0x2A })] // Read one byte + [InlineData(new byte[] { 0x01, 0x15, 0x32 }, new byte[] { 0x15, 0x32 })] // Read two bytes + [InlineData(new byte[] { 0xFF, 0x2A }, new byte[] { 0x2A, 0x2A })] // Repeat two bytes + [InlineData(new byte[] { 0xFE, 0x2A }, new byte[] { 0x2A, 0x2A, 0x2A })] // Repeat three bytes + [InlineData(new byte[] { 0x80 }, new byte[] { })] // Read a 'No operation' byte + [InlineData(new byte[] { 0x01, 0x15, 0x32, 0x80, 0xFF, 0xA2 }, new byte[] { 0x15, 0x32, 0xA2, 0xA2 })] // Read two bytes, nop, repeat two bytes + [InlineData(new byte[] { 0xFE, 0xAA, 0x02, 0x80, 0x00, 0x2A, 0xFD, 0xAA, 0x03, 0x80, 0x00, 0x2A, 0x22, 0xF7, 0xAA }, new byte[] { 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0xAA, 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA })] // Apple PackBits sample + public void Decompress_ReadsData(byte[] inputData, byte[] expectedResult) + { + var stream = new BufferedReadStream(Configuration.Default, new MemoryStream(inputData)); + var buffer = new byte[expectedResult.Length]; + + using var decompressor = new PackBitsTiffCompression(new ArrayPoolMemoryAllocator(), default, default); + decompressor.Decompress(stream, 0, (uint)inputData.Length, buffer); + + Assert.Equal(expectedResult, buffer); + } + + [Theory] + [InlineData(new byte[] { 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0xAA, 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA }, new byte[] { 0xFE, 0xAA, 0x02, 0x80, 0x00, 0x2A, 0xFD, 0xAA, 0x03, 0x80, 0x00, 0x2A, 0x22, 0xF7, 0xAA })] // Apple PackBits sample + public void Compress_Works(byte[] inputData, byte[] expectedResult) + { + // arrange + Span input = inputData.AsSpan(); + var compressed = new byte[expectedResult.Length]; + + // act + PackBitsWriter.PackBits(input, compressed); + + // assert + Assert.Equal(expectedResult, compressed); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Tiff/ImageExtensionsTest.cs new file mode 100644 index 0000000000..e2aeeb9e84 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/ImageExtensionsTest.cs @@ -0,0 +1,164 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +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("Format", "Tiff")] + public class ImageExtensionsTest + { + private readonly Configuration configuration; + + public ImageExtensionsTest() + { + this.configuration = new Configuration(); + this.configuration.AddTiff(); + } + + [Fact] + public void SaveAsTiff_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsTiff_Path.tiff"); + + using (var image = new Image(this.configuration, 10, 10)) + { + image.SaveAsTiff(file); + } + + using (Image.Load(this.configuration, file, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsTiffAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsTiffAsync_Path.tiff"); + + using (var image = new Image(this.configuration, 10, 10)) + { + await image.SaveAsTiffAsync(file); + } + + using (Image.Load(this.configuration, file, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact] + 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(this.configuration, 10, 10)) + { + image.SaveAsTiff(file, new TiffEncoder()); + } + + using (Image.Load(this.configuration, file, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact] + 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(this.configuration, 10, 10)) + { + await image.SaveAsTiffAsync(file, new TiffEncoder()); + } + + using (Image.Load(this.configuration, file, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsTiff_Stream() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(this.configuration, 10, 10)) + { + image.SaveAsTiff(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(this.configuration, memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsTiffAsync_StreamAsync() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(this.configuration, 10, 10)) + { + await image.SaveAsTiffAsync(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(this.configuration, memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsTiff_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(this.configuration, 10, 10)) + { + image.SaveAsTiff(memoryStream, new TiffEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(this.configuration, memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsTiffAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(this.configuration, 10, 10)) + { + await image.SaveAsTiffAsync(memoryStream, new TiffEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(this.configuration, memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs new file mode 100644 index 0000000000..579ee02901 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs @@ -0,0 +1,195 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation +{ + [Trait("Format", "Tiff")] + public class BlackIsZeroTiffColorTests : PhotometricInterpretationTestBase + { + private static readonly Rgba32 Gray000 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Gray128 = new Rgba32(128, 128, 128, 255); + private static readonly Rgba32 Gray255 = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Gray0 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Gray8 = new Rgba32(136, 136, 136, 255); + private static readonly Rgba32 GrayF = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Bit0 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Bit1 = new Rgba32(255, 255, 255, 255); + + private static readonly byte[] BilevelBytes4X4 = + { + 0b01010000, + 0b11110000, + 0b01110000, + 0b10010000 + }; + + private static readonly Rgba32[][] BilevelResult4X4 = new[] + { + new[] { Bit0, Bit1, Bit0, Bit1 }, + new[] { Bit1, Bit1, Bit1, Bit1 }, + new[] { Bit0, Bit1, Bit1, Bit1 }, + new[] { Bit1, Bit0, Bit0, Bit1 } + }; + + private static readonly byte[] BilevelBytes12X4 = + { + 0b01010101, 0b01010000, + 0b11111111, 0b11111111, + 0b01101001, 0b10100000, + 0b10010000, 0b01100000 + }; + + private static readonly Rgba32[][] BilevelResult12X4 = + { + new[] { Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1 }, + new[] { Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1 }, + new[] { Bit0, Bit1, Bit1, Bit0, Bit1, Bit0, Bit0, Bit1, Bit1, Bit0, Bit1, Bit0 }, + new[] { Bit1, Bit0, Bit0, Bit1, Bit0, Bit0, Bit0, Bit0, Bit0, Bit1, Bit1, Bit0 } + }; + + private static readonly byte[] Grayscale4Bytes4X4 = + { + 0x8F, 0x0F, + 0xFF, 0xFF, + 0x08, 0x8F, + 0xF0, 0xF8 + }; + + private static readonly Rgba32[][] Grayscale4Result4X4 = + { + new[] { Gray8, GrayF, Gray0, GrayF }, + new[] { GrayF, GrayF, GrayF, GrayF }, + new[] { Gray0, Gray8, Gray8, GrayF }, + new[] { GrayF, Gray0, GrayF, Gray8 } + }; + + private static readonly byte[] Grayscale4Bytes3X4 = + { + 0x8F, 0x00, + 0xFF, 0xF0, + 0x08, 0x80, + 0xF0, 0xF0 + }; + + private static readonly Rgba32[][] Grayscale4Result3X4 = + { + new[] { Gray8, GrayF, Gray0 }, + new[] { GrayF, GrayF, GrayF }, + new[] { Gray0, Gray8, Gray8 }, + new[] { GrayF, Gray0, GrayF } + }; + + private static readonly byte[] Grayscale8Bytes4X4 = + { + 128, 255, 000, 255, + 255, 255, 255, 255, + 000, 128, 128, 255, + 255, 000, 255, 128 + }; + + private static readonly Rgba32[][] Grayscale8Result4X4 = + { + new[] { Gray128, Gray255, Gray000, Gray255 }, + new[] { Gray255, Gray255, Gray255, Gray255 }, + new[] { Gray000, Gray128, Gray128, Gray255 }, + new[] { Gray255, Gray000, Gray255, Gray128 } + }; + + public static IEnumerable BilevelData + { + get + { + yield return new object[] { BilevelBytes4X4, 1, 0, 0, 4, 4, BilevelResult4X4 }; + yield return new object[] { BilevelBytes4X4, 1, 0, 0, 4, 4, Offset(BilevelResult4X4, 0, 0, 6, 6) }; + yield return new object[] { BilevelBytes4X4, 1, 1, 0, 4, 4, Offset(BilevelResult4X4, 1, 0, 6, 6) }; + yield return new object[] { BilevelBytes4X4, 1, 0, 1, 4, 4, Offset(BilevelResult4X4, 0, 1, 6, 6) }; + yield return new object[] { BilevelBytes4X4, 1, 1, 1, 4, 4, Offset(BilevelResult4X4, 1, 1, 6, 6) }; + + yield return new object[] { BilevelBytes12X4, 1, 0, 0, 12, 4, BilevelResult12X4 }; + yield return new object[] { BilevelBytes12X4, 1, 0, 0, 12, 4, Offset(BilevelResult12X4, 0, 0, 18, 6) }; + yield return new object[] { BilevelBytes12X4, 1, 1, 0, 12, 4, Offset(BilevelResult12X4, 1, 0, 18, 6) }; + yield return new object[] { BilevelBytes12X4, 1, 0, 1, 12, 4, Offset(BilevelResult12X4, 0, 1, 18, 6) }; + yield return new object[] { BilevelBytes12X4, 1, 1, 1, 12, 4, Offset(BilevelResult12X4, 1, 1, 18, 6) }; + } + } + + public static IEnumerable Grayscale4_Data + { + get + { + yield return new object[] { Grayscale4Bytes4X4, 4, 0, 0, 4, 4, Grayscale4Result4X4 }; + yield return new object[] { Grayscale4Bytes4X4, 4, 0, 0, 4, 4, Offset(Grayscale4Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Grayscale4Bytes4X4, 4, 1, 0, 4, 4, Offset(Grayscale4Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Grayscale4Bytes4X4, 4, 0, 1, 4, 4, Offset(Grayscale4Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Grayscale4Bytes4X4, 4, 1, 1, 4, 4, Offset(Grayscale4Result4X4, 1, 1, 6, 6) }; + + yield return new object[] { Grayscale4Bytes3X4, 4, 0, 0, 3, 4, Grayscale4Result3X4 }; + yield return new object[] { Grayscale4Bytes3X4, 4, 0, 0, 3, 4, Offset(Grayscale4Result3X4, 0, 0, 6, 6) }; + yield return new object[] { Grayscale4Bytes3X4, 4, 1, 0, 3, 4, Offset(Grayscale4Result3X4, 1, 0, 6, 6) }; + yield return new object[] { Grayscale4Bytes3X4, 4, 0, 1, 3, 4, Offset(Grayscale4Result3X4, 0, 1, 6, 6) }; + yield return new object[] { Grayscale4Bytes3X4, 4, 1, 1, 3, 4, Offset(Grayscale4Result3X4, 1, 1, 6, 6) }; + } + } + + public static IEnumerable Grayscale8_Data + { + get + { + yield return new object[] { Grayscale8Bytes4X4, 8, 0, 0, 4, 4, Grayscale8Result4X4 }; + yield return new object[] { Grayscale8Bytes4X4, 8, 0, 0, 4, 4, Offset(Grayscale8Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Grayscale8Bytes4X4, 8, 1, 0, 4, 4, Offset(Grayscale8Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Grayscale8Bytes4X4, 8, 0, 1, 4, 4, Offset(Grayscale8Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Grayscale8Bytes4X4, 8, 1, 1, 4, 4, Offset(Grayscale8Result4X4, 1, 1, 6, 6) }; + } + } + + [Theory] + [MemberData(nameof(BilevelData))] + [MemberData(nameof(Grayscale4_Data))] + [MemberData(nameof(Grayscale8_Data))] + public void Decode_WritesPixelData(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new BlackIsZeroTiffColor(new[] { (ushort)bitsPerSample }).Decode(inputData, pixels, left, top, width, height); + }); + } + + [Theory] + [MemberData(nameof(BilevelData))] + public void Decode_WritesPixelData_Bilevel(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new BlackIsZero1TiffColor().Decode(inputData, pixels, left, top, width, height); + }); + } + + [Theory] + [MemberData(nameof(Grayscale4_Data))] + public void Decode_WritesPixelData_4Bit(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new BlackIsZero4TiffColor().Decode(inputData, pixels, left, top, width, height); + }); + } + + [Theory] + [MemberData(nameof(Grayscale8_Data))] + public void Decode_WritesPixelData_8Bit(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new BlackIsZero8TiffColor().Decode(inputData, pixels, left, top, width, height); + }); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PaletteTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PaletteTiffColorTests.cs new file mode 100644 index 0000000000..0da1d8bbd4 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PaletteTiffColorTests.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation +{ + [Trait("Format", "Tiff")] + public class PaletteTiffColorTests : PhotometricInterpretationTestBase + { + public static uint[][] Palette4ColorPalette => GeneratePalette(16); + + public static ushort[] Palette4ColorMap => GenerateColorMap(Palette4ColorPalette); + + private static readonly byte[] Palette4Bytes4X4 = + { + 0x01, 0x23, 0x4A, 0xD2, 0x12, 0x34, 0xAB, 0xEF + }; + + private static readonly Rgba32[][] Palette4Result4X4 = GenerateResult( + Palette4ColorPalette, + new[] { new[] { 0x00, 0x01, 0x02, 0x03 }, new[] { 0x04, 0x0A, 0x0D, 0x02 }, new[] { 0x01, 0x02, 0x03, 0x04 }, new[] { 0x0A, 0x0B, 0x0E, 0x0F } }); + + private static readonly byte[] Palette4Bytes3X4 = + { + 0x01, 0x20, + 0x4A, 0xD0, + 0x12, 0x30, + 0xAB, 0xE0 + }; + + private static readonly Rgba32[][] Palette4Result3X4 = GenerateResult(Palette4ColorPalette, new[] { new[] { 0x00, 0x01, 0x02 }, new[] { 0x04, 0x0A, 0x0D }, new[] { 0x01, 0x02, 0x03 }, new[] { 0x0A, 0x0B, 0x0E } }); + + public static IEnumerable Palette4Data + { + get + { + yield return new object[] { Palette4Bytes4X4, 4, Palette4ColorMap, 0, 0, 4, 4, Palette4Result4X4 }; + yield return new object[] { Palette4Bytes4X4, 4, Palette4ColorMap, 0, 0, 4, 4, Offset(Palette4Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Palette4Bytes4X4, 4, Palette4ColorMap, 1, 0, 4, 4, Offset(Palette4Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Palette4Bytes4X4, 4, Palette4ColorMap, 0, 1, 4, 4, Offset(Palette4Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Palette4Bytes4X4, 4, Palette4ColorMap, 1, 1, 4, 4, Offset(Palette4Result4X4, 1, 1, 6, 6) }; + + yield return new object[] { Palette4Bytes3X4, 4, Palette4ColorMap, 0, 0, 3, 4, Palette4Result3X4 }; + yield return new object[] { Palette4Bytes3X4, 4, Palette4ColorMap, 0, 0, 3, 4, Offset(Palette4Result3X4, 0, 0, 6, 6) }; + yield return new object[] { Palette4Bytes3X4, 4, Palette4ColorMap, 1, 0, 3, 4, Offset(Palette4Result3X4, 1, 0, 6, 6) }; + yield return new object[] { Palette4Bytes3X4, 4, Palette4ColorMap, 0, 1, 3, 4, Offset(Palette4Result3X4, 0, 1, 6, 6) }; + yield return new object[] { Palette4Bytes3X4, 4, Palette4ColorMap, 1, 1, 3, 4, Offset(Palette4Result3X4, 1, 1, 6, 6) }; + } + } + + public static uint[][] Palette8ColorPalette => GeneratePalette(256); + + public static ushort[] Palette8ColorMap => GenerateColorMap(Palette8ColorPalette); + + private static readonly byte[] Palette8Bytes4X4 = + { + 000, 001, 002, 003, + 100, 110, 120, 130, + 000, 255, 128, 255, + 050, 100, 150, 200 + }; + + private static readonly Rgba32[][] Palette8Result4X4 = GenerateResult(Palette8ColorPalette, new[] { new[] { 000, 001, 002, 003 }, new[] { 100, 110, 120, 130 }, new[] { 000, 255, 128, 255 }, new[] { 050, 100, 150, 200 } }); + + public static IEnumerable Palette8Data + { + get + { + yield return new object[] { Palette8Bytes4X4, 8, Palette8ColorMap, 0, 0, 4, 4, Palette8Result4X4 }; + yield return new object[] { Palette8Bytes4X4, 8, Palette8ColorMap, 0, 0, 4, 4, Offset(Palette8Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Palette8Bytes4X4, 8, Palette8ColorMap, 1, 0, 4, 4, Offset(Palette8Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Palette8Bytes4X4, 8, Palette8ColorMap, 0, 1, 4, 4, Offset(Palette8Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Palette8Bytes4X4, 8, Palette8ColorMap, 1, 1, 4, 4, Offset(Palette8Result4X4, 1, 1, 6, 6) }; + } + } + + [Theory] + [MemberData(nameof(Palette4Data))] + [MemberData(nameof(Palette8Data))] + public void Decode_WritesPixelData(byte[] inputData, ushort bitsPerSample, ushort[] colorMap, int left, int top, int width, int height, Rgba32[][] expectedResult) => AssertDecode(expectedResult, pixels => + { + new PaletteTiffColor(new[] { bitsPerSample }, colorMap).Decode(inputData, pixels, left, top, width, height); + }); + + private static uint[][] GeneratePalette(int count) + { + var palette = new uint[count][]; + + for (uint i = 0; i < count; i++) + { + palette[i] = new[] { (i * 2u) % 65536u, (i * 2625u) % 65536u, (i * 29401u) % 65536u }; + } + + return palette; + } + + private static ushort[] GenerateColorMap(uint[][] colorPalette) + { + int colorCount = colorPalette.Length; + var colorMap = new ushort[colorCount * 3]; + + for (int i = 0; i < colorCount; i++) + { + colorMap[(colorCount * 0) + i] = (ushort)colorPalette[i][0]; + colorMap[(colorCount * 1) + i] = (ushort)colorPalette[i][1]; + colorMap[(colorCount * 2) + i] = (ushort)colorPalette[i][2]; + } + + return colorMap; + } + + private static Rgba32[][] GenerateResult(uint[][] colorPalette, int[][] pixelLookup) + { + var result = new Rgba32[pixelLookup.Length][]; + + for (int y = 0; y < pixelLookup.Length; y++) + { + result[y] = new Rgba32[pixelLookup[y].Length]; + + for (int x = 0; x < pixelLookup[y].Length; x++) + { + uint[] sourceColor = colorPalette[pixelLookup[y][x]]; + result[y][x] = new Rgba32(sourceColor[0] / 65535F, sourceColor[1] / 65535F, sourceColor[2] / 65535F); + } + } + + return result; + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs new file mode 100644 index 0000000000..0bb61ce1c4 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation +{ + [Trait("Format", "Tiff")] + public abstract class PhotometricInterpretationTestBase + { + public static Rgba32 DefaultColor = new Rgba32(42, 96, 18, 128); + + public static Rgba32[][] Offset(Rgba32[][] input, int xOffset, int yOffset, int width, int height) + { + int inputHeight = input.Length; + int inputWidth = input[0].Length; + + var output = new Rgba32[height][]; + + for (int y = 0; y < output.Length; y++) + { + output[y] = new Rgba32[width]; + + for (int x = 0; x < width; x++) + { + output[y][x] = DefaultColor; + } + } + + for (int y = 0; y < inputHeight; y++) + { + for (int x = 0; x < inputWidth; x++) + { + output[y + yOffset][x + xOffset] = input[y][x]; + } + } + + return output; + } + + internal static void AssertDecode(Rgba32[][] expectedResult, Action> decodeAction) + { + int resultWidth = expectedResult[0].Length; + int resultHeight = expectedResult.Length; + + using (var image = new Image(resultWidth, resultHeight)) + { + image.Mutate(x => x.BackgroundColor(DefaultColor)); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + decodeAction(pixels); + + for (int y = 0; y < resultHeight; y++) + { + for (int x = 0; x < resultWidth; x++) + { + Assert.True( + expectedResult[y][x] == pixels[x, y], + $"Pixel ({x}, {y}) should be {expectedResult[y][x]} but was {pixels[x, y]}"); + } + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs new file mode 100644 index 0000000000..abfae6ab40 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; + +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation +{ + [Trait("Format", "Tiff")] + public class RgbPlanarTiffColorTests : PhotometricInterpretationTestBase + { + private static readonly Rgba32 Rgb4_000 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Rgb4_444 = new Rgba32(68, 68, 68, 255); + private static readonly Rgba32 Rgb4_888 = new Rgba32(136, 136, 136, 255); + private static readonly Rgba32 Rgb4_CCC = new Rgba32(204, 204, 204, 255); + private static readonly Rgba32 Rgb4_FFF = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Rgb4_F00 = new Rgba32(255, 0, 0, 255); + private static readonly Rgba32 Rgb4_0F0 = new Rgba32(0, 255, 0, 255); + private static readonly Rgba32 Rgb4_00F = new Rgba32(0, 0, 255, 255); + private static readonly Rgba32 Rgb4_F0F = new Rgba32(255, 0, 255, 255); + private static readonly Rgba32 Rgb4_400 = new Rgba32(68, 0, 0, 255); + private static readonly Rgba32 Rgb4_800 = new Rgba32(136, 0, 0, 255); + private static readonly Rgba32 Rgb4_C00 = new Rgba32(204, 0, 0, 255); + private static readonly Rgba32 Rgb4_48C = new Rgba32(68, 136, 204, 255); + + private static readonly byte[] Rgb4Bytes4X4R = + { + 0x0F, 0x0F, + 0xF0, 0x0F, + 0x48, 0xC4, + 0x04, 0x8C + }; + + private static readonly byte[] Rgb4Bytes4X4G = + { + 0x0F, 0x0F, + 0x0F, 0x00, + 0x00, 0x08, + 0x04, 0x8C + }; + + private static readonly byte[] Rgb4Bytes4X4B = + { + 0x0F, 0x0F, + 0x00, 0xFF, + 0x00, 0x0C, + 0x04, 0x8C + }; + + private static readonly byte[][] Rgb4Bytes4X4 = { Rgb4Bytes4X4R, Rgb4Bytes4X4G, Rgb4Bytes4X4B }; + + private static readonly Rgba32[][] Rgb4Result4X4 = + { + new[] { Rgb4_000, Rgb4_FFF, Rgb4_000, Rgb4_FFF }, + new[] { Rgb4_F00, Rgb4_0F0, Rgb4_00F, Rgb4_F0F }, + new[] { Rgb4_400, Rgb4_800, Rgb4_C00, Rgb4_48C }, + new[] { Rgb4_000, Rgb4_444, Rgb4_888, Rgb4_CCC } + }; + + private static readonly byte[] Rgb4Bytes3X4R = + { + 0x0F, 0x00, + 0xF0, 0x00, + 0x48, 0xC0, + 0x04, 0x80 + }; + + private static readonly byte[] Rgb4Bytes3X4G = + { + 0x0F, 0x00, + 0x0F, 0x00, + 0x00, 0x00, + 0x04, 0x80 + }; + + private static readonly byte[] Rgb4Bytes3X4B = + { + 0x0F, 0x00, + 0x00, 0xF0, + 0x00, 0x00, + 0x04, 0x80 + }; + + private static readonly byte[][] Rgb4Bytes3X4 = { Rgb4Bytes3X4R, Rgb4Bytes3X4G, Rgb4Bytes3X4B }; + + private static readonly Rgba32[][] Rgb4Result3X4 = + { + new[] { Rgb4_000, Rgb4_FFF, Rgb4_000 }, + new[] { Rgb4_F00, Rgb4_0F0, Rgb4_00F }, + new[] { Rgb4_400, Rgb4_800, Rgb4_C00 }, + new[] { Rgb4_000, Rgb4_444, Rgb4_888 } + }; + + public static IEnumerable Rgb4Data + { + get + { + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Rgb4Result4X4 }; + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Offset(Rgb4Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 1, 0, 4, 4, Offset(Rgb4Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 1, 4, 4, Offset(Rgb4Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 1, 1, 4, 4, Offset(Rgb4Result4X4, 1, 1, 6, 6) }; + + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Rgb4Result3X4 }; + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Offset(Rgb4Result3X4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 1, 0, 3, 4, Offset(Rgb4Result3X4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 1, 3, 4, Offset(Rgb4Result3X4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 1, 1, 3, 4, Offset(Rgb4Result3X4, 1, 1, 6, 6) }; + } + } + + private static readonly Rgba32 Rgb8_000 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Rgb8_444 = new Rgba32(64, 64, 64, 255); + private static readonly Rgba32 Rgb8_888 = new Rgba32(128, 128, 128, 255); + private static readonly Rgba32 Rgb8_CCC = new Rgba32(192, 192, 192, 255); + private static readonly Rgba32 Rgb8_FFF = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Rgb8_F00 = new Rgba32(255, 0, 0, 255); + private static readonly Rgba32 Rgb8_0F0 = new Rgba32(0, 255, 0, 255); + private static readonly Rgba32 Rgb8_00F = new Rgba32(0, 0, 255, 255); + private static readonly Rgba32 Rgb8_F0F = new Rgba32(255, 0, 255, 255); + private static readonly Rgba32 Rgb8_400 = new Rgba32(64, 0, 0, 255); + private static readonly Rgba32 Rgb8_800 = new Rgba32(128, 0, 0, 255); + private static readonly Rgba32 Rgb8_C00 = new Rgba32(192, 0, 0, 255); + private static readonly Rgba32 Rgb8_48C = new Rgba32(64, 128, 192, 255); + + private static readonly byte[] Rgb8Bytes4X4R = + { + 000, 255, 000, 255, + 255, 000, 000, 255, + 064, 128, 192, 064, + 000, 064, 128, 192 + }; + + private static readonly byte[] Rgb8Bytes4X4G = + { + 000, 255, 000, 255, + 000, 255, 000, 000, + 000, 000, 000, 128, + 000, 064, 128, 192 + }; + + private static readonly byte[] Rgb8Bytes4X4B = + { + 000, 255, 000, 255, + 000, 000, 255, 255, + 000, 000, 000, 192, + 000, 064, 128, 192 + }; + + private static readonly byte[][] Rgb8Bytes4X4 = + { + Rgb8Bytes4X4R, Rgb8Bytes4X4G, Rgb8Bytes4X4B + }; + + private static readonly Rgba32[][] Rgb8Result4X4 = + { + new[] { Rgb8_000, Rgb8_FFF, Rgb8_000, Rgb8_FFF }, + new[] { Rgb8_F00, Rgb8_0F0, Rgb8_00F, Rgb8_F0F }, + new[] { Rgb8_400, Rgb8_800, Rgb8_C00, Rgb8_48C }, + new[] { Rgb8_000, Rgb8_444, Rgb8_888, Rgb8_CCC } + }; + + public static IEnumerable Rgb8Data + { + get + { + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Rgb8Result4X4 }; + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Offset(Rgb8Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 1, 0, 4, 4, Offset(Rgb8Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 1, 4, 4, Offset(Rgb8Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 1, 1, 4, 4, Offset(Rgb8Result4X4, 1, 1, 6, 6) }; + } + } + + private static readonly Rgba32 Rgb484_000 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Rgb484_444 = new Rgba32(68, 64, 68, 255); + private static readonly Rgba32 Rgb484_888 = new Rgba32(136, 128, 136, 255); + private static readonly Rgba32 Rgb484_CCC = new Rgba32(204, 192, 204, 255); + private static readonly Rgba32 Rgb484_FFF = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Rgb484_F00 = new Rgba32(255, 0, 0, 255); + private static readonly Rgba32 Rgb484_0F0 = new Rgba32(0, 255, 0, 255); + private static readonly Rgba32 Rgb484_00F = new Rgba32(0, 0, 255, 255); + private static readonly Rgba32 Rgb484_F0F = new Rgba32(255, 0, 255, 255); + private static readonly Rgba32 Rgb484_400 = new Rgba32(68, 0, 0, 255); + private static readonly Rgba32 Rgb484_800 = new Rgba32(136, 0, 0, 255); + private static readonly Rgba32 Rgb484_C00 = new Rgba32(204, 0, 0, 255); + private static readonly Rgba32 Rgb484_48C = new Rgba32(68, 128, 204, 255); + + private static readonly byte[] Rgb484Bytes4X4R = + { + 0x0F, 0x0F, + 0xF0, 0x0F, + 0x48, 0xC4, + 0x04, 0x8C + }; + + private static readonly byte[] Rgb484Bytes4X4G = + { + 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x80, + 0x00, 0x40, 0x80, 0xC0 + }; + + private static readonly byte[] Rgb484Bytes4X4B = + { + 0x0F, 0x0F, + 0x00, 0xFF, + 0x00, 0x0C, + 0x04, 0x8C + }; + + private static readonly Rgba32[][] Rgb484Result4X4 = + { + new[] { Rgb484_000, Rgb484_FFF, Rgb484_000, Rgb484_FFF }, + new[] { Rgb484_F00, Rgb484_0F0, Rgb484_00F, Rgb484_F0F }, + new[] { Rgb484_400, Rgb484_800, Rgb484_C00, Rgb484_48C }, + new[] { Rgb484_000, Rgb484_444, Rgb484_888, Rgb484_CCC } + }; + + private static readonly byte[][] Rgb484Bytes4X4 = { Rgb484Bytes4X4R, Rgb484Bytes4X4G, Rgb484Bytes4X4B }; + + public static IEnumerable Rgb484_Data + { + get + { + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Rgb484Result4X4 }; + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Offset(Rgb484Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 1, 0, 4, 4, Offset(Rgb484Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 1, 4, 4, Offset(Rgb484Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 1, 1, 4, 4, Offset(Rgb484Result4X4, 1, 1, 6, 6) }; + } + } + + [Theory] + [MemberData(nameof(Rgb4Data))] + [MemberData(nameof(Rgb8Data))] + [MemberData(nameof(Rgb484_Data))] + public void Decode_WritesPixelData(byte[][] inputData, ushort[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + var buffers = new IManagedByteBuffer[inputData.Length]; + for (int i = 0; i < buffers.Length; i++) + { + buffers[i] = Configuration.Default.MemoryAllocator.AllocateManagedByteBuffer(inputData[i].Length); + ((Span)inputData[i]).CopyTo(buffers[i].GetSpan()); + } + + new RgbPlanarTiffColor(bitsPerSample).Decode(buffers, pixels, left, top, width, height); + }); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs new file mode 100644 index 0000000000..4abde8f17e --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs @@ -0,0 +1,186 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation +{ + [Trait("Format", "Tiff")] + public class RgbTiffColorTests : PhotometricInterpretationTestBase + { + private static readonly Rgba32 Rgb4_000 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Rgb4_444 = new Rgba32(68, 68, 68, 255); + private static readonly Rgba32 Rgb4_888 = new Rgba32(136, 136, 136, 255); + private static readonly Rgba32 Rgb4_CCC = new Rgba32(204, 204, 204, 255); + private static readonly Rgba32 Rgb4_FFF = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Rgb4_F00 = new Rgba32(255, 0, 0, 255); + private static readonly Rgba32 Rgb4_0F0 = new Rgba32(0, 255, 0, 255); + private static readonly Rgba32 Rgb4_00F = new Rgba32(0, 0, 255, 255); + private static readonly Rgba32 Rgb4_F0F = new Rgba32(255, 0, 255, 255); + private static readonly Rgba32 Rgb4_400 = new Rgba32(68, 0, 0, 255); + private static readonly Rgba32 Rgb4_800 = new Rgba32(136, 0, 0, 255); + private static readonly Rgba32 Rgb4_C00 = new Rgba32(204, 0, 0, 255); + private static readonly Rgba32 Rgb4_48C = new Rgba32(68, 136, 204, 255); + + private static readonly byte[] Rgb4Bytes4X4 = + { + 0x00, 0x0F, 0xFF, 0x00, 0x0F, 0xFF, + 0xF0, 0x00, 0xF0, 0x00, 0xFF, 0x0F, + 0x40, 0x08, 0x00, 0xC0, 0x04, 0x8C, + 0x00, 0x04, 0x44, 0x88, 0x8C, 0xCC + }; + + private static readonly Rgba32[][] Rgb4Result4X4 = + { + new[] { Rgb4_000, Rgb4_FFF, Rgb4_000, Rgb4_FFF }, + new[] { Rgb4_F00, Rgb4_0F0, Rgb4_00F, Rgb4_F0F }, + new[] { Rgb4_400, Rgb4_800, Rgb4_C00, Rgb4_48C }, + new[] { Rgb4_000, Rgb4_444, Rgb4_888, Rgb4_CCC } + }; + + private static readonly byte[] Rgb4Bytes3X4 = + { + 0x00, 0x0F, 0xFF, 0x00, 0x00, + 0xF0, 0x00, 0xF0, 0x00, 0xF0, + 0x40, 0x08, 0x00, 0xC0, 0x00, + 0x00, 0x04, 0x44, 0x88, 0x80 + }; + + private static readonly Rgba32[][] Rgb4Result3X4 = + { + new[] { Rgb4_000, Rgb4_FFF, Rgb4_000 }, + new[] { Rgb4_F00, Rgb4_0F0, Rgb4_00F }, + new[] { Rgb4_400, Rgb4_800, Rgb4_C00 }, + new[] { Rgb4_000, Rgb4_444, Rgb4_888 } + }; + + public static IEnumerable Rgb4Data + { + get + { + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Rgb4Result4X4 }; + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Offset(Rgb4Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 1, 0, 4, 4, Offset(Rgb4Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 1, 4, 4, Offset(Rgb4Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 1, 1, 4, 4, Offset(Rgb4Result4X4, 1, 1, 6, 6) }; + + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Rgb4Result3X4 }; + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Offset(Rgb4Result3X4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 1, 0, 3, 4, Offset(Rgb4Result3X4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 1, 3, 4, Offset(Rgb4Result3X4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 1, 1, 3, 4, Offset(Rgb4Result3X4, 1, 1, 6, 6) }; + } + } + + private static readonly Rgba32 Rgb8_000 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Rgb8_444 = new Rgba32(64, 64, 64, 255); + private static readonly Rgba32 Rgb8_888 = new Rgba32(128, 128, 128, 255); + private static readonly Rgba32 Rgb8_CCC = new Rgba32(192, 192, 192, 255); + private static readonly Rgba32 Rgb8_FFF = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Rgb8_F00 = new Rgba32(255, 0, 0, 255); + private static readonly Rgba32 Rgb8_0F0 = new Rgba32(0, 255, 0, 255); + private static readonly Rgba32 Rgb8_00F = new Rgba32(0, 0, 255, 255); + private static readonly Rgba32 Rgb8_F0F = new Rgba32(255, 0, 255, 255); + private static readonly Rgba32 Rgb8_400 = new Rgba32(64, 0, 0, 255); + private static readonly Rgba32 Rgb8_800 = new Rgba32(128, 0, 0, 255); + private static readonly Rgba32 Rgb8_C00 = new Rgba32(192, 0, 0, 255); + private static readonly Rgba32 Rgb8_48C = new Rgba32(64, 128, 192, 255); + + private static readonly byte[] Rgb8Bytes4X4 = + { + 000, 000, 000, 255, 255, 255, 000, 000, 000, 255, 255, 255, + 255, 000, 000, 000, 255, 000, 000, 000, 255, 255, 000, 255, + 064, 000, 000, 128, 000, 000, 192, 000, 000, 064, 128, 192, + 000, 000, 000, 064, 064, 064, 128, 128, 128, 192, 192, 192 + }; + + private static readonly Rgba32[][] Rgb8Result4X4 = + { + new[] { Rgb8_000, Rgb8_FFF, Rgb8_000, Rgb8_FFF }, + new[] { Rgb8_F00, Rgb8_0F0, Rgb8_00F, Rgb8_F0F }, + new[] { Rgb8_400, Rgb8_800, Rgb8_C00, Rgb8_48C }, + new[] { Rgb8_000, Rgb8_444, Rgb8_888, Rgb8_CCC } + }; + + public static IEnumerable Rgb8Data + { + get + { + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Rgb8Result4X4 }; + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Offset(Rgb8Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 1, 0, 4, 4, Offset(Rgb8Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 1, 4, 4, Offset(Rgb8Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 1, 1, 4, 4, Offset(Rgb8Result4X4, 1, 1, 6, 6) }; + } + } + + private static readonly Rgba32 Rgb484_000 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Rgb484_444 = new Rgba32(68, 64, 68, 255); + private static readonly Rgba32 Rgb484_888 = new Rgba32(136, 128, 136, 255); + private static readonly Rgba32 Rgb484_CCC = new Rgba32(204, 192, 204, 255); + private static readonly Rgba32 Rgb484_FFF = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Rgb484_F00 = new Rgba32(255, 0, 0, 255); + private static readonly Rgba32 Rgb484_0F0 = new Rgba32(0, 255, 0, 255); + private static readonly Rgba32 Rgb484_00F = new Rgba32(0, 0, 255, 255); + private static readonly Rgba32 Rgb484_F0F = new Rgba32(255, 0, 255, 255); + private static readonly Rgba32 Rgb484_400 = new Rgba32(68, 0, 0, 255); + private static readonly Rgba32 Rgb484_800 = new Rgba32(136, 0, 0, 255); + private static readonly Rgba32 Rgb484_C00 = new Rgba32(204, 0, 0, 255); + private static readonly Rgba32 Rgb484_48C = new Rgba32(68, 128, 204, 255); + + private static readonly byte[] Rgb484Bytes4X4 = + { + 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, + 0xF0, 0x00, 0x0F, 0xF0, 0x00, 0x0F, 0xF0, 0x0F, + 0x40, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x48, 0x0C, + 0x00, 0x00, 0x44, 0x04, 0x88, 0x08, 0xCC, 0x0C + }; + + private static readonly Rgba32[][] Rgb484Result4X4 = + { + new[] { Rgb484_000, Rgb484_FFF, Rgb484_000, Rgb484_FFF }, + new[] { Rgb484_F00, Rgb484_0F0, Rgb484_00F, Rgb484_F0F }, + new[] { Rgb484_400, Rgb484_800, Rgb484_C00, Rgb484_48C }, + new[] { Rgb484_000, Rgb484_444, Rgb484_888, Rgb484_CCC } + }; + + public static IEnumerable Rgb484Data + { + get + { + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Rgb484Result4X4 }; + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Offset(Rgb484Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 1, 0, 4, 4, Offset(Rgb484Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 1, 4, 4, Offset(Rgb484Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 1, 1, 4, 4, Offset(Rgb484Result4X4, 1, 1, 6, 6) }; + } + } + + [Theory] + [MemberData(nameof(Rgb4Data))] + [MemberData(nameof(Rgb8Data))] + [MemberData(nameof(Rgb484Data))] + public void Decode_WritesPixelData(byte[] inputData, ushort[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new RgbTiffColor(bitsPerSample).Decode(inputData, pixels, left, top, width, height); + }); + } + + [Theory] + [MemberData(nameof(Rgb8Data))] + public void Decode_WritesPixelData_8Bit(byte[] inputData, ushort[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new Rgb888TiffColor().Decode(inputData, pixels, left, top, width, height); + }); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs new file mode 100644 index 0000000000..620fddd7d0 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs @@ -0,0 +1,195 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation +{ + [Trait("Format", "Tiff")] + public class WhiteIsZeroTiffColorTests : PhotometricInterpretationTestBase + { + private static readonly Rgba32 Gray000 = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Gray128 = new Rgba32(127, 127, 127, 255); + private static readonly Rgba32 Gray255 = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Gray0 = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Gray8 = new Rgba32(119, 119, 119, 255); + private static readonly Rgba32 GrayF = new Rgba32(0, 0, 0, 255); + private static readonly Rgba32 Bit0 = new Rgba32(255, 255, 255, 255); + private static readonly Rgba32 Bit1 = new Rgba32(0, 0, 0, 255); + + private static readonly byte[] BilevelBytes4X4 = + { + 0b01010000, + 0b11110000, + 0b01110000, + 0b10010000 + }; + + private static readonly Rgba32[][] BilevelResult4X4 = + { + new[] { Bit0, Bit1, Bit0, Bit1 }, + new[] { Bit1, Bit1, Bit1, Bit1 }, + new[] { Bit0, Bit1, Bit1, Bit1 }, + new[] { Bit1, Bit0, Bit0, Bit1 } + }; + + private static readonly byte[] BilevelBytes12X4 = + { + 0b01010101, 0b01010000, + 0b11111111, 0b11111111, + 0b01101001, 0b10100000, + 0b10010000, 0b01100000 + }; + + private static readonly Rgba32[][] BilevelResult12X4 = + { + new[] { Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1 }, + new[] { Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1 }, + new[] { Bit0, Bit1, Bit1, Bit0, Bit1, Bit0, Bit0, Bit1, Bit1, Bit0, Bit1, Bit0 }, + new[] { Bit1, Bit0, Bit0, Bit1, Bit0, Bit0, Bit0, Bit0, Bit0, Bit1, Bit1, Bit0 } + }; + + private static readonly byte[] Grayscale4Bytes4X4 = + { + 0x8F, 0x0F, + 0xFF, 0xFF, + 0x08, 0x8F, + 0xF0, 0xF8 + }; + + private static readonly Rgba32[][] Grayscale4Result4X4 = + { + new[] { Gray8, GrayF, Gray0, GrayF }, + new[] { GrayF, GrayF, GrayF, GrayF }, + new[] { Gray0, Gray8, Gray8, GrayF }, + new[] { GrayF, Gray0, GrayF, Gray8 } + }; + + private static readonly byte[] Grayscale4Bytes3X4 = + { + 0x8F, 0x00, + 0xFF, 0xF0, + 0x08, 0x80, + 0xF0, 0xF0 + }; + + private static readonly Rgba32[][] Grayscale4Result3X4 = + { + new[] { Gray8, GrayF, Gray0 }, + new[] { GrayF, GrayF, GrayF }, + new[] { Gray0, Gray8, Gray8 }, + new[] { GrayF, Gray0, GrayF } + }; + + private static readonly byte[] Grayscale8Bytes4X4 = + { + 128, 255, 000, 255, + 255, 255, 255, 255, + 000, 128, 128, 255, + 255, 000, 255, 128 + }; + + private static readonly Rgba32[][] Grayscale8Result4X4 = + { + new[] { Gray128, Gray255, Gray000, Gray255 }, + new[] { Gray255, Gray255, Gray255, Gray255 }, + new[] { Gray000, Gray128, Gray128, Gray255 }, + new[] { Gray255, Gray000, Gray255, Gray128 } + }; + + public static IEnumerable BilevelData + { + get + { + yield return new object[] { BilevelBytes4X4, 1, 0, 0, 4, 4, BilevelResult4X4 }; + yield return new object[] { BilevelBytes4X4, 1, 0, 0, 4, 4, Offset(BilevelResult4X4, 0, 0, 6, 6) }; + yield return new object[] { BilevelBytes4X4, 1, 1, 0, 4, 4, Offset(BilevelResult4X4, 1, 0, 6, 6) }; + yield return new object[] { BilevelBytes4X4, 1, 0, 1, 4, 4, Offset(BilevelResult4X4, 0, 1, 6, 6) }; + yield return new object[] { BilevelBytes4X4, 1, 1, 1, 4, 4, Offset(BilevelResult4X4, 1, 1, 6, 6) }; + + yield return new object[] { BilevelBytes12X4, 1, 0, 0, 12, 4, BilevelResult12X4 }; + yield return new object[] { BilevelBytes12X4, 1, 0, 0, 12, 4, Offset(BilevelResult12X4, 0, 0, 18, 6) }; + yield return new object[] { BilevelBytes12X4, 1, 1, 0, 12, 4, Offset(BilevelResult12X4, 1, 0, 18, 6) }; + yield return new object[] { BilevelBytes12X4, 1, 0, 1, 12, 4, Offset(BilevelResult12X4, 0, 1, 18, 6) }; + yield return new object[] { BilevelBytes12X4, 1, 1, 1, 12, 4, Offset(BilevelResult12X4, 1, 1, 18, 6) }; + } + } + + public static IEnumerable Grayscale4Data + { + get + { + yield return new object[] { Grayscale4Bytes4X4, 4, 0, 0, 4, 4, Grayscale4Result4X4 }; + yield return new object[] { Grayscale4Bytes4X4, 4, 0, 0, 4, 4, Offset(Grayscale4Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Grayscale4Bytes4X4, 4, 1, 0, 4, 4, Offset(Grayscale4Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Grayscale4Bytes4X4, 4, 0, 1, 4, 4, Offset(Grayscale4Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Grayscale4Bytes4X4, 4, 1, 1, 4, 4, Offset(Grayscale4Result4X4, 1, 1, 6, 6) }; + + yield return new object[] { Grayscale4Bytes3X4, 4, 0, 0, 3, 4, Grayscale4Result3X4 }; + yield return new object[] { Grayscale4Bytes3X4, 4, 0, 0, 3, 4, Offset(Grayscale4Result3X4, 0, 0, 6, 6) }; + yield return new object[] { Grayscale4Bytes3X4, 4, 1, 0, 3, 4, Offset(Grayscale4Result3X4, 1, 0, 6, 6) }; + yield return new object[] { Grayscale4Bytes3X4, 4, 0, 1, 3, 4, Offset(Grayscale4Result3X4, 0, 1, 6, 6) }; + yield return new object[] { Grayscale4Bytes3X4, 4, 1, 1, 3, 4, Offset(Grayscale4Result3X4, 1, 1, 6, 6) }; + } + } + + public static IEnumerable Grayscale8Data + { + get + { + yield return new object[] { Grayscale8Bytes4X4, 8, 0, 0, 4, 4, Grayscale8Result4X4 }; + yield return new object[] { Grayscale8Bytes4X4, 8, 0, 0, 4, 4, Offset(Grayscale8Result4X4, 0, 0, 6, 6) }; + yield return new object[] { Grayscale8Bytes4X4, 8, 1, 0, 4, 4, Offset(Grayscale8Result4X4, 1, 0, 6, 6) }; + yield return new object[] { Grayscale8Bytes4X4, 8, 0, 1, 4, 4, Offset(Grayscale8Result4X4, 0, 1, 6, 6) }; + yield return new object[] { Grayscale8Bytes4X4, 8, 1, 1, 4, 4, Offset(Grayscale8Result4X4, 1, 1, 6, 6) }; + } + } + + [Theory] + [MemberData(nameof(BilevelData))] + [MemberData(nameof(Grayscale4Data))] + [MemberData(nameof(Grayscale8Data))] + public void Decode_WritesPixelData(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new WhiteIsZeroTiffColor(new[] { (ushort)bitsPerSample }).Decode(inputData, pixels, left, top, width, height); + }); + } + + [Theory] + [MemberData(nameof(BilevelData))] + public void Decode_WritesPixelData_Bilevel(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new WhiteIsZero1TiffColor().Decode(inputData, pixels, left, top, width, height); + }); + } + + [Theory] + [MemberData(nameof(Grayscale4Data))] + public void Decode_WritesPixelData_4Bit(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new WhiteIsZero4TiffColor().Decode(inputData, pixels, left, top, width, height); + }); + } + + [Theory] + [MemberData(nameof(Grayscale8Data))] + public void Decode_WritesPixelData_8Bit(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + new WhiteIsZero8TiffColor().Decode(inputData, pixels, left, top, width, height); + }); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs new file mode 100644 index 0000000000..bffb603028 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +using System; +using System.IO; + +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +using Xunit; + +using static SixLabors.ImageSharp.Tests.TestImages.Tiff; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class TiffDecoderTests + { + public static readonly string[] MultiframeTestImages = Multiframes; + + public static readonly string[] NotSupportedImages = NotSupported; + + private static TiffDecoder TiffDecoder => new TiffDecoder(); + + private static MagickReferenceDecoder ReferenceDecoder => new MagickReferenceDecoder(); + + private readonly Configuration configuration; + + public TiffDecoderTests() + { + this.configuration = new Configuration(); + this.configuration.AddTiff(); + } + + [Theory] + [WithFileCollection(nameof(NotSupportedImages), PixelTypes.Rgba32)] + public void ThrowsNotSupported(TestImageProvider provider) + where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder)); + + [Theory] + [InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)] + [InlineData(SmallRgbDeflate, 24, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(Calliphora_GrayscaleUncompressed, 8, 804, 1198, 96, 96, PixelResolutionUnit.PixelsPerInch)] + public void Identify(string imagePath, int expectedPixelSize, int expectedWidth, int expectedHeight, double expectedHResolution, double expectedVResolution, PixelResolutionUnit expectedResolutionUnit) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo info = Image.Identify(this.configuration, stream); + + Assert.Equal(expectedPixelSize, info.PixelType?.BitsPerPixel); + Assert.Equal(expectedWidth, info.Width); + Assert.Equal(expectedHeight, info.Height); + Assert.NotNull(info.Metadata); + Assert.Equal(expectedHResolution, info.Metadata.HorizontalResolution); + Assert.Equal(expectedVResolution, info.Metadata.VerticalResolution); + Assert.Equal(expectedResolutionUnit, info.Metadata.ResolutionUnits); + } + } + + [Theory] + [InlineData(RgbLzwNoPredictorMultistrip, ImageSharp.ByteOrder.LittleEndian)] + [InlineData(RgbLzwNoPredictorMultistripMotorola, ImageSharp.ByteOrder.BigEndian)] + public void ByteOrder(string imagePath, ByteOrder expectedByteOrder) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo info = Image.Identify(this.configuration, stream); + + Assert.NotNull(info.Metadata); + Assert.Equal(expectedByteOrder, info.Metadata.GetTiffMetadata().ByteOrder); + + stream.Seek(0, SeekOrigin.Begin); + + using var img = Image.Load(this.configuration, stream); + Assert.Equal(expectedByteOrder, img.Metadata.GetTiffMetadata().ByteOrder); + } + } + + [Theory] + [WithFile(RgbUncompressed, PixelTypes.Rgba32)] + [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Uncompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] + [WithFile(PaletteDeflateMultistrip, PixelTypes.Rgba32)] + [WithFile(RgbPalette, PixelTypes.Rgba32)] + [WithFile(RgbPaletteDeflate, PixelTypes.Rgba32)] + [WithFile(PaletteUncompressed, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_WithPalette(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(GrayscaleDeflateMultistrip, PixelTypes.Rgba32)] + [WithFile(RgbDeflateMultistrip, PixelTypes.Rgba32)] + [WithFile(Calliphora_GrayscaleDeflate, PixelTypes.Rgba32)] + [WithFile(Calliphora_GrayscaleDeflate_Predictor, PixelTypes.Rgba32)] + [WithFile(Calliphora_RgbDeflate_Predictor, PixelTypes.Rgba32)] + [WithFile(RgbDeflate, PixelTypes.Rgba32)] + [WithFile(RgbDeflatePredictor, PixelTypes.Rgba32)] + [WithFile(SmallRgbDeflate, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_DeflateCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(RgbLzwPredictor, PixelTypes.Rgba32)] + [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32)] + [WithFile(RgbLzwNoPredictorSinglestripMotorola, PixelTypes.Rgba32)] + [WithFile(RgbLzwNoPredictorMultistripMotorola, PixelTypes.Rgba32)] + [WithFile(RgbLzwMultistripPredictor, PixelTypes.Rgba32)] + [WithFile(Calliphora_RgbPaletteLzw_Predictor, PixelTypes.Rgba32)] + [WithFile(Calliphora_RgbLzwPredictor, PixelTypes.Rgba32)] + [WithFile(Calliphora_GrayscaleLzw_Predictor, PixelTypes.Rgba32)] + [WithFile(SmallRgbLzw, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_LzwCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(HuffmanRleAllTermCodes, PixelTypes.Rgba32)] + [WithFile(HuffmanRleAllMakeupCodes, PixelTypes.Rgba32)] + [WithFile(HuffmanRle_basi3p02, PixelTypes.Rgba32)] + [WithFile(Calliphora_HuffmanCompressed, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_HuffmanCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(CcittFax3AllTermCodes, PixelTypes.Rgba32)] + [WithFile(CcittFax3AllMakeupCodes, PixelTypes.Rgba32)] + [WithFile(Calliphora_Fax3Compressed, PixelTypes.Rgba32)] + [WithFile(Calliphora_Fax3Compressed_WithEolPadding, PixelTypes.Rgba32)] + [WithFile(Fax3Uncompressed, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Fax3Compressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Calliphora_RgbPackbits, PixelTypes.Rgba32)] + [WithFile(RgbPackbits, PixelTypes.Rgba32)] + [WithFile(RgbPackbitsMultistrip, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_PackBitsCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)] + public void DecodeMultiframe(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder); + Assert.True(image.Frames.Count > 1); + + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + + image.DebugSaveMultiFrame(provider); + image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, ReferenceDecoder); + } + + private static void TestTiffDecoder(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs new file mode 100644 index 0000000000..acbe2b4896 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Writers; +using SixLabors.ImageSharp.Memory; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class TiffEncoderHeaderTests + { + 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() + { + using var stream = new MemoryStream(); + var encoder = new TiffEncoderCore(Options, MemoryAllocator); + + using (var writer = new TiffStreamWriter(stream)) + { + long firstIfdMarker = encoder.WriteHeader(writer); + } + + Assert.Equal(new byte[] { 0x49, 0x49, 42, 0, 0x00, 0x00, 0x00, 0x00 }, stream.ToArray()); + } + + [Fact] + public void WriteHeader_ReturnsFirstIfdMarker() + { + using var stream = new MemoryStream(); + var encoder = new TiffEncoderCore(Options, MemoryAllocator); + + using (var writer = new TiffStreamWriter(stream)) + { + long firstIfdMarker = encoder.WriteHeader(writer); + Assert.Equal(4, firstIfdMarker); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs new file mode 100644 index 0000000000..a40ca04bda --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -0,0 +1,494 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +using Xunit; + +using static SixLabors.ImageSharp.Tests.TestImages.Tiff; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class TiffEncoderTests + { + private static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); + + private static readonly Configuration Configuration; + + static TiffEncoderTests() + { + Configuration = new Configuration(); + Configuration.AddTiff(); + } + + [Theory] + [InlineData(null, TiffBitsPerPixel.Bit24)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)] + [InlineData(TiffPhotometricInterpretation.PaletteColor, TiffBitsPerPixel.Bit8)] + [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffBitsPerPixel.Bit8)] + [InlineData(TiffPhotometricInterpretation.WhiteIsZero, TiffBitsPerPixel.Bit8)] + //// Unsupported TiffPhotometricInterpretation should default to 24 bits + [InlineData(TiffPhotometricInterpretation.CieLab, TiffBitsPerPixel.Bit24)] + [InlineData(TiffPhotometricInterpretation.ColorFilterArray, TiffBitsPerPixel.Bit24)] + [InlineData(TiffPhotometricInterpretation.ItuLab, TiffBitsPerPixel.Bit24)] + [InlineData(TiffPhotometricInterpretation.LinearRaw, TiffBitsPerPixel.Bit24)] + [InlineData(TiffPhotometricInterpretation.Separated, TiffBitsPerPixel.Bit24)] + [InlineData(TiffPhotometricInterpretation.TransparencyMask, TiffBitsPerPixel.Bit24)] + public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffBitsPerPixel expectedBitsPerPixel) + { + // arrange + var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation }; + using Image input = new Image(10, 10); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(Configuration, memStream); + TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); + Assert.Equal(TiffCompression.None, frameMetaData.Compression); + } + + [Theory] + [InlineData(TiffBitsPerPixel.Bit24)] + [InlineData(TiffBitsPerPixel.Bit8)] + [InlineData(TiffBitsPerPixel.Bit4)] + [InlineData(TiffBitsPerPixel.Bit1)] + public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel) + { + // arrange + var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel }; + using Image input = new Image(10, 10); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(Configuration, memStream); + + TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(bitsPerPixel, frameMetaData.BitsPerPixel); + Assert.Equal(TiffCompression.None, frameMetaData.Compression); + } + + [Theory] + [InlineData(null, TiffCompression.Deflate, TiffBitsPerPixel.Bit24, TiffCompression.Deflate)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate, TiffBitsPerPixel.Bit24, TiffCompression.Deflate)] + [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate, TiffBitsPerPixel.Bit8, TiffCompression.Deflate)] + [InlineData(TiffPhotometricInterpretation.PaletteColor, TiffCompression.Deflate, TiffBitsPerPixel.Bit8, TiffCompression.Deflate)] + [InlineData(null, TiffCompression.PackBits, TiffBitsPerPixel.Bit24, TiffCompression.PackBits)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.PackBits, TiffBitsPerPixel.Bit24, TiffCompression.PackBits)] + [InlineData(TiffPhotometricInterpretation.PaletteColor, TiffCompression.PackBits, TiffBitsPerPixel.Bit8, TiffCompression.PackBits)] + [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits, TiffBitsPerPixel.Bit8, TiffCompression.PackBits)] + [InlineData(null, TiffCompression.Lzw, TiffBitsPerPixel.Bit24, TiffCompression.Lzw)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Lzw, TiffBitsPerPixel.Bit24, TiffCompression.Lzw)] + [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw, TiffBitsPerPixel.Bit8, TiffCompression.Lzw)] + [InlineData(TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, TiffBitsPerPixel.Bit8, TiffCompression.Lzw)] + [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax, TiffBitsPerPixel.Bit1, TiffCompression.CcittGroup3Fax)] + [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Ccitt1D, TiffBitsPerPixel.Bit1, TiffCompression.Ccitt1D)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT43, TiffBitsPerPixel.Bit24, TiffCompression.None)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT82, TiffBitsPerPixel.Bit24, TiffCompression.None)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Jpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldDeflate, TiffBitsPerPixel.Bit24, TiffCompression.None)] + [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.OldJpeg, TiffBitsPerPixel.Bit24, TiffCompression.None)] + public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffBitsPerPixel expectedBitsPerPixel, TiffCompression expectedCompression) + { + // arrange + var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression }; + using Image input = new Image(10, 10); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(Configuration, memStream); + TiffFrameMetadata rootFrameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(expectedBitsPerPixel, rootFrameMetaData.BitsPerPixel); + Assert.Equal(expectedCompression, rootFrameMetaData.Compression); + } + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32, TiffBitsPerPixel.Bit1)] + [WithFile(GrayscaleUncompressed, PixelTypes.Rgba32, TiffBitsPerPixel.Bit8)] + [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffBitsPerPixel.Bit24)] + [WithFile(Rgb4BitPalette, PixelTypes.Rgba32, TiffBitsPerPixel.Bit4)] + [WithFile(RgbPalette, PixelTypes.Rgba32, TiffBitsPerPixel.Bit8)] + [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32, TiffBitsPerPixel.Bit8)] + public void TiffEncoder_PreservesBitsPerPixel(TestImageProvider provider, TiffBitsPerPixel expectedBitsPerPixel) + where TPixel : unmanaged, IPixel + { + // arrange + var tiffEncoder = new TiffEncoder(); + using Image input = provider.GetImage(); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(Configuration, memStream); + TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); + } + + [Fact] + public void TiffEncoder_PreservesBitsPerPixel_WhenInputIsL8() + { + // arrange + var tiffEncoder = new TiffEncoder(); + using Image input = new Image(10, 10); + using var memStream = new MemoryStream(); + TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8; + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(Configuration, memStream); + TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); + } + + [Theory] + [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.None)] + [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32, TiffCompression.Lzw)] + [WithFile(RgbDeflate, PixelTypes.Rgba32, TiffCompression.Deflate)] + [WithFile(RgbPackbits, PixelTypes.Rgba32, TiffCompression.PackBits)] + public void TiffEncoder_PreservesCompression(TestImageProvider provider, TiffCompression expectedCompression) + where TPixel : unmanaged, IPixel + { + // arrange + var tiffEncoder = new TiffEncoder(); + using Image input = provider.GetImage(); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(Configuration, memStream); + Assert.Equal(expectedCompression, output.Frames.RootFrame.Metadata.GetTiffMetadata().Compression); + } + + [Theory] + [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32, null)] + [WithFile(RgbLzwPredictor, PixelTypes.Rgba32, TiffPredictor.Horizontal)] + [WithFile(RgbDeflate, PixelTypes.Rgba32, null)] + [WithFile(RgbDeflatePredictor, PixelTypes.Rgba32, TiffPredictor.Horizontal)] + public void TiffEncoder_PreservesPredictor(TestImageProvider provider, TiffPredictor? expectedPredictor) + where TPixel : unmanaged, IPixel + { + // arrange + var tiffEncoder = new TiffEncoder(); + using Image input = provider.GetImage(); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(Configuration, memStream); + TiffFrameMetadata frameMetadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(expectedPredictor, frameMetadata.Predictor); + } + + [Theory] + [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.CcittGroup3Fax, TiffCompression.CcittGroup3Fax)] + [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.Ccitt1D, TiffCompression.Ccitt1D)] + [WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffCompression.CcittGroup3Fax, TiffCompression.CcittGroup3Fax)] + [WithFile(PaletteDeflateMultistrip, PixelTypes.L8, TiffCompression.Ccitt1D, TiffCompression.Ccitt1D)] + public void TiffEncoder_EncodesWithCorrectBiColorModeCompression(TestImageProvider provider, TiffCompression compression, TiffCompression expectedCompression) + where TPixel : unmanaged, IPixel + { + // arrange + var encoder = new TiffEncoder() { Compression = compression, BitsPerPixel = TiffBitsPerPixel.Bit1 }; + using Image input = provider.GetImage(); + using var memStream = new MemoryStream(); + + // act + input.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(Configuration, memStream); + TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(TiffBitsPerPixel.Bit1, frameMetaData.BitsPerPixel); + Assert.Equal(expectedCompression, frameMetaData.Compression); + } + + [Theory] + [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeRgb_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); + + [Theory] + [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeRgb_WithDeflateCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate); + + [Theory] + [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeRgb_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate, TiffPredictor.Horizontal); + + [Theory] + [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeRgb_WithLzwCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Lzw); + + [Theory] + [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeRgb_WithLzwCompressionAndPredictor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.Lzw, TiffPredictor.Horizontal); + + [Theory] + [WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeRgb_WithPackBitsCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, TiffCompression.PackBits); + + [Theory] + [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeGray_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero); + + [Theory] + [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeGray_WithDeflateCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate); + + [Theory] + [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeGray_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate, TiffPredictor.Horizontal); + + [Theory] + [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeGray_WithLzwCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw); + + [Theory] + [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeGray_WithLzwCompressionAndPredictor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw, TiffPredictor.Horizontal); + + [Theory] + [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeGray_WithPackBitsCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits); + + [Theory] + [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeColorPalette_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, useExactComparer: false, compareTolerance: 0.001f); + + [Theory] + [WithFile(Rgb4BitPalette, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeColorPalette_With4Bit_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => + //// Note: The magick reference decoder does not support 4 bit tiff's, so we use our TIFF decoder instead. + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit4, TiffPhotometricInterpretation.PaletteColor, useExactComparer: false, compareTolerance: 0.001f, imageDecoder: new TiffDecoder()); + + [Theory] + [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeColorPalette_WithPackBitsCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.PackBits, useExactComparer: false, compareTolerance: 0.001f); + + [Theory] + [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeColorPalette_WithDeflateCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Deflate, useExactComparer: false, compareTolerance: 0.001f); + + [Theory] + [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeColorPalette_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Deflate, TiffPredictor.Horizontal, useExactComparer: false, compareTolerance: 0.001f); + + [Theory] + [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeColorPalette_WithLzwCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, useExactComparer: false, compareTolerance: 0.001f); + + [Theory] + [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeColorPalette_WithLzwCompressionAndPredictor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => + TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, TiffPredictor.Horizontal, useExactComparer: false, compareTolerance: 0.001f); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_BlackIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WhiteIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithDeflateCompression_BlackIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithDeflateCompression_WhiteIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.Deflate); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithPackBitsCompression_BlackIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithPackBitsCompression_WhiteIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.PackBits); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_WhiteIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.CcittGroup3Fax); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_BlackIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_WhiteIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.Ccitt1D); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_BlackIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Ccitt1D); + + [Theory] + [WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits)] + [WithFile(PaletteDeflateMultistrip, PixelTypes.L8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw)] + [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate)] + [WithFile(RgbUncompressed, PixelTypes.Rgb24, TiffPhotometricInterpretation.Rgb, TiffCompression.None)] + [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffPhotometricInterpretation.Rgb, TiffCompression.None)] + [WithFile(RgbUncompressed, PixelTypes.Rgb48, TiffPhotometricInterpretation.Rgb, TiffCompression.None)] + public void TiffEncoder_StripLength(TestImageProvider provider, TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression) + where TPixel : unmanaged, IPixel => + TestStripLength(provider, photometricInterpretation, compression); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.L8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax)] + public void TiffEncoder_StripLength_OutOfBounds(TestImageProvider provider, TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression) + where TPixel : unmanaged, IPixel => + //// CcittGroup3Fax compressed data length can be larger than the original length. + Assert.Throws(() => TestStripLength(provider, photometricInterpretation, compression)); + + private static void TestStripLength(TestImageProvider provider, TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression) + where TPixel : unmanaged, IPixel + { + // arrange + var tiffEncoder = new TiffEncoder() { PhotometricInterpretation = photometricInterpretation, Compression = compression }; + using Image input = provider.GetImage(); + using var memStream = new MemoryStream(); + TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); + TiffCompression inputCompression = inputMeta.Compression ?? TiffCompression.None; + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(Configuration, memStream); + ExifProfile exifProfileOutput = output.Frames.RootFrame.Metadata.ExifProfile; + TiffFrameMetadata outputMeta = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + ImageFrame rootFrame = output.Frames.RootFrame; + + Number rowsPerStrip = exifProfileOutput.GetValue(ExifTag.RowsPerStrip) != null ? exifProfileOutput.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; + Assert.True(output.Height > (int)rowsPerStrip); + Assert.True(exifProfileOutput.GetValue(ExifTag.StripOffsets)?.Value.Length > 1); + Number[] stripByteCounts = exifProfileOutput.GetValue(ExifTag.StripByteCounts)?.Value; + Assert.NotNull(stripByteCounts); + Assert.True(stripByteCounts.Length > 1); + Assert.NotNull(outputMeta.BitsPerPixel); + + foreach (Number sz in stripByteCounts) + { + Assert.True((uint)sz <= TiffConstants.DefaultStripSize); + } + + // For uncompressed more accurate test. + if (compression == TiffCompression.None) + { + for (int i = 0; i < stripByteCounts.Length - 1; i++) + { + // The difference must be less than one row. + int stripBytes = (int)stripByteCounts[i]; + int widthBytes = ((int)outputMeta.BitsPerPixel + 7) / 8 * rootFrame.Width; + + Assert.True((TiffConstants.DefaultStripSize - stripBytes) < widthBytes); + } + } + + // Compare with reference. + TestTiffEncoderCore( + provider, + inputMeta.BitsPerPixel, + photometricInterpretation, + inputCompression); + } + + private static void TestTiffEncoderCore( + TestImageProvider provider, + TiffBitsPerPixel? bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression = TiffCompression.None, + TiffPredictor predictor = TiffPredictor.None, + bool useExactComparer = true, + float compareTolerance = 0.01f, + IImageDecoder imageDecoder = null) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + var encoder = new TiffEncoder + { + PhotometricInterpretation = photometricInterpretation, + BitsPerPixel = bitsPerPixel, + Compression = compression, + HorizontalPredictor = predictor + }; + + // Does DebugSave & load reference CompareToReferenceInput(): + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), + referenceDecoder: imageDecoder ?? ReferenceDecoder); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffFormatTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffFormatTests.cs new file mode 100644 index 0000000000..4510bf9127 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffFormatTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Tiff; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class TiffFormatTests + { + [Fact] + public void FormatProperties_AreAsExpected() + { + TiffFormat tiffFormat = TiffFormat.Instance; + + Assert.Equal("TIFF", tiffFormat.Name); + Assert.Equal("image/tiff", tiffFormat.DefaultMimeType); + Assert.Contains("image/tiff", tiffFormat.MimeTypes); + Assert.Contains("tif", tiffFormat.FileExtensions); + Assert.Contains("tiff", tiffFormat.FileExtensions); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs new file mode 100644 index 0000000000..f52b74e833 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -0,0 +1,305 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Linq; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +using static SixLabors.ImageSharp.Tests.TestImages.Tiff; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class TiffMetadataTests + { + private readonly Configuration configuration; + + private static TiffDecoder TiffDecoder => new TiffDecoder(); + + public TiffMetadataTests() + { + this.configuration = new Configuration(); + this.configuration.AddTiff(); + } + + [Fact] + public void TiffMetadata_CloneIsDeep() + { + var meta = new TiffMetadata + { + ByteOrder = ByteOrder.BigEndian, + }; + + var clone = (TiffMetadata)meta.DeepClone(); + + clone.ByteOrder = ByteOrder.LittleEndian; + + Assert.False(meta.ByteOrder == clone.ByteOrder); + } + + [Theory] + [WithFile(SampleMetadata, PixelTypes.Rgba32)] + public void TiffFrameMetadata_CloneIsDeep(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(TiffDecoder)) + { + TiffFrameMetadata meta = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + var cloneSameAsSampleMetaData = (TiffFrameMetadata)meta.DeepClone(); + VerifyExpectedTiffFrameMetaDataIsPresent(cloneSameAsSampleMetaData); + + var clone = (TiffFrameMetadata)meta.DeepClone(); + + clone.BitsPerPixel = TiffBitsPerPixel.Bit8; + clone.Compression = TiffCompression.None; + clone.PhotometricInterpretation = TiffPhotometricInterpretation.CieLab; + clone.Predictor = TiffPredictor.Horizontal; + + Assert.False(meta.BitsPerPixel == clone.BitsPerPixel); + Assert.False(meta.Compression == clone.Compression); + Assert.False(meta.PhotometricInterpretation == clone.PhotometricInterpretation); + Assert.False(meta.Predictor == clone.Predictor); + } + } + + private static void VerifyExpectedTiffFrameMetaDataIsPresent(TiffFrameMetadata frameMetaData) + { + Assert.NotNull(frameMetaData); + Assert.NotNull(frameMetaData.BitsPerPixel); + Assert.Equal(TiffBitsPerSample.Bit4, (TiffBitsPerSample)frameMetaData.BitsPerPixel); + Assert.Equal(TiffCompression.Lzw, frameMetaData.Compression); + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frameMetaData.PhotometricInterpretation); + Assert.Equal(TiffPredictor.None, frameMetaData.Predictor); + } + + [Theory] + [InlineData(Calliphora_BiColorUncompressed, 1)] + [InlineData(GrayscaleUncompressed, 8)] + [InlineData(RgbUncompressed, 24)] + public void Identify_DetectsCorrectBitPerPixel(string imagePath, int expectedBitsPerPixel) + { + var testFile = TestFile.Create(imagePath); + using var stream = new MemoryStream(testFile.Bytes, false); + + IImageInfo imageInfo = Image.Identify(this.configuration, stream); + + Assert.NotNull(imageInfo); + TiffMetadata tiffMetadata = imageInfo.Metadata.GetTiffMetadata(); + Assert.NotNull(tiffMetadata); + Assert.Equal(expectedBitsPerPixel, imageInfo.PixelType.BitsPerPixel); + } + + [Theory] + [InlineData(GrayscaleUncompressed, ByteOrder.BigEndian)] + [InlineData(LittleEndianByteOrder, ByteOrder.LittleEndian)] + public void Identify_DetectsCorrectByteOrder(string imagePath, ByteOrder expectedByteOrder) + { + var testFile = TestFile.Create(imagePath); + using var stream = new MemoryStream(testFile.Bytes, false); + + IImageInfo imageInfo = Image.Identify(this.configuration, stream); + + Assert.NotNull(imageInfo); + TiffMetadata tiffMetadata = imageInfo.Metadata.GetTiffMetadata(); + Assert.NotNull(tiffMetadata); + Assert.Equal(expectedByteOrder, tiffMetadata.ByteOrder); + } + + [Theory] + [WithFile(SampleMetadata, PixelTypes.Rgba32, false)] + [WithFile(SampleMetadata, PixelTypes.Rgba32, true)] + public void MetadataProfiles(TestImageProvider provider, bool ignoreMetadata) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new TiffDecoder() { IgnoreMetadata = ignoreMetadata })) + { + TiffMetadata meta = image.Metadata.GetTiffMetadata(); + ImageFrameMetadata rootFrameMetaData = image.Frames.RootFrame.Metadata; + Assert.NotNull(meta); + if (ignoreMetadata) + { + Assert.Null(rootFrameMetaData.XmpProfile); + Assert.Null(rootFrameMetaData.ExifProfile); + } + else + { + Assert.NotNull(rootFrameMetaData.XmpProfile); + Assert.NotNull(rootFrameMetaData.ExifProfile); + Assert.Equal(2599, rootFrameMetaData.XmpProfile.Length); + Assert.Equal(26, rootFrameMetaData.ExifProfile.Values.Count); + } + } + } + + [Theory] + [WithFile(InvalidIptcData, PixelTypes.Rgba32)] + public void CanDecodeImage_WithIptcDataAsLong(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder); + + IptcProfile iptcProfile = image.Frames.RootFrame.Metadata.IptcProfile; + Assert.NotNull(iptcProfile); + IptcValue byline = iptcProfile.Values.FirstOrDefault(data => data.Tag == IptcTag.Byline); + Assert.NotNull(byline); + Assert.Equal("Studio Mantyniemi", byline.Value); + } + + [Theory] + [WithFile(SampleMetadata, PixelTypes.Rgba32)] + public void BaselineTags(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(TiffDecoder)) + { + ImageFrame rootFrame = image.Frames.RootFrame; + Assert.Equal(32, rootFrame.Width); + Assert.Equal(32, rootFrame.Height); + Assert.NotNull(rootFrame.Metadata.XmpProfile); + Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Length); + + ExifProfile exifProfile = rootFrame.Metadata.ExifProfile; + TiffFrameMetadata tiffFrameMetadata = rootFrame.Metadata.GetTiffMetadata(); + Assert.NotNull(exifProfile); + + // The original exifProfile has 30 values, but 4 of those values will be stored in the TiffFrameMetaData + // and removed from the profile on decode. + Assert.Equal(26, exifProfile.Values.Count); + Assert.Equal(TiffBitsPerPixel.Bit4, tiffFrameMetadata.BitsPerPixel); + Assert.Equal(TiffCompression.Lzw, tiffFrameMetadata.Compression); + Assert.Equal("This is Название", exifProfile.GetValue(ExifTag.ImageDescription).Value); + Assert.Equal("This is Изготовитель камеры", exifProfile.GetValue(ExifTag.Make).Value); + Assert.Equal("This is Модель камеры", exifProfile.GetValue(ExifTag.Model).Value); + Assert.Equal("IrfanView", exifProfile.GetValue(ExifTag.Software).Value); + Assert.Null(exifProfile.GetValue(ExifTag.DateTime)?.Value); + Assert.Equal("This is author1;Author2", exifProfile.GetValue(ExifTag.Artist).Value); + Assert.Null(exifProfile.GetValue(ExifTag.HostComputer)?.Value); + Assert.Equal("This is Авторские права", exifProfile.GetValue(ExifTag.Copyright).Value); + Assert.Equal(4, exifProfile.GetValue(ExifTag.Rating).Value); + Assert.Equal(75, exifProfile.GetValue(ExifTag.RatingPercent).Value); + var expectedResolution = new Rational(10000, 1000, simplify: false); + Assert.Equal(expectedResolution, exifProfile.GetValue(ExifTag.XResolution).Value); + Assert.Equal(expectedResolution, exifProfile.GetValue(ExifTag.YResolution).Value); + Assert.Equal(new Number[] { 8u }, exifProfile.GetValue(ExifTag.StripOffsets)?.Value, new NumberComparer()); + Assert.Equal(new Number[] { 297u }, exifProfile.GetValue(ExifTag.StripByteCounts)?.Value, new NumberComparer()); + Assert.Null(exifProfile.GetValue(ExifTag.ExtraSamples)?.Value); + Assert.Equal(32u, exifProfile.GetValue(ExifTag.RowsPerStrip).Value); + Assert.Null(exifProfile.GetValue(ExifTag.SampleFormat)); + Assert.Equal(TiffPredictor.None, tiffFrameMetadata.Predictor); + Assert.Equal(PixelResolutionUnit.PixelsPerInch, UnitConverter.ExifProfileToResolutionUnit(exifProfile)); + ushort[] colorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value; + Assert.NotNull(colorMap); + Assert.Equal(48, colorMap.Length); + Assert.Equal(10537, colorMap[0]); + Assert.Equal(14392, colorMap[1]); + Assert.Equal(58596, colorMap[46]); + Assert.Equal(3855, colorMap[47]); + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, tiffFrameMetadata.PhotometricInterpretation); + Assert.Equal(1u, exifProfile.GetValue(ExifTag.SamplesPerPixel).Value); + + ImageMetadata imageMetaData = image.Metadata; + Assert.NotNull(imageMetaData); + Assert.Equal(PixelResolutionUnit.PixelsPerInch, imageMetaData.ResolutionUnits); + Assert.Equal(10, imageMetaData.HorizontalResolution); + Assert.Equal(10, imageMetaData.VerticalResolution); + + TiffMetadata tiffMetaData = image.Metadata.GetTiffMetadata(); + Assert.NotNull(tiffMetaData); + Assert.Equal(ByteOrder.LittleEndian, tiffMetaData.ByteOrder); + } + } + + [Theory] + [WithFile(MultiframeDeflateWithPreview, PixelTypes.Rgba32)] + public void SubfileType(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(TiffDecoder)) + { + TiffMetadata meta = image.Metadata.GetTiffMetadata(); + Assert.NotNull(meta); + + Assert.Equal(2, image.Frames.Count); + + ExifProfile frame0Exif = image.Frames[0].Metadata.ExifProfile; + Assert.Equal(TiffNewSubfileType.FullImage, (TiffNewSubfileType)frame0Exif.GetValue(ExifTag.SubfileType).Value); + Assert.Equal(255, image.Frames[0].Width); + Assert.Equal(255, image.Frames[0].Height); + + ExifProfile frame1Exif = image.Frames[1].Metadata.ExifProfile; + Assert.Equal(TiffNewSubfileType.Preview, (TiffNewSubfileType)frame1Exif.GetValue(ExifTag.SubfileType).Value); + Assert.Equal(255, image.Frames[1].Width); + Assert.Equal(255, image.Frames[1].Height); + } + } + + [Theory] + [WithFile(SampleMetadata, PixelTypes.Rgba32)] + public void Encode_PreservesMetadata(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Load Tiff image + using Image image = provider.GetImage(new TiffDecoder() { IgnoreMetadata = false }); + + ImageMetadata inputMetaData = image.Metadata; + ImageFrame rootFrameInput = image.Frames.RootFrame; + TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata(); + byte[] xmpProfileInput = rootFrameInput.Metadata.XmpProfile; + ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile; + + Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression); + Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaInput.BitsPerPixel); + + // Save to Tiff + var tiffEncoder = new TiffEncoder() { PhotometricInterpretation = TiffPhotometricInterpretation.Rgb }; + using var ms = new MemoryStream(); + image.Save(ms, tiffEncoder); + + // Assert + ms.Position = 0; + using var encodedImage = Image.Load(this.configuration, ms); + + ImageMetadata encodedImageMetaData = encodedImage.Metadata; + ImageFrame rootFrameEncodedImage = encodedImage.Frames.RootFrame; + TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata(); + ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile; + byte[] encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile; + + Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel); + Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression); + + Assert.Equal(inputMetaData.HorizontalResolution, encodedImageMetaData.HorizontalResolution); + Assert.Equal(inputMetaData.VerticalResolution, encodedImageMetaData.VerticalResolution); + Assert.Equal(inputMetaData.ResolutionUnits, encodedImageMetaData.ResolutionUnits); + + Assert.Equal(rootFrameInput.Width, rootFrameEncodedImage.Width); + Assert.Equal(rootFrameInput.Height, rootFrameEncodedImage.Height); + + PixelResolutionUnit resolutionUnitInput = UnitConverter.ExifProfileToResolutionUnit(exifProfileInput); + PixelResolutionUnit resolutionUnitEncoded = UnitConverter.ExifProfileToResolutionUnit(encodedImageExifProfile); + Assert.Equal(resolutionUnitInput, resolutionUnitEncoded); + Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution), encodedImageExifProfile.GetValue(ExifTag.XResolution)); + Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution), encodedImageExifProfile.GetValue(ExifTag.YResolution)); + + Assert.Equal(xmpProfileInput, encodedImageXmpProfile); + + Assert.Equal("IrfanView", exifProfileInput.GetValue(ExifTag.Software).Value); + Assert.Equal("This is Название", exifProfileInput.GetValue(ExifTag.ImageDescription).Value); + Assert.Equal("This is Изготовитель камеры", exifProfileInput.GetValue(ExifTag.Make).Value); + Assert.Equal("This is Авторские права", exifProfileInput.GetValue(ExifTag.Copyright).Value); + + Assert.Equal(exifProfileInput.Values.Count, encodedImageExifProfile.Values.Count); + Assert.Equal(exifProfileInput.GetValue(ExifTag.ImageDescription).Value, encodedImageExifProfile.GetValue(ExifTag.ImageDescription).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Make).Value, encodedImageExifProfile.GetValue(ExifTag.Make).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Copyright).Value, encodedImageExifProfile.GetValue(ExifTag.Copyright).Value); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs new file mode 100644 index 0000000000..5ac2f475ed --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.IO; + +using ImageMagick; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + public static class TiffTestUtils + { + public static void CompareWithReferenceDecoder( + string encodedImagePath, + Image image, + bool useExactComparer = true, + float compareTolerance = 0.01f) + where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel + { + var testFile = TestFile.Create(encodedImagePath); + 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; + } + } + + internal class NumberComparer : IEqualityComparer + { + public bool Equals(Number x, Number y) => x.Equals(y); + + public int GetHashCode(Number obj) => obj.GetHashCode(); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs new file mode 100644 index 0000000000..6eea0a1a9c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Writers; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Utils +{ + [Trait("Format", "Tiff")] + public class TiffWriterTests + { + [Fact] + public void IsLittleEndian_IsTrueOnWindows() + { + using var stream = new MemoryStream(); + using var writer = new TiffStreamWriter(stream); + Assert.True(writer.IsLittleEndian); + } + + [Theory] + [InlineData(new byte[] { }, 0)] + [InlineData(new byte[] { 42 }, 1)] + [InlineData(new byte[] { 1, 2, 3, 4, 5 }, 5)] + public void Position_EqualsTheStreamPosition(byte[] data, long expectedResult) + { + using var stream = new MemoryStream(); + using var writer = new TiffStreamWriter(stream); + writer.Write(data); + Assert.Equal(writer.Position, expectedResult); + } + + [Fact] + public void Write_WritesByte() + { + using var stream = new MemoryStream(); + using var writer = new TiffStreamWriter(stream); + writer.Write(42); + + Assert.Equal(new byte[] { 42 }, stream.ToArray()); + } + + [Fact] + public void Write_WritesByteArray() + { + using var stream = new MemoryStream(); + using var writer = new TiffStreamWriter(stream); + writer.Write(new byte[] { 2, 4, 6, 8 }); + + Assert.Equal(new byte[] { 2, 4, 6, 8 }, stream.ToArray()); + } + + [Fact] + public void Write_WritesUInt16() + { + using var stream = new MemoryStream(); + using var writer = new TiffStreamWriter(stream); + writer.Write(1234); + + Assert.Equal(new byte[] { 0xD2, 0x04 }, stream.ToArray()); + } + + [Fact] + public void Write_WritesUInt32() + { + using var stream = new MemoryStream(); + using var writer = new TiffStreamWriter(stream); + writer.Write(12345678U); + + Assert.Equal(new byte[] { 0x4E, 0x61, 0xBC, 0x00 }, stream.ToArray()); + } + + [Theory] + [InlineData(new byte[] { }, new byte[] { })] + [InlineData(new byte[] { 2 }, new byte[] { 2, 0, 0, 0 })] + [InlineData(new byte[] { 2, 4 }, new byte[] { 2, 4, 0, 0 })] + [InlineData(new byte[] { 2, 4, 6 }, new byte[] { 2, 4, 6, 0 })] + [InlineData(new byte[] { 2, 4, 6, 8 }, new byte[] { 2, 4, 6, 8 })] + [InlineData(new byte[] { 2, 4, 6, 8, 10, 12 }, new byte[] { 2, 4, 6, 8, 10, 12, 0, 0 })] + + public void WritePadded_WritesByteArray(byte[] bytes, byte[] expectedResult) + { + using var stream = new MemoryStream(); + using var writer = new TiffStreamWriter(stream); + writer.WritePadded(bytes); + + Assert.Equal(expectedResult, stream.ToArray()); + } + + [Fact] + public void WriteMarker_WritesToPlacedPosition() + { + using var stream = new MemoryStream(); + + using (var writer = new TiffStreamWriter(stream)) + { + writer.Write(0x11111111); + long marker = writer.PlaceMarker(); + writer.Write(0x33333333); + + writer.WriteMarker(marker, 0x12345678); + + writer.Write(0x44444444); + } + + Assert.Equal( + new byte[] + { + 0x11, 0x11, 0x11, 0x11, + 0x78, 0x56, 0x34, 0x12, + 0x33, 0x33, 0x33, 0x33, + 0x44, 0x44, 0x44, 0x44 + }, stream.ToArray()); + } + } +} diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs index 3f89049049..f1a90d43e7 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs @@ -1,11 +1,15 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System.Linq; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using Xunit; +using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile; +using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag; -namespace SixLabors.ImageSharp.Tests +namespace SixLabors.ImageSharp.Tests.Metadata { /// /// Tests the class. @@ -36,9 +40,43 @@ public void ConstructorImageFrameMetadata() [Fact] public void CloneIsDeep() { - var metaData = new ImageFrameMetadata(); + // arrange + byte[] xmpProfile = { 1, 2, 3 }; + var exifProfile = new ExifProfile(); + exifProfile.SetValue(ExifTag.Software, "UnitTest"); + exifProfile.SetValue(ExifTag.Artist, "UnitTest"); + var iccProfile = new IccProfile() + { + Header = new IccProfileHeader() + { + CmmType = "Unittest" + } + }; + var iptcProfile = new ImageSharp.Metadata.Profiles.Iptc.IptcProfile(); + var metaData = new ImageFrameMetadata() + { + XmpProfile = xmpProfile, + ExifProfile = exifProfile, + IccProfile = iccProfile, + IptcProfile = iptcProfile + }; + + // act ImageFrameMetadata clone = metaData.DeepClone(); + + // assert + Assert.NotNull(clone); + Assert.NotNull(clone.ExifProfile); + Assert.NotNull(clone.XmpProfile); + Assert.NotNull(clone.IccProfile); + Assert.NotNull(clone.IptcProfile); + Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile)); + Assert.True(metaData.ExifProfile.Values.Count == clone.ExifProfile.Values.Count); + Assert.False(metaData.XmpProfile.Equals(clone.XmpProfile)); + Assert.True(metaData.XmpProfile.SequenceEqual(clone.XmpProfile)); Assert.False(metaData.GetGifMetadata().Equals(clone.GetGifMetadata())); + Assert.False(metaData.IccProfile.Equals(clone.IccProfile)); + Assert.False(metaData.IptcProfile.Equals(clone.IptcProfile)); } } } diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 466568bfec..cac46fd609 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -14,6 +14,7 @@ namespace SixLabors.ImageSharp.Tests { + [Trait("Profile", "Exif")] public class ExifProfileTests { public enum TestImageWriteFormat @@ -67,8 +68,8 @@ public void Constructor(TestImageWriteFormat imageFormat) [Fact] public void ConstructorEmpty() { - new ExifProfile((byte[])null); - new ExifProfile(new byte[] { }); + new ExifProfile(null); + new ExifProfile(Array.Empty()); } [Fact] @@ -159,9 +160,14 @@ public void ReadWriteInfinity(TestImageWriteFormat imageFormat) } [Theory] - [InlineData(TestImageWriteFormat.Jpeg)] - [InlineData(TestImageWriteFormat.Png)] - public void SetValue(TestImageWriteFormat imageFormat) + /* The original exif profile has 19 values, the written profile should be 3 less. + 1 x due to setting of null "ReferenceBlackWhite" value. + 2 x due to use of non-standard padding tag 0xEA1C listed in EXIF Tool. We can read those values but adhere + strictly to the 2.3.1 specification when writing. (TODO: Support 2.3.2) + https://exiftool.org/TagNames/EXIF.html */ + [InlineData(TestImageWriteFormat.Jpeg, 16)] + [InlineData(TestImageWriteFormat.Png, 16)] + public void SetValue(TestImageWriteFormat imageFormat, int expectedProfileValueCount) { Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); image.Metadata.ExifProfile.SetValue(ExifTag.Software, "ImageSharp"); @@ -201,17 +207,15 @@ public void SetValue(TestImageWriteFormat imageFormat) IExifValue latitude = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude); Assert.Equal(expectedLatitude, latitude.Value); - int profileCount = image.Metadata.ExifProfile.Values.Count; + // todo: duplicate tags + Assert.Equal(2, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932)); + image = WriteAndRead(image, imageFormat); Assert.NotNull(image.Metadata.ExifProfile); + Assert.Equal(0, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932)); - // Should be 3 less. - // 1 x due to setting of null "ReferenceBlackWhite" value. - // 2 x due to use of non-standard padding tag 0xEA1C listed in EXIF Tool. We can read those values but adhere - // strictly to the 2.3.1 specification when writing. (TODO: Support 2.3.2) - // https://exiftool.org/TagNames/EXIF.html - Assert.Equal(profileCount - 3, image.Metadata.ExifProfile.Values.Count); + Assert.Equal(expectedProfileValueCount, image.Metadata.ExifProfile.Values.Count); software = image.Metadata.ExifProfile.GetValue(ExifTag.Software); Assert.Equal("15", software.Value); @@ -227,20 +231,42 @@ public void SetValue(TestImageWriteFormat imageFormat) latitude = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude); Assert.Equal(expectedLatitude, latitude.Value); + } + [Theory] + [InlineData(TestImageWriteFormat.Jpeg)] + [InlineData(TestImageWriteFormat.Png)] + public void WriteOnlyExifTags_Works(TestImageWriteFormat imageFormat) + { + // Arrange + Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); image.Metadata.ExifProfile.Parts = ExifParts.ExifTags; + // Act image = WriteAndRead(image, imageFormat); + // Assert Assert.NotNull(image.Metadata.ExifProfile); - Assert.Equal(8, image.Metadata.ExifProfile.Values.Count); + Assert.Equal(7, image.Metadata.ExifProfile.Values.Count); + foreach (IExifValue exifProfileValue in image.Metadata.ExifProfile.Values) + { + Assert.True(ExifTags.GetPart(exifProfileValue.Tag) == ExifParts.ExifTags); + } + } + + [Fact] + public void RemoveEntry_Works() + { + // Arrange + Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); + int profileCount = image.Metadata.ExifProfile.Values.Count; + // Assert Assert.NotNull(image.Metadata.ExifProfile.GetValue(ExifTag.ColorSpace)); Assert.True(image.Metadata.ExifProfile.RemoveValue(ExifTag.ColorSpace)); Assert.False(image.Metadata.ExifProfile.RemoveValue(ExifTag.ColorSpace)); Assert.Null(image.Metadata.ExifProfile.GetValue(ExifTag.ColorSpace)); - - Assert.Equal(7, image.Metadata.ExifProfile.Values.Count); + Assert.Equal(profileCount - 1, image.Metadata.ExifProfile.Values.Count); } [Fact] @@ -301,7 +327,7 @@ public void ReadWriteLargeProfileJpg() var junk = new StringBuilder(); for (int i = 0; i < 65600; i++) { - junk.Append("a"); + junk.Append('a'); } var image = new Image(100, 100); @@ -363,8 +389,14 @@ public void TestArrayValueWithUnspecifiedSize() // Force parsing of the profile. Assert.Equal(25, profile.Values.Count); + // todo: duplicate tags (from root container and subIfd) + Assert.Equal(2, profile.Values.Count(v => (ExifTagValue)(ushort)v.Tag == ExifTagValue.DateTime)); + byte[] bytes = profile.ToByteArray(); Assert.Equal(525, bytes.Length); + + var profile2 = new ExifProfile(bytes); + Assert.Equal(25, profile2.Values.Count); } [Theory] @@ -477,6 +509,9 @@ private static void TestProfile(ExifProfile profile) { Assert.NotNull(profile); + // todo: duplicate tags + Assert.Equal(2, profile.Values.Count(v => (ushort)v.Tag == 59932)); + Assert.Equal(16, profile.Values.Count); foreach (IExifValue value in profile.Values) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs index 401546e5c6..4ff37eb6b0 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs @@ -8,6 +8,7 @@ namespace SixLabors.ImageSharp.Tests { + [Trait("Profile", "Exif")] public class ExifReaderTests { [Fact] diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs index 2b00cc5b48..42a3b72a6a 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs @@ -6,6 +6,7 @@ namespace SixLabors.ImageSharp.Tests { + [Trait("Profile", "Exif")] public class ExifTagDescriptionAttributeTests { [Fact] diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs index 5fe1b51baf..a2c01ea612 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs @@ -7,6 +7,7 @@ namespace SixLabors.ImageSharp.Tests { + [Trait("Profile", "Exif")] public class ExifValueTests { private ExifProfile profile; diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs index 898c693562..3358b1f977 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs @@ -6,6 +6,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values { + [Trait("Profile", "Exif")] public class ExifValuesTests { public static TheoryData ByteTags => new TheoryData @@ -94,6 +95,7 @@ public class ExifValuesTests public static TheoryData NumberArrayTags => new TheoryData { { ExifTag.StripOffsets }, + { ExifTag.StripByteCounts }, { ExifTag.TileByteCounts }, { ExifTag.ImageLayer } }; @@ -160,6 +162,7 @@ public class ExifValuesTests { ExifTag.Orientation }, { ExifTag.SamplesPerPixel }, { ExifTag.PlanarConfiguration }, + { ExifTag.Predictor }, { ExifTag.GrayResponseUnit }, { ExifTag.ResolutionUnit }, { ExifTag.CleanFaxData }, @@ -208,7 +211,6 @@ public class ExifValuesTests { ExifTag.ExtraSamples }, { ExifTag.PageNumber }, { ExifTag.TransferFunction }, - { ExifTag.Predictor }, { ExifTag.HalftoneHints }, { ExifTag.SampleFormat }, { ExifTag.TransferRange }, diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs index 9e763536bc..b5bb53b5b6 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -16,6 +17,8 @@ public class IptcProfileTests { private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false }; + private static TiffDecoder TiffDecoder => new TiffDecoder() { IgnoreMetadata = false }; + public static IEnumerable AllIptcTags() { foreach (object tag in Enum.GetValues(typeof(IptcTag))) @@ -100,34 +103,53 @@ public void IptcProfile_SetTimeValue_Works(IptcTag tag) [Theory] [WithFile(TestImages.Jpeg.Baseline.Iptc, PixelTypes.Rgba32)] - public void ReadIptcMetadata_Works(TestImageProvider provider) + public void ReadIptcMetadata_FromJpg_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel { using (Image image = provider.GetImage(JpegDecoder)) { Assert.NotNull(image.Metadata.IptcProfile); var iptcValues = image.Metadata.IptcProfile.Values.ToList(); - ContainsIptcValue(iptcValues, IptcTag.Caption, "description"); - ContainsIptcValue(iptcValues, IptcTag.CaptionWriter, "description writer"); - ContainsIptcValue(iptcValues, IptcTag.Headline, "headline"); - ContainsIptcValue(iptcValues, IptcTag.SpecialInstructions, "special instructions"); - ContainsIptcValue(iptcValues, IptcTag.Byline, "author"); - ContainsIptcValue(iptcValues, IptcTag.BylineTitle, "author title"); - ContainsIptcValue(iptcValues, IptcTag.Credit, "credits"); - ContainsIptcValue(iptcValues, IptcTag.Source, "source"); - ContainsIptcValue(iptcValues, IptcTag.Name, "title"); - ContainsIptcValue(iptcValues, IptcTag.CreatedDate, "20200414"); - ContainsIptcValue(iptcValues, IptcTag.City, "city"); - ContainsIptcValue(iptcValues, IptcTag.SubLocation, "sublocation"); - ContainsIptcValue(iptcValues, IptcTag.ProvinceState, "province-state"); - ContainsIptcValue(iptcValues, IptcTag.Country, "country"); - ContainsIptcValue(iptcValues, IptcTag.Category, "category"); - ContainsIptcValue(iptcValues, IptcTag.Urgency, "1"); - ContainsIptcValue(iptcValues, IptcTag.Keywords, "keywords"); - ContainsIptcValue(iptcValues, IptcTag.CopyrightNotice, "copyright"); + IptcProfileContainsExpectedValues(iptcValues); } } + [Theory] + [WithFile(TestImages.Tiff.IptcData, PixelTypes.Rgba32)] + public void ReadIptcMetadata_FromTiff_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(TiffDecoder)) + { + IptcProfile iptc = image.Frames.RootFrame.Metadata.IptcProfile; + Assert.NotNull(iptc); + var iptcValues = iptc.Values.ToList(); + IptcProfileContainsExpectedValues(iptcValues); + } + } + + private static void IptcProfileContainsExpectedValues(List iptcValues) + { + ContainsIptcValue(iptcValues, IptcTag.Caption, "description"); + ContainsIptcValue(iptcValues, IptcTag.CaptionWriter, "description writer"); + ContainsIptcValue(iptcValues, IptcTag.Headline, "headline"); + ContainsIptcValue(iptcValues, IptcTag.SpecialInstructions, "special instructions"); + ContainsIptcValue(iptcValues, IptcTag.Byline, "author"); + ContainsIptcValue(iptcValues, IptcTag.BylineTitle, "author title"); + ContainsIptcValue(iptcValues, IptcTag.Credit, "credits"); + ContainsIptcValue(iptcValues, IptcTag.Source, "source"); + ContainsIptcValue(iptcValues, IptcTag.Name, "title"); + ContainsIptcValue(iptcValues, IptcTag.CreatedDate, "20200414"); + ContainsIptcValue(iptcValues, IptcTag.City, "city"); + ContainsIptcValue(iptcValues, IptcTag.SubLocation, "sublocation"); + ContainsIptcValue(iptcValues, IptcTag.ProvinceState, "province-state"); + ContainsIptcValue(iptcValues, IptcTag.Country, "country"); + ContainsIptcValue(iptcValues, IptcTag.Category, "category"); + ContainsIptcValue(iptcValues, IptcTag.Urgency, "1"); + ContainsIptcValue(iptcValues, IptcTag.Keywords, "keywords"); + ContainsIptcValue(iptcValues, IptcTag.CopyrightNotice, "copyright"); + } + [Theory] [WithFile(TestImages.Jpeg.Baseline.App13WithEmptyIptc, PixelTypes.Rgba32)] public void ReadApp13_WithEmptyIptc_Works(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index c15327e0de..49d0e759cd 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -497,5 +497,95 @@ public static class Tga public const string NoAlphaBits32Bit = "Tga/32bit_no_alphabits.tga"; public const string NoAlphaBits32BitRle = "Tga/32bit_rle_no_alphabits.tga"; } + + public static class Tiff + { + public const string Benchmark_Path = "Tiff/Benchmarks/"; + public const string Benchmark_BwFax3 = "medium_bw_Fax3.tiff"; + public const string Benchmark_BwFax4 = "medium_bw_Fax4.tiff"; + public const string Benchmark_BwRle = "medium_bw_Rle.tiff"; + public const string Benchmark_GrayscaleUncompressed = "medium_grayscale_uncompressed.tiff"; + public const string Benchmark_PaletteUncompressed = "medium_palette_uncompressed.tiff"; + public const string Benchmark_RgbDeflate = "medium_rgb_deflate.tiff"; + public const string Benchmark_RgbLzw = "medium_rgb_lzw.tiff"; + public const string Benchmark_RgbPackbits = "medium_rgb_packbits.tiff"; + public const string Benchmark_RgbUncompressed = "medium_rgb_uncompressed.tiff"; + + public const string Calliphora_GrayscaleUncompressed = "Tiff/Calliphora_grayscale_uncompressed.tiff"; + public const string Calliphora_GrayscaleDeflate_Predictor = "Tiff/Calliphora_gray_deflate_predictor.tiff"; + public const string Calliphora_GrayscaleLzw_Predictor = "Tiff/Calliphora_gray_lzw_predictor.tiff"; + public const string Calliphora_GrayscaleDeflate = "Tiff/Calliphora_gray_deflate.tiff"; + public const string Calliphora_RgbDeflate_Predictor = "Tiff/Calliphora_rgb_deflate_predictor.tiff"; + public const string Calliphora_RgbJpeg = "Tiff/Calliphora_rgb_jpeg.tiff"; + public const string Calliphora_PaletteUncompressed = "Tiff/Calliphora_palette_uncompressed.tiff"; + public const string Calliphora_RgbLzwPredictor = "Tiff/Calliphora_rgb_lzw_predictor.tiff"; + public const string Calliphora_RgbPaletteLzw = "Tiff/Calliphora_rgb_palette_lzw.tiff"; + public const string Calliphora_RgbPaletteLzw_Predictor = "Tiff/Calliphora_rgb_palette_lzw_predictor.tiff"; + public const string Calliphora_RgbPackbits = "Tiff/Calliphora_rgb_packbits.tiff"; + public const string Calliphora_RgbUncompressed = "Tiff/Calliphora_rgb_uncompressed.tiff"; + public const string Calliphora_Fax3Compressed = "Tiff/Calliphora_ccitt_fax3.tiff"; + public const string Fax3Uncompressed = "Tiff/ccitt_fax3_uncompressed.tiff"; + public const string Calliphora_Fax3Compressed_WithEolPadding = "Tiff/Calliphora_ccitt_fax3_with_eol_padding.tiff"; + public const string Calliphora_Fax4Compressed = "Tiff/Calliphora_ccitt_fax4.tiff"; + public const string Calliphora_HuffmanCompressed = "Tiff/Calliphora_huffman_rle.tiff"; + public const string Calliphora_BiColorUncompressed = "Tiff/Calliphora_bicolor_uncompressed.tiff"; + + public const string CcittFax3AllTermCodes = "Tiff/ccitt_fax3_all_terminating_codes.tiff"; + public const string CcittFax3AllMakeupCodes = "Tiff/ccitt_fax3_all_makeup_codes.tiff"; + public const string HuffmanRleAllTermCodes = "Tiff/huffman_rle_all_terminating_codes.tiff"; + public const string HuffmanRleAllMakeupCodes = "Tiff/huffman_rle_all_makeup_codes.tiff"; + + // Test case for an issue, that the last bits in a row got ignored. + public const string HuffmanRle_basi3p02 = "Tiff/basi3p02_huffman_rle.tiff"; + + public const string GrayscaleDeflateMultistrip = "Tiff/grayscale_deflate_multistrip.tiff"; + public const string GrayscaleUncompressed = "Tiff/grayscale_uncompressed.tiff"; + public const string PaletteDeflateMultistrip = "Tiff/palette_grayscale_deflate_multistrip.tiff"; + public const string PaletteUncompressed = "Tiff/palette_uncompressed.tiff"; + public const string RgbDeflate = "Tiff/rgb_deflate.tiff"; + public const string RgbDeflatePredictor = "Tiff/rgb_deflate_predictor.tiff"; + public const string RgbDeflateMultistrip = "Tiff/rgb_deflate_multistrip.tiff"; + public const string RgbJpeg = "Tiff/rgb_jpeg.tiff"; + public const string RgbLzwPredictor = "Tiff/rgb_lzw_predictor.tiff"; + public const string RgbLzwNoPredictor = "Tiff/rgb_lzw_no_predictor.tiff"; + public const string RgbLzwNoPredictorMultistrip = "Tiff/rgb_lzw_noPredictor_multistrip.tiff"; + public const string RgbLzwNoPredictorMultistripMotorola = "Tiff/rgb_lzw_noPredictor_multistrip_Motorola.tiff"; + public const string RgbLzwNoPredictorSinglestripMotorola = "Tiff/rgb_lzw_noPredictor_singlestrip_Motorola.tiff"; + public const string RgbLzwMultistripPredictor = "Tiff/rgb_lzw_multistrip.tiff"; + public const string RgbPackbits = "Tiff/rgb_packbits.tiff"; + public const string RgbPackbitsMultistrip = "Tiff/rgb_packbits_multistrip.tiff"; + public const string RgbUncompressed = "Tiff/rgb_uncompressed.tiff"; + public const string RgbPalette = "Tiff/rgb_palette.tiff"; + public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff"; + public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff"; + + public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff"; + public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff"; + + public const string RgbUncompressedTiled = "Tiff/rgb_uncompressed_tiled.tiff"; + public const string MultiframeDifferentSizeTiled = "Tiff/multipage_ withPreview_differentSize_tiled.tiff"; + + public const string MultiframeLzwPredictor = "Tiff/multipage_lzw.tiff"; + public const string MultiframeDeflateWithPreview = "Tiff/multipage_deflate_withPreview.tiff"; + public const string MultiframeDifferentSize = "Tiff/multipage_differentSize.tiff"; + public const string MultiframeDifferentVariants = "Tiff/multipage_differentVariants.tiff"; + + public const string FillOrder2 = "Tiff/b0350_fillorder2.tiff"; + public const string LittleEndianByteOrder = "Tiff/little_endian.tiff"; + + public const string Fax4_Motorola = "Tiff/moy.tiff"; + + public const string SampleMetadata = "Tiff/metadata_sample.tiff"; + + // Iptc data as long[] instead of byte[] + public const string InvalidIptcData = "Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff"; + public const string IptcData = "Tiff/iptc.tiff"; + + public static readonly string[] Multiframes = { MultiframeDeflateWithPreview, MultiframeLzwPredictor /*, MultiFrameDifferentSize, MultiframeDifferentSizeTiled, MultiFrameDifferentVariants,*/ }; + + public static readonly string[] Metadata = { SampleMetadata }; + + public static readonly string[] NotSupported = { Calliphora_RgbJpeg, RgbJpeg, RgbUncompressedTiled, MultiframeDifferentSize, MultiframeDifferentVariants, FillOrder2, Calliphora_Fax4Compressed, Fax4_Motorola }; + } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ByteArrayUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ByteArrayUtility.cs new file mode 100644 index 0000000000..501651285d --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/ByteArrayUtility.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Tests.TestUtilities +{ + using System; + + public static class ByteArrayUtility + { + public static byte[] WithByteOrder(this byte[] bytes, bool isLittleEndian) + { + if (isLittleEndian != BitConverter.IsLittleEndian) + { + var reversedBytes = new byte[bytes.Length]; + Array.Copy(bytes, reversedBytes, bytes.Length); + Array.Reverse(reversedBytes); + return reversedBytes; + } + else + { + return bytes; + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/ByteBuffer.cs b/tests/ImageSharp.Tests/TestUtilities/ByteBuffer.cs new file mode 100644 index 0000000000..bbb75a9cf2 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/ByteBuffer.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Tests.TestUtilities +{ + using System; + using System.Collections.Generic; + + public class ByteBuffer + { + private readonly List bytes = new List(); + private readonly bool isLittleEndian; + + public ByteBuffer(bool isLittleEndian) + { + this.isLittleEndian = isLittleEndian; + } + + public void AddByte(byte value) + { + this.bytes.Add(value); + } + + public void AddUInt16(ushort value) + { + this.bytes.AddRange(BitConverter.GetBytes(value).WithByteOrder(this.isLittleEndian)); + } + + public void AddUInt32(uint value) + { + this.bytes.AddRange(BitConverter.GetBytes(value).WithByteOrder(this.isLittleEndian)); + } + + public byte[] ToArray() + { + return this.bytes.ToArray(); + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 163bb84d56..4e2866be1f 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -10,6 +11,7 @@ using ImageMagick.Formats; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs @@ -75,23 +77,27 @@ public Image Decode(Configuration configuration, Stream stream) var settings = new MagickReadSettings(); settings.SetDefines(bmpReadDefines); - using var magickImage = new MagickImage(stream, settings); - var result = new Image(configuration, magickImage.Width, magickImage.Height); - MemoryGroup resultPixels = result.GetRootFramePixelBuffer().FastMemoryGroup; - - using (IUnsafePixelCollection pixels = magickImage.GetPixelsUnsafe()) + using var magickImageCollection = new MagickImageCollection(stream, settings); + var framesList = new List>(); + foreach (IMagickImage magicFrame in magickImageCollection) { - if (magickImage.Depth == 8) + var frame = new ImageFrame(configuration, magicFrame.Width, magicFrame.Height); + framesList.Add(frame); + + MemoryGroup framePixels = frame.PixelBuffer.FastMemoryGroup; + + using IUnsafePixelCollection pixels = magicFrame.GetPixelsUnsafe(); + if (magicFrame.Depth == 8 || magicFrame.Depth == 1) { byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - FromRgba32Bytes(configuration, data, resultPixels); + FromRgba32Bytes(configuration, data, framePixels); } - else if (magickImage.Depth == 16) + else if (magicFrame.Depth == 16) { ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); Span bytes = MemoryMarshal.Cast(data.AsSpan()); - FromRgba64Bytes(configuration, bytes, resultPixels); + FromRgba64Bytes(configuration, bytes, framePixels); } else { @@ -99,7 +105,7 @@ public Image Decode(Configuration configuration, Stream stream) } } - return result; + return new Image(configuration, new ImageMetadata(), framesList); } public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index ea5b2f6652..f68fb4d95d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; namespace SixLabors.ImageSharp.Tests @@ -55,7 +56,8 @@ private static Configuration CreateDefaultConfiguration() var cfg = new Configuration( new JpegConfigurationModule(), new GifConfigurationModule(), - new TgaConfigurationModule()); + new TgaConfigurationModule(), + new TiffConfigurationModule()); IImageEncoder pngEncoder = IsWindows ? (IImageEncoder)SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration(); IImageEncoder bmpEncoder = IsWindows ? (IImageEncoder)SystemDrawingReferenceEncoder.Bmp : new BmpEncoder(); diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 1c5aedd9b7..de365c4295 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -514,6 +514,31 @@ public static Image CompareToOriginal( return image; } + public static Image CompareToOriginalMultiFrame( + this Image image, + ITestImageProvider provider, + ImageComparer comparer, + IImageDecoder referenceDecoder = null) + where TPixel : unmanaged, IPixel + { + string path = TestImageProvider.GetFilePathOrNull(provider); + if (path == null) + { + throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); + } + + var testFile = TestFile.Create(path); + + referenceDecoder = referenceDecoder ?? TestEnvironment.GetReferenceDecoder(path); + + using (var original = Image.Load(testFile.Bytes, referenceDecoder)) + { + comparer.VerifySimilarity(original, image); + } + + return image; + } + /// /// Utility method for doing the following in one step: /// 1. Executing an operation (taken as a delegate) diff --git a/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff b/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff new file mode 100644 index 0000000000..5574bd58ed --- /dev/null +++ b/tests/Images/Input/Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:622d69dba0a8a67aa3b87e384a2b9ea8d29689eaa5cb5d0eee857f98ed660517 +size 15154924 diff --git a/tests/Images/Input/Tiff/Benchmarks/.gitignore b/tests/Images/Input/Tiff/Benchmarks/.gitignore new file mode 100644 index 0000000000..ab0f2bdbc2 --- /dev/null +++ b/tests/Images/Input/Tiff/Benchmarks/.gitignore @@ -0,0 +1,2 @@ +*.tiff +*.tif diff --git a/tests/Images/Input/Tiff/Benchmarks/gen.bat b/tests/Images/Input/Tiff/Benchmarks/gen.bat new file mode 100644 index 0000000000..3a0a032c1d --- /dev/null +++ b/tests/Images/Input/Tiff/Benchmarks/gen.bat @@ -0,0 +1,2 @@ +powershell -executionpolicy RemoteSigned -file gen_big.ps1 +powershell -executionpolicy RemoteSigned -file gen_medium.ps1 diff --git a/tests/Images/Input/Tiff/Benchmarks/gen_big.ps1 b/tests/Images/Input/Tiff/Benchmarks/gen_big.ps1 new file mode 100644 index 0000000000..9d0c137f7e --- /dev/null +++ b/tests/Images/Input/Tiff/Benchmarks/gen_big.ps1 @@ -0,0 +1,14 @@ +$Gm_Exe = "C:\Program Files\ImageMagick-7.0.10-Q16-HDRI\magick.exe" +$Source_Image = ".\jpeg444_big.jpg" +$Output_Prefix = ".\big" + +& $Gm_Exe convert $Source_Image -compress None -type TrueColor $Output_Prefix"_rgb_uncompressed.tiff" +& $Gm_Exe convert $Source_Image -compress LZW -type TrueColor $Output_Prefix"_rgb_lzw.tiff" +& $Gm_Exe convert $Source_Image -compress RLE -type TrueColor $Output_Prefix"_rgb_packbits.tiff" +& $Gm_Exe convert $Source_Image -compress JPEG -type TrueColor $Output_Prefix"_rgb_jpeg.tiff" +& $Gm_Exe convert $Source_Image -compress Zip -type TrueColor $Output_Prefix"_rgb_deflate.tiff" +& $Gm_Exe convert $Source_Image -compress Group4 -type Bilevel $Output_Prefix"_bw_Group4.tiff" +& $Gm_Exe convert $Source_Image -compress Fax -type Bilevel $Output_Prefix"_bw_Fax3.tiff" + +& $Gm_Exe convert $Source_Image -compress None -type Grayscale $Output_Prefix"_grayscale_uncompressed.tiff" +& $Gm_Exe convert $Source_Image -compress None -colors 256 $Output_Prefix"_palette_uncompressed.tiff" \ No newline at end of file diff --git a/tests/Images/Input/Tiff/Benchmarks/gen_medium.ps1 b/tests/Images/Input/Tiff/Benchmarks/gen_medium.ps1 new file mode 100644 index 0000000000..9bfc650668 --- /dev/null +++ b/tests/Images/Input/Tiff/Benchmarks/gen_medium.ps1 @@ -0,0 +1,14 @@ +$Gm_Exe = "C:\Program Files\ImageMagick-7.0.10-Q16-HDRI\magick.exe" +$Source_Image = ".\jpeg444_medium.jpg" +$Output_Prefix = ".\medium" + +& $Gm_Exe convert $Source_Image -compress None -type TrueColor $Output_Prefix"_rgb_uncompressed.tiff" +& $Gm_Exe convert $Source_Image -compress LZW -type TrueColor $Output_Prefix"_rgb_lzw.tiff" +& $Gm_Exe convert $Source_Image -compress RLE -type TrueColor $Output_Prefix"_rgb_packbits.tiff" +& $Gm_Exe convert $Source_Image -compress JPEG -type TrueColor $Output_Prefix"_rgb_jpeg.tiff" +& $Gm_Exe convert $Source_Image -compress Zip -type TrueColor $Output_Prefix"_rgb_deflate.tiff" +& $Gm_Exe convert $Source_Image -compress Group4 -type Bilevel $Output_Prefix"_bw_Group4.tiff" +& $Gm_Exe convert $Source_Image -compress Fax -type Bilevel $Output_Prefix"_bw_Fax3.tiff" + +& $Gm_Exe convert $Source_Image -compress None -type Grayscale $Output_Prefix"_grayscale_uncompressed.tiff" +& $Gm_Exe convert $Source_Image -compress None -colors 256 $Output_Prefix"_palette_uncompressed.tiff" \ No newline at end of file diff --git a/tests/Images/Input/Tiff/Benchmarks/genimages.ps1 b/tests/Images/Input/Tiff/Benchmarks/genimages.ps1 new file mode 100644 index 0000000000..6ed0c080cd --- /dev/null +++ b/tests/Images/Input/Tiff/Benchmarks/genimages.ps1 @@ -0,0 +1,12 @@ +$Gm_Exe = "C:\Program Files\GraphicsMagick-1.3.25-Q16\gm.exe" +$Source_Image = "..\Jpg\baseline\Calliphora.jpg" +$Output_Prefix = ".\Calliphora" + +& $Gm_Exe convert $Source_Image -compress None -type TrueColor $Output_Prefix"_rgb_uncompressed.tiff" +& $Gm_Exe convert $Source_Image -compress LZW -type TrueColor $Output_Prefix"_rgb_lzw.tiff" +& $Gm_Exe convert $Source_Image -compress RLE -type TrueColor $Output_Prefix"_rgb_packbits.tiff" +& $Gm_Exe convert $Source_Image -compress JPEG -type TrueColor $Output_Prefix"_rgb_jpeg.tiff" +& $Gm_Exe convert $Source_Image -compress Zip -type TrueColor $Output_Prefix"_rgb_deflate.tiff" + +& $Gm_Exe convert $Source_Image -compress None -type Grayscale $Output_Prefix"_grayscale_uncompressed.tiff" +& $Gm_Exe convert $Source_Image -compress None -colors 256 $Output_Prefix"_palette_uncompressed.tiff" \ No newline at end of file diff --git a/tests/Images/Input/Tiff/Benchmarks/jpeg444.jpg b/tests/Images/Input/Tiff/Benchmarks/jpeg444.jpg new file mode 100644 index 0000000000..1887fa4a51 --- /dev/null +++ b/tests/Images/Input/Tiff/Benchmarks/jpeg444.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89c632bbc42bc917f81e7c47595c95cb914a619604ac07b8cebf6fd4d1d744ca +size 5667 diff --git a/tests/Images/Input/Tiff/Benchmarks/jpeg444_big.jpg b/tests/Images/Input/Tiff/Benchmarks/jpeg444_big.jpg new file mode 100644 index 0000000000..650aff92b8 --- /dev/null +++ b/tests/Images/Input/Tiff/Benchmarks/jpeg444_big.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf48ba72885b98b9d05f1e4bed2e85f5db1db04b0206fc8160a9da2367f4467c +size 1984946 diff --git a/tests/Images/Input/Tiff/Benchmarks/jpeg444_medium.jpg b/tests/Images/Input/Tiff/Benchmarks/jpeg444_medium.jpg new file mode 100644 index 0000000000..0300c67ab2 --- /dev/null +++ b/tests/Images/Input/Tiff/Benchmarks/jpeg444_medium.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75cdf78efd14c880f26d5009e087df06e772b000edddbb404e7098177f895ac1 +size 525610 diff --git a/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff new file mode 100644 index 0000000000..b0dbdde549 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_bicolor_uncompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed3b08730e5c34eb8d268f58d1e09efe2605398899bfd726cc3b35de21baa6ff +size 121196 diff --git a/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff new file mode 100644 index 0000000000..39852d5345 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b9b105857723bca5f478a9ab23c0aeca93abe863781019bbd2da47f18c46f24 +size 125778 diff --git a/tests/Images/Input/Tiff/Calliphora_ccitt_fax3_with_eol_padding.tiff b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3_with_eol_padding.tiff new file mode 100644 index 0000000000..2072b0c47d --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_ccitt_fax3_with_eol_padding.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15e0f4f04699c7253ce422a7741ada192615182da53e9fd86bdf547cd991b290 +size 126382 diff --git a/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff b/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff new file mode 100644 index 0000000000..384d00eaa9 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_ccitt_fax4.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a2c95aec08b96bca30af344f7d9952a603a951802ce534a5f2c5f563795cbd2 +size 117704 diff --git a/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff b/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff new file mode 100644 index 0000000000..621ef158aa --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_gray_deflate.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2314b31ca9938fa8b11cbabda0b118a90025a45d2931fca9afa131c0d6919aca +size 557717 diff --git a/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff new file mode 100644 index 0000000000..f44a6e9343 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_gray_deflate_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9576b3a49b84e26938a7e9ded5f43a1a3c3390bf4824803f5aaab8e00c1afb4 +size 630947 diff --git a/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff new file mode 100644 index 0000000000..b14eeba8d5 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_gray_lzw_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f24fd8f36a4847fcb84a317de4fd2eacd5eb0c58ef4436d33919f0a6658d0d9 +size 698309 diff --git a/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff new file mode 100644 index 0000000000..5db7ef564e --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_grayscale_uncompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0283f2be39a151ca3ed19be97ebe4a6b17978ed251dd4d0d568895865fec24c7 +size 964588 diff --git a/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff b/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff new file mode 100644 index 0000000000..e0a39d2483 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_huffman_rle.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a4f687de9925863b1c9f32f53b6c05fb121f9d7b02ff5869113c4745433f10d +size 124644 diff --git a/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff new file mode 100644 index 0000000000..1592645c8e --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_palette_uncompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b70500348b1af7828c15e7782eaca105ff749136d7c45eb4cab8c5cd5269c3f6 +size 966134 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff new file mode 100644 index 0000000000..c2ebed3649 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_rgb_deflate_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da6e6a35a0bb0f5f2d49e3c5f0eb2deb7118718dd08844f66a6cb72f48b5c489 +size 1476294 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff new file mode 100644 index 0000000000..c9f5fadee8 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_rgb_jpeg.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba79ffac35e16208406e073492d770d3a77e51a47112aa02ab8fd98b5a8487b2 +size 198564 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif b/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif new file mode 100644 index 0000000000..7450522679 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_rgb_lzw.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53006876fcdc655a794462de57eb6b56f4d0cdd3cb8b752c63328db0eb4aa3c1 +size 725085 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff new file mode 100644 index 0000000000..99642af524 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_rgb_lzw_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecb529e5e3e0eca6f5e407b034fa8ba67bb4b9068af9e9b30425b08d30a249c0 +size 1756355 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff new file mode 100644 index 0000000000..862db0b39f --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_rgb_packbits.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59dbb48f10c40cbbd4f5617a9f57536790ce0b9a4cc241dc8d6257095598cb76 +size 2891292 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_palette_lzw_predictor.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_palette_lzw_predictor.tiff new file mode 100644 index 0000000000..be84f0a30a --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_rgb_palette_lzw_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29fa2b157c92f6a8bd4036e9d075e24fc451e72ec1a251d97a4b40454e01405c +size 792087 diff --git a/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff b/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff new file mode 100644 index 0000000000..7ebd74d9d4 --- /dev/null +++ b/tests/Images/Input/Tiff/Calliphora_rgb_uncompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acb46c990af78fcb0e63849f0f26acffe26833d5cfd2fc77a728baf92166eea3 +size 2893218 diff --git a/tests/Images/Input/Tiff/b0350_fillorder2.tiff b/tests/Images/Input/Tiff/b0350_fillorder2.tiff new file mode 100644 index 0000000000..3b7ee6ac3a --- /dev/null +++ b/tests/Images/Input/Tiff/b0350_fillorder2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37c6a28f460d8781fdc3bcf0cc9bd23f633b03899563546bfc6234a8478f67f0 +size 68637 diff --git a/tests/Images/Input/Tiff/basi3p02_huffman_rle.tiff b/tests/Images/Input/Tiff/basi3p02_huffman_rle.tiff new file mode 100644 index 0000000000..2b290438a3 --- /dev/null +++ b/tests/Images/Input/Tiff/basi3p02_huffman_rle.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af20deb1b64cac3272b6560565cb01f28247b9fd8b6d5a86eafbe7b0aea27d48 +size 340 diff --git a/tests/Images/Input/Tiff/bike_colorpalette_4bit.tiff b/tests/Images/Input/Tiff/bike_colorpalette_4bit.tiff new file mode 100644 index 0000000000..d76966336d --- /dev/null +++ b/tests/Images/Input/Tiff/bike_colorpalette_4bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc9047bc28ff5256530a7203bf73fa0a7ed1545736cd57fabdaff2d600cfa3f1 +size 132265 diff --git a/tests/Images/Input/Tiff/ccitt_fax3_all_makeup_codes.tiff b/tests/Images/Input/Tiff/ccitt_fax3_all_makeup_codes.tiff new file mode 100644 index 0000000000..6a1153bac7 --- /dev/null +++ b/tests/Images/Input/Tiff/ccitt_fax3_all_makeup_codes.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60b64bd3c24437eb90c0a17a4328e997702d7e4c0889ec90abde092ab9b490e8 +size 546 diff --git a/tests/Images/Input/Tiff/ccitt_fax3_all_terminating_codes.tiff b/tests/Images/Input/Tiff/ccitt_fax3_all_terminating_codes.tiff new file mode 100644 index 0000000000..a2da71cf61 --- /dev/null +++ b/tests/Images/Input/Tiff/ccitt_fax3_all_terminating_codes.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:faa92346ccff9cc7e3275b31cbc4ec054e27d0d0ed20a215a22b6178c2d7adf0 +size 564 diff --git a/tests/Images/Input/Tiff/ccitt_fax3_uncompressed.tiff b/tests/Images/Input/Tiff/ccitt_fax3_uncompressed.tiff new file mode 100644 index 0000000000..d1bbbec427 --- /dev/null +++ b/tests/Images/Input/Tiff/ccitt_fax3_uncompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f60615dea1b417ec125bf365b7c3a7c15ee2381947eb29dae2c34655fd0c2530 +size 821 diff --git a/tests/Images/Input/Tiff/grayscale_deflate_multistrip.tiff b/tests/Images/Input/Tiff/grayscale_deflate_multistrip.tiff new file mode 100644 index 0000000000..6e57bb56e8 --- /dev/null +++ b/tests/Images/Input/Tiff/grayscale_deflate_multistrip.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaee95d80f1e9eb9afbb7447da78a685f29359181ce71c045cff3aacda28a916 +size 14530 diff --git a/tests/Images/Input/Tiff/grayscale_uncompressed.tiff b/tests/Images/Input/Tiff/grayscale_uncompressed.tiff new file mode 100644 index 0000000000..570edfc6d9 --- /dev/null +++ b/tests/Images/Input/Tiff/grayscale_uncompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fefe6f6e1daf270546848c23ef437cfd072abb812e539fbab1006d74d416e9a4 +size 65758 diff --git a/tests/Images/Input/Tiff/huffman_rle_all_makeup_codes.tiff b/tests/Images/Input/Tiff/huffman_rle_all_makeup_codes.tiff new file mode 100644 index 0000000000..643805ca77 --- /dev/null +++ b/tests/Images/Input/Tiff/huffman_rle_all_makeup_codes.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa8dfeb96763b2b35b5f06f37021d7e33551485105ad4a3a704d76b3aecf039d +size 518 diff --git a/tests/Images/Input/Tiff/huffman_rle_all_terminating_codes.tiff b/tests/Images/Input/Tiff/huffman_rle_all_terminating_codes.tiff new file mode 100644 index 0000000000..2ab78c71ec --- /dev/null +++ b/tests/Images/Input/Tiff/huffman_rle_all_terminating_codes.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99a488957403e3c35f7dfbbcbe7f187a74e6cab61b233c91f4892079c04984fd +size 550 diff --git a/tests/Images/Input/Tiff/iptc.tiff b/tests/Images/Input/Tiff/iptc.tiff new file mode 100644 index 0000000000..ed06ff4117 --- /dev/null +++ b/tests/Images/Input/Tiff/iptc.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7a8d89df97b35ab3be9745a345687defd427bf4ca519ad3c5a52208d98f7694 +size 15170 diff --git a/tests/Images/Input/Tiff/little_endian.tiff b/tests/Images/Input/Tiff/little_endian.tiff new file mode 100644 index 0000000000..64653d620e --- /dev/null +++ b/tests/Images/Input/Tiff/little_endian.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b42205ddeb20f8bdb1182bdf1345e695be4bf9617ba0576bef0d5b76642fa1a +size 191232 diff --git a/tests/Images/Input/Tiff/metadata_sample.tiff b/tests/Images/Input/Tiff/metadata_sample.tiff new file mode 100644 index 0000000000..d767352688 --- /dev/null +++ b/tests/Images/Input/Tiff/metadata_sample.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72a1c8022d699e0e7248940f0734d01d6ab9bf4a71022e8b5626b64d66a5f39d +size 8107 diff --git a/tests/Images/Input/Tiff/moy.tiff b/tests/Images/Input/Tiff/moy.tiff new file mode 100644 index 0000000000..197aade491 --- /dev/null +++ b/tests/Images/Input/Tiff/moy.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:026bb9372882d8fc15540b4f94e23138e75aacb0ebf2f5940b056fc66819ec46 +size 1968862 diff --git a/tests/Images/Input/Tiff/multipage_ withPreview_differentSize_tiled.tiff b/tests/Images/Input/Tiff/multipage_ withPreview_differentSize_tiled.tiff new file mode 100644 index 0000000000..164740c4a5 --- /dev/null +++ b/tests/Images/Input/Tiff/multipage_ withPreview_differentSize_tiled.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e5ef9ccff292ed33a352cd040326c1ceeefc2cd68aedf0598dbff8326deecf6 +size 113784 diff --git a/tests/Images/Input/Tiff/multipage_deflate_withPreview.tiff b/tests/Images/Input/Tiff/multipage_deflate_withPreview.tiff new file mode 100644 index 0000000000..4a4db4524e --- /dev/null +++ b/tests/Images/Input/Tiff/multipage_deflate_withPreview.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63360eb1e383d0d3830155a269c4b5f4b07f3a2cb386f18427ea1c5ae5f1817a +size 160015 diff --git a/tests/Images/Input/Tiff/multipage_differentSize.tiff b/tests/Images/Input/Tiff/multipage_differentSize.tiff new file mode 100644 index 0000000000..7caa44710d --- /dev/null +++ b/tests/Images/Input/Tiff/multipage_differentSize.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8476813a4c68883872a8a196f567a567d2351802f86114aef0c64e9786a7d8b9 +size 210533 diff --git a/tests/Images/Input/Tiff/multipage_differentVariants.tiff b/tests/Images/Input/Tiff/multipage_differentVariants.tiff new file mode 100644 index 0000000000..9bbb84d8ba --- /dev/null +++ b/tests/Images/Input/Tiff/multipage_differentVariants.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e54681a90093f0f613299eabf01db60ac971c96d26d281db01873b2cb9fb2d09 +size 483062 diff --git a/tests/Images/Input/Tiff/multipage_lzw.tiff b/tests/Images/Input/Tiff/multipage_lzw.tiff new file mode 100644 index 0000000000..d2595dcdc5 --- /dev/null +++ b/tests/Images/Input/Tiff/multipage_lzw.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da86a6d5fa61609edb54ef9118be16d89488f0cfce0acd16990e68b685f76094 +size 43432 diff --git a/tests/Images/Input/Tiff/palette_grayscale_deflate_multistrip.tiff b/tests/Images/Input/Tiff/palette_grayscale_deflate_multistrip.tiff new file mode 100644 index 0000000000..d68c53483d --- /dev/null +++ b/tests/Images/Input/Tiff/palette_grayscale_deflate_multistrip.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6b269e44539ea2e36f6357941c97a158ee288a7b44dab35338c241de69b5d37 +size 16078 diff --git a/tests/Images/Input/Tiff/palette_uncompressed.tiff b/tests/Images/Input/Tiff/palette_uncompressed.tiff new file mode 100644 index 0000000000..b282d65b56 --- /dev/null +++ b/tests/Images/Input/Tiff/palette_uncompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75f1bcaff7dc09ddbe6ded7b764b8c0b17bffc3392bafdc7bc7a4c7d616a38e5 +size 67394 diff --git a/tests/Images/Input/Tiff/rgb_deflate.tiff b/tests/Images/Input/Tiff/rgb_deflate.tiff new file mode 100644 index 0000000000..7abd84d866 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_deflate.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af0db6a42424e3db5c6b84be6e253817413b2de68cc91f7288a8434150fe088 +size 67130 diff --git a/tests/Images/Input/Tiff/rgb_deflate_multistrip.tiff b/tests/Images/Input/Tiff/rgb_deflate_multistrip.tiff new file mode 100644 index 0000000000..dc9b36a9f5 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_deflate_multistrip.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb8375584d0ba70626f0026bf91306c423a6c00f511362a3ce523cefb1e65d56 +size 68058 diff --git a/tests/Images/Input/Tiff/rgb_deflate_predictor.tiff b/tests/Images/Input/Tiff/rgb_deflate_predictor.tiff new file mode 100644 index 0000000000..97623cd5b6 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_deflate_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1db70e0cfb056cfc675db3a2b85a1f226c53cd70275808773ff580c738b3db1 +size 3158 diff --git a/tests/Images/Input/Tiff/rgb_jpeg.tiff b/tests/Images/Input/Tiff/rgb_jpeg.tiff new file mode 100644 index 0000000000..b198d1aba9 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_jpeg.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa7f77e16bc51a55f5d2cb8d162801ea9edc620e16ec7ab43323f3c994830399 +size 5736 diff --git a/tests/Images/Input/Tiff/rgb_lzw_multistrip.tiff b/tests/Images/Input/Tiff/rgb_lzw_multistrip.tiff new file mode 100644 index 0000000000..6cfc803bb6 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_lzw_multistrip.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ecb81526b238ca4a43b559a33a3b393b9776fe32fd2f3b78a1b460780f7ba9 +size 26962 diff --git a/tests/Images/Input/Tiff/rgb_lzw_noPredictor_multistrip.tiff b/tests/Images/Input/Tiff/rgb_lzw_noPredictor_multistrip.tiff new file mode 100644 index 0000000000..4d8c11afe2 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_lzw_noPredictor_multistrip.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b67399bf019d8a585a6433bcaf6299846b85cb7783cf9350340b900185e645c6 +size 155004 diff --git a/tests/Images/Input/Tiff/rgb_lzw_noPredictor_multistrip_Motorola.tiff b/tests/Images/Input/Tiff/rgb_lzw_noPredictor_multistrip_Motorola.tiff new file mode 100644 index 0000000000..59290df1c3 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_lzw_noPredictor_multistrip_Motorola.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3fa8877b14979d2b56c0f0acd18b1c797ffe5d4ab91fce5dad8e1177acf7f4d +size 155004 diff --git a/tests/Images/Input/Tiff/rgb_lzw_noPredictor_singlestrip_Motorola.tiff b/tests/Images/Input/Tiff/rgb_lzw_noPredictor_singlestrip_Motorola.tiff new file mode 100644 index 0000000000..557fb4c517 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_lzw_noPredictor_singlestrip_Motorola.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86cd325c7587d1712c91fed16d18a16dcbbbce97de6f9e3ae782e0e5da1ff541 +size 154735 diff --git a/tests/Images/Input/Tiff/rgb_lzw_no_predictor.tiff b/tests/Images/Input/Tiff/rgb_lzw_no_predictor.tiff new file mode 100644 index 0000000000..44092f6c7c --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_lzw_no_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:895f7e1fb17e42175e6c0d67fbc08a7c65d7e19a71e67388034cdaecc407407a +size 131092 diff --git a/tests/Images/Input/Tiff/rgb_lzw_predictor.tiff b/tests/Images/Input/Tiff/rgb_lzw_predictor.tiff new file mode 100644 index 0000000000..a1d3d77f46 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_lzw_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fe0ad0f383136e32f45c922de530ebcbce055f99ca81ef1d50608e241ea621c +size 25806 diff --git a/tests/Images/Input/Tiff/rgb_packbits.tiff b/tests/Images/Input/Tiff/rgb_packbits.tiff new file mode 100644 index 0000000000..28310cade6 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_packbits.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c5c7996d78fb97a43bdd6d9fec7c1cb6bdea546d73c21f5a068edc602ff3aa8 +size 198460 diff --git a/tests/Images/Input/Tiff/rgb_packbits_multistrip.tiff b/tests/Images/Input/Tiff/rgb_packbits_multistrip.tiff new file mode 100644 index 0000000000..fa9a8f2aeb --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_packbits_multistrip.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79ea79362ddad668b28cb919c91dc793b4246f33e411e3794cbe587c5461367a +size 198402 diff --git a/tests/Images/Input/Tiff/rgb_palette.tiff b/tests/Images/Input/Tiff/rgb_palette.tiff new file mode 100644 index 0000000000..b282d65b56 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_palette.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75f1bcaff7dc09ddbe6ded7b764b8c0b17bffc3392bafdc7bc7a4c7d616a38e5 +size 67394 diff --git a/tests/Images/Input/Tiff/rgb_palette_deflate.tiff b/tests/Images/Input/Tiff/rgb_palette_deflate.tiff new file mode 100644 index 0000000000..ef03cdb3e4 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_palette_deflate.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:434cc4c212dfa975c130e2acd7c704b9cc6d0bf168336b8f778f811ddaf6a812 +size 24990 diff --git a/tests/Images/Input/Tiff/rgb_small_deflate.tiff b/tests/Images/Input/Tiff/rgb_small_deflate.tiff new file mode 100644 index 0000000000..cd78dfc883 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_small_deflate.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09c964c2f11806c2ff1e9fc7b06ddbecbc9e94cb7f4bd2b9841f0a3939d98eef +size 2575 diff --git a/tests/Images/Input/Tiff/rgb_small_lzw.tiff b/tests/Images/Input/Tiff/rgb_small_lzw.tiff new file mode 100644 index 0000000000..deaeda645d --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_small_lzw.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2630a452dcc86f557594fe29ae4244fbb29a276cdee53835157af17f966e1405 +size 3221 diff --git a/tests/Images/Input/Tiff/rgb_uncompressed.tiff b/tests/Images/Input/Tiff/rgb_uncompressed.tiff new file mode 100644 index 0000000000..c9602763da --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_uncompressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87134a685bcd816d77cae664b415f1b6a25b78933953c128a742fba653eca9fa +size 196924 diff --git a/tests/Images/Input/Tiff/rgb_uncompressed_tiled.tiff b/tests/Images/Input/Tiff/rgb_uncompressed_tiled.tiff new file mode 100644 index 0000000000..0f49121361 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_uncompressed_tiled.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:165d85d8e3be6b44309855474c73ae6c14267393945d287fc20be4fcadc0e3f3 +size 3337