diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs index 075c708b6..0f8b1e16d 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.Gif; 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 @@ namespace SixLabors.ImageSharp 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 63b404cc4..af9531225 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/Tiff/Compression/CompressionFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/CompressionFactory.cs new file mode 100644 index 000000000..f65b3caf3 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/CompressionFactory.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + internal static class CompressionFactory + { + /// + /// Decompresses an image block from the input stream into the specified buffer. + /// + /// The input stream. + /// Type of the compression. + /// The offset within the file of the image block. + /// The size (in bytes) of the compressed data. + /// The buffer to write the uncompressed data. + public static void DecompressImageBlock(Stream stream, TiffCompressionType compressionType, uint offset, uint byteCount, byte[] buffer) + { + stream.Seek(offset, SeekOrigin.Begin); + + switch (compressionType) + { + case TiffCompressionType.None: + NoneTiffCompression.Decompress(stream, (int)byteCount, buffer); + break; + case TiffCompressionType.PackBits: + PackBitsTiffCompression.Decompress(stream, (int)byteCount, buffer); + break; + case TiffCompressionType.Deflate: + DeflateTiffCompression.Decompress(stream, (int)byteCount, buffer); + break; + case TiffCompressionType.Lzw: + LzwTiffCompression.Decompress(stream, (int)byteCount, buffer); + break; + default: + throw new InvalidOperationException(); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs index b42ac9ee1..f5295de4a 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs @@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff // The subsequent data is the Deflate compressed data (except for the last four bytes of checksum) int headerLength = fdict ? 10 : 6; SubStream subStream = new SubStream(stream, byteCount - headerLength); - using (DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress, true)) + using (DeflateStream deflateStream = new DeflateStream(subStream, CompressionMode.Decompress, true)) { deflateStream.ReadFull(buffer); } diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffByteOrder.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffByteOrder.cs new file mode 100644 index 000000000..b6418d11d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffByteOrder.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The tiff data stream byte order enum. + /// + public enum TiffByteOrder + { + /// + /// The big-endian byte order (Motorola). + /// + BigEndian, + + /// + /// The little-endian byte order (Intel). + /// + LittleEndian + } +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs index f8a661c1b..a8a46409f 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Enumeration representing the compression formats defined by the Tiff file-format. /// - internal enum TiffCompression + public enum TiffCompression : ushort { /// /// No compression. @@ -68,4 +68,4 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// ItuTRecT43 = 10 } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs index 024a41911..3febf2a96 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Enumeration representing the fill orders defined by the Tiff file-format. /// - internal enum TiffFillOrder + internal enum TiffFillOrder : ushort { /// /// Pixels with lower column values are stored in the higher-order bits of the byte. @@ -18,4 +18,4 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// LeastSignificantBitFirst = 2 } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs index 495b499f8..35c4439cb 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Enumeration representing the sub-file types defined by the Tiff file-format. /// [Flags] - internal enum TiffNewSubfileType + public enum TiffNewSubfileType : uint { /// /// A full-resolution image. @@ -41,4 +41,4 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// MixedRasterContent = 0x0008 } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs index c197965a6..dc8225a7a 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Enumeration representing the photometric interpretation formats defined by the Tiff file-format. /// - internal enum TiffPhotometricInterpretation + public enum TiffPhotometricInterpretation : ushort { /// /// Bilevel and grayscale: 0 is imaged as white. The maximum value is imaged as black. @@ -68,4 +68,4 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// LinearRaw = 34892 } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs index e0535be9d..e04e100e6 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Enumeration representing how the components of each pixel are stored the Tiff file-format. /// - internal enum TiffPlanarConfiguration + public enum TiffPlanarConfiguration : ushort { /// /// Chunky format. @@ -18,4 +18,4 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Planar = 2 } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs index 51c3a72ce..a523f0bc2 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Enumeration representing the resolution units defined by the Tiff file-format. /// - internal enum TiffResolutionUnit + public enum TiffResolutionUnit : ushort { /// /// No absolute unit of measurement. @@ -23,4 +23,4 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Centimeter = 3 } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs index 4b6b2061f..b66b72ce9 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Enumeration representing the sub-file types defined by the Tiff file-format. /// - internal enum TiffSubfileType + public enum TiffSubfileType : uint { /// /// Full-resolution image data. @@ -23,4 +23,4 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// SinglePage = 3 } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Tiff/MetadataExtensions.cs b/src/ImageSharp/Formats/Tiff/MetadataExtensions.cs new file mode 100644 index 000000000..b9da86fc4 --- /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.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor.cs index 224da447c..a19cd6d44 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -11,21 +10,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'BlackIsZero' photometric interpretation (optimised for bilevel images). /// - internal static class BlackIsZero1TiffColor + /// The pixel format. + internal class BlackIsZero1TiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + public BlackIsZero1TiffColor() + : base(null, null) + { + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor.cs index 1e59624b1..059de1a3e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; +using System; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -10,21 +10,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'BlackIsZero' photometric interpretation (optimised for 4-bit grayscale images). /// - internal static class BlackIsZero4TiffColor + internal class BlackIsZero4TiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + public BlackIsZero4TiffColor() + : base(null, null) + { + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor.cs index e34346fd4..5d50600d9 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; +using System; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -10,21 +10,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'BlackIsZero' photometric interpretation (optimised for 8-bit grayscale images). /// - internal static class BlackIsZero8TiffColor + internal class BlackIsZero8TiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + public BlackIsZero8TiffColor() + : base(null, null) + { + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor.cs index e5414da8b..7de303536 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor.cs @@ -3,7 +3,6 @@ using System; using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -12,34 +11,41 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'BlackIsZero' photometric interpretation (for all bit depths). /// - internal static class BlackIsZeroTiffColor + internal class BlackIsZeroTiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + private readonly ushort bitsPerSample0; + + private readonly float factor; + + public BlackIsZeroTiffColor(ushort[] bitsPerSample) + : base(bitsPerSample, null) + { + this.bitsPerSample0 = bitsPerSample[0]; + this.factor = (float)Math.Pow(2, this.bitsPerSample0) - 1.0f; + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// The buffer to read image data from. - /// The number of bits per sample for each pixel. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, uint[] bitsPerSample, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); BitReader bitReader = new BitReader(data); - float factor = (float)Math.Pow(2, bitsPerSample[0]) - 1.0f; for (int y = top; y < top + height; y++) { for (int x = left; x < left + width; x++) { - int value = bitReader.ReadBits(bitsPerSample[0]); - float intensity = ((float)value) / factor; + int value = bitReader.ReadBits(this.bitsPerSample0); + float intensity = ((float)value) / this.factor; color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); pixels[x, y] = color; } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor.cs index 509965641..7b4f50e80 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor.cs @@ -12,35 +12,40 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'PaletteTiffColor' photometric interpretation (for all bit depths). /// - internal static class PaletteTiffColor + internal class PaletteTiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + private readonly ushort bitsPerSample0; + + private readonly TPixel[] palette; + + public PaletteTiffColor(ushort[] bitsPerSample, ushort[] colorMap) + : base(bitsPerSample, colorMap) + { + this.bitsPerSample0 = bitsPerSample[0]; + int colorCount = (int)Math.Pow(2, this.bitsPerSample0); + this.palette = GeneratePalette(colorMap, colorCount); + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// The buffer to read image data from. - /// The number of bits per sample for each pixel. - /// The RGB color lookup table to use for decoding the image. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, uint[] bitsPerSample, uint[] colorMap, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { - int colorCount = (int)Math.Pow(2, bitsPerSample[0]); - TPixel[] palette = GeneratePalette(colorMap, colorCount); - BitReader bitReader = new BitReader(data); for (int y = top; y < top + height; y++) { for (int x = left; x < left + width; x++) { - int index = bitReader.ReadBits(bitsPerSample[0]); - pixels[x, y] = palette[index]; + int index = bitReader.ReadBits(this.bitsPerSample0); + pixels[x, y] = this.palette[index]; } bitReader.NextRow(); @@ -48,10 +53,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TPixel[] GeneratePalette(uint[] colorMap, int colorCount) - where TPixel : unmanaged, IPixel + private static TPixel[] GeneratePalette(ushort[] colorMap, int colorCount) { - TPixel[] palette = new TPixel[colorCount]; + var palette = new TPixel[colorCount]; int rOffset = 0; int gOffset = colorCount; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor.cs index c34d4f087..352c1e26c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; +using System; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -10,21 +10,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'RGB' photometric interpretation (optimised for 8-bit full color images). /// - internal static class Rgb888TiffColor + internal class Rgb888TiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + public Rgb888TiffColor() + : base(null, null) + { + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor.cs index b8a62b3ae..23f05c35f 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor.cs @@ -3,7 +3,6 @@ using System; using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -12,39 +11,58 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'RGB' photometric interpretation with 'Planar' layout (for all bit depths). /// - internal static class RgbPlanarTiffColor + internal class RgbPlanarTiffColor /* : TiffColorDecoder */ + where TPixel : unmanaged, IPixel { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly uint bitsPerSampleR; + + private readonly uint bitsPerSampleG; + + private readonly uint bitsPerSampleB; + + public RgbPlanarTiffColor(ushort[] bitsPerSample) + /* : base(bitsPerSample, null) */ + { + this.bitsPerSampleR = bitsPerSample[0]; + this.bitsPerSampleG = bitsPerSample[1]; + this.bitsPerSampleB = bitsPerSample[2]; + + this.rFactor = (float)Math.Pow(2, this.bitsPerSampleR) - 1.0f; + this.gFactor = (float)Math.Pow(2, this.bitsPerSampleG) - 1.0f; + this.bFactor = (float)Math.Pow(2, this.bitsPerSampleB) - 1.0f; + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// The buffers to read image data from. - /// The number of bits per sample for each pixel. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[][] data, uint[] bitsPerSample, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public void Decode(byte[][] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); - BitReader rBitReader = new BitReader(data[0]); - BitReader gBitReader = new BitReader(data[1]); - BitReader bBitReader = new BitReader(data[2]); - float rFactor = (float)Math.Pow(2, bitsPerSample[0]) - 1.0f; - float gFactor = (float)Math.Pow(2, bitsPerSample[1]) - 1.0f; - float bFactor = (float)Math.Pow(2, bitsPerSample[2]) - 1.0f; + var rBitReader = new BitReader(data[0]); + var gBitReader = new BitReader(data[1]); + var bBitReader = new BitReader(data[2]); for (int y = top; y < top + height; y++) { for (int x = left; x < left + width; x++) { - float r = ((float)rBitReader.ReadBits(bitsPerSample[0])) / rFactor; - float g = ((float)gBitReader.ReadBits(bitsPerSample[1])) / gFactor; - float b = ((float)bBitReader.ReadBits(bitsPerSample[2])) / bFactor; + float r = ((float)rBitReader.ReadBits(this.bitsPerSampleR)) / this.rFactor; + float g = ((float)gBitReader.ReadBits(this.bitsPerSampleG)) / this.gFactor; + float b = ((float)bBitReader.ReadBits(this.bitsPerSampleB)) / this.bFactor; + color.FromVector4(new Vector4(r, g, b, 1.0f)); pixels[x, y] = color; } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor.cs index 584beb365..35b51fd95 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor.cs @@ -3,7 +3,6 @@ using System; using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -12,37 +11,56 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'RGB' photometric interpretation (for all bit depths). /// - internal static class RgbTiffColor + internal class RgbTiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly uint bitsPerSampleR; + + private readonly uint bitsPerSampleG; + + private readonly uint bitsPerSampleB; + + public RgbTiffColor(ushort[] bitsPerSample) + : base(bitsPerSample, null) + { + this.bitsPerSampleR = bitsPerSample[0]; + this.bitsPerSampleG = bitsPerSample[1]; + this.bitsPerSampleB = bitsPerSample[2]; + + this.rFactor = (float)Math.Pow(2, this.bitsPerSampleR) - 1.0f; + this.gFactor = (float)Math.Pow(2, this.bitsPerSampleG) - 1.0f; + this.bFactor = (float)Math.Pow(2, this.bitsPerSampleB) - 1.0f; + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// The buffer to read image data from. - /// The number of bits per sample for each pixel. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, uint[] bitsPerSample, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); BitReader bitReader = new BitReader(data); - float rFactor = (float)Math.Pow(2, bitsPerSample[0]) - 1.0f; - float gFactor = (float)Math.Pow(2, bitsPerSample[1]) - 1.0f; - float bFactor = (float)Math.Pow(2, bitsPerSample[2]) - 1.0f; for (int y = top; y < top + height; y++) { for (int x = left; x < left + width; x++) { - float r = ((float)bitReader.ReadBits(bitsPerSample[0])) / rFactor; - float g = ((float)bitReader.ReadBits(bitsPerSample[1])) / gFactor; - float b = ((float)bitReader.ReadBits(bitsPerSample[2])) / bFactor; + float r = ((float)bitReader.ReadBits(this.bitsPerSampleR)) / this.rFactor; + float g = ((float)bitReader.ReadBits(this.bitsPerSampleG)) / this.gFactor; + float b = ((float)bitReader.ReadBits(this.bitsPerSampleB)) / this.bFactor; + color.FromVector4(new Vector4(r, g, b, 1.0f)); pixels[x, y] = color; } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoder.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoder.cs new file mode 100644 index 000000000..8c4f7e9b5 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoder.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The base class for photometric interpretation decoders. + /// + /// The pixel format. + internal abstract class TiffColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly ushort[] bitsPerSample; + + private readonly ushort[] colorMap; + + /// + /// Initializes a new instance of the class. + /// + /// The number of bits per sample for each pixel. + /// The RGB color lookup table to use for decoding the image. + protected TiffColorDecoder(ushort[] bitsPerSample, ushort[] colorMap) + { + this.bitsPerSample = bitsPerSample; + this.colorMap = colorMap; + } + + /* + /// + /// Gets the photometric interpretation value. + /// + /// + /// The photometric interpretation value. + /// + public TiffColorType ColorType { get; } + */ + + /// + /// 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. [MethodImpl(MethodImplOptions.AggressiveInlining)] + public abstract void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height); + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory.cs new file mode 100644 index 000000000..37bc96ef4 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + internal static class TiffColorDecoderFactory + where TPixel : unmanaged, IPixel + { + public static TiffColorDecoder 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 new InvalidOperationException(); + } + } + + 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 new InvalidOperationException(); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index f58b37431..c86a5e76c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Formats.Tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor.cs index 79784cbd9..666d03f9c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -11,21 +10,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'WhiteIsZero' photometric interpretation (optimised for bilevel images). /// - internal static class WhiteIsZero1TiffColor + internal class WhiteIsZero1TiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + public WhiteIsZero1TiffColor() + : base(null, null) + { + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor.cs index 061624349..9e5ce1f59 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; +using System; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -10,21 +10,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'WhiteIsZero' photometric interpretation (optimised for 4-bit grayscale images). /// - internal static class WhiteIsZero4TiffColor + internal class WhiteIsZero4TiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + public WhiteIsZero4TiffColor() + : base(null, null) + { + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs index eb8608173..4e3a0e443 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Runtime.CompilerServices; +using System; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -10,21 +10,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'WhiteIsZero' photometric interpretation (optimised for 8-bit grayscale images). /// - internal static class WhiteIsZero8TiffColor + internal class WhiteIsZero8TiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + public WhiteIsZero8TiffColor() + : base(null, null) + { + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs index f8492a510..329454e9d 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs @@ -3,7 +3,6 @@ using System; using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -12,34 +11,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Implements the 'WhiteIsZero' photometric interpretation (for all bit depths). /// - internal static class WhiteIsZeroTiffColor + internal class WhiteIsZeroTiffColor : TiffColorDecoder + where TPixel : unmanaged, IPixel { + private readonly ushort bitsPerSample0; + + private readonly float factor; + + public WhiteIsZeroTiffColor(ushort[] bitsPerSample) + : base(bitsPerSample, null) + { + this.bitsPerSample0 = bitsPerSample[0]; + this.factor = (float)Math.Pow(2, this.bitsPerSample0) - 1.0f; + } + /// /// Decodes pixel data using the current photometric interpretation. /// - /// The pixel format. /// The buffer to read image data from. - /// The number of bits per sample for each pixel. /// 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. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Decode(byte[] data, uint[] bitsPerSample, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel + public override void Decode(byte[] data, Buffer2D pixels, int left, int top, int width, int height) { TPixel color = default(TPixel); BitReader bitReader = new BitReader(data); - float factor = (float)Math.Pow(2, bitsPerSample[0]) - 1.0f; for (int y = top; y < top + height; y++) { for (int x = left; x < left + width; x++) { - int value = bitReader.ReadBits(bitsPerSample[0]); - float intensity = 1.0f - (((float)value) / factor); + int value = bitReader.ReadBits(this.bitsPerSample0); + float intensity = 1.0f - (((float)value) / this.factor); + color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); pixels[x, y] = color; } diff --git a/src/ImageSharp/Formats/Tiff/Streams/TiffBigEndianStream.cs b/src/ImageSharp/Formats/Tiff/Streams/TiffBigEndianStream.cs new file mode 100644 index 000000000..157937055 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Streams/TiffBigEndianStream.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + internal class TiffBigEndianStream : TiffStream + { + public TiffBigEndianStream(Stream stream) + : base(stream) + { + } + + public override TiffByteOrder ByteOrder => TiffByteOrder.BigEndian; + + /// + /// Converts buffer data into an using the correct endianness. + /// + /// The converted value. + public override short ReadInt16() + { + byte[] bytes = this.ReadBytes(2); + return (short)((bytes[0] << 8) | bytes[1]); + } + + /// + /// Converts buffer data into an using the correct endianness. + /// + /// The converted value. + public override int ReadInt32() + { + byte[] bytes = this.ReadBytes(4); + return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; + } + + /// + /// Converts buffer data into a using the correct endianness. + /// + /// The converted value. + public override uint ReadUInt32() + { + return (uint)this.ReadInt32(); + } + + /// + /// Converts buffer data into a using the correct endianness. + /// + /// The converted value. + public override ushort ReadUInt16() + { + return (ushort)this.ReadInt16(); + } + + /// + /// Converts buffer data into a using the correct endianness. + /// + /// The converted value. + public override float ReadSingle() + { + byte[] bytes = this.ReadBytes(4); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + return BitConverter.ToSingle(bytes, 0); + } + + /// + /// Converts buffer data into a using the correct endianness. + /// + /// The converted value. + public override double ReadDouble() + { + byte[] bytes = this.ReadBytes(8); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + return BitConverter.ToDouble(bytes, 0); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Streams/TiffLittleEndianStream.cs b/src/ImageSharp/Formats/Tiff/Streams/TiffLittleEndianStream.cs new file mode 100644 index 000000000..da6b8b8ef --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Streams/TiffLittleEndianStream.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + internal class TiffLittleEndianStream : TiffStream + { + public TiffLittleEndianStream(Stream stream) + : base(stream) + { + } + + public override TiffByteOrder ByteOrder => TiffByteOrder.LittleEndian; + + /// + /// Converts buffer data into an using the correct endianness. + /// + /// The converted value. + public override short ReadInt16() + { + byte[] bytes = this.ReadBytes(2); + return (short)(bytes[0] | (bytes[1] << 8)); + } + + /// + /// Converts buffer data into an using the correct endianness. + /// + /// The converted value. + public override int ReadInt32() + { + byte[] bytes = this.ReadBytes(4); + return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + } + + /// + /// Converts buffer data into a using the correct endianness. + /// + /// The converted value. + public override uint ReadUInt32() + { + return (uint)this.ReadInt32(); + } + + /// + /// Converts buffer data into a using the correct endianness. + /// + /// The converted value. + public override ushort ReadUInt16() + { + return (ushort)this.ReadInt16(); + } + + /// + /// Converts buffer data into a using the correct endianness. + /// + /// The converted value. + public override float ReadSingle() + { + byte[] bytes = this.ReadBytes(4); + + if (!BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + return BitConverter.ToSingle(bytes, 0); + } + + /// + /// Converts buffer data into a using the correct endianness. + /// + /// The converted value. + public override double ReadDouble() + { + byte[] bytes = this.ReadBytes(8); + + if (!BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + return BitConverter.ToDouble(bytes, 0); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Streams/TiffStream.cs b/src/ImageSharp/Formats/Tiff/Streams/TiffStream.cs new file mode 100644 index 000000000..0c62c01c3 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Streams/TiffStream.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The tiff data stream base class. + /// + internal abstract class TiffStream + { + /// + /// The input stream. + /// + private readonly Stream stream; + + /// + /// Initializes a new instance of the class. + /// + /// The stream. + protected TiffStream(Stream stream) + { + this.stream = stream; + } + + /// + /// Gets a value indicating whether the file is encoded in little-endian or big-endian format. + /// + public abstract TiffByteOrder ByteOrder { get; } + + /// + /// Gets the input stream. + /// + public Stream InputStream => this.stream; + + /// + /// Gets the stream position. + /// + public long Position => this.stream.Position; + + public void Seek(uint offset) + { + this.stream.Seek(offset, SeekOrigin.Begin); + } + + public void Skip(uint offset) + { + this.stream.Seek(offset, SeekOrigin.Current); + } + + public void Skip(int offset) + { + this.stream.Seek(offset, SeekOrigin.Current); + } + + /// + /// Converts buffer data into a using the correct endianness. + /// + /// The converted value. + public byte ReadByte() + { + return (byte)this.stream.ReadByte(); + } + + /// + /// Converts buffer data into an using the correct endianness. + /// + /// The converted value. + public sbyte ReadSByte() + { + return (sbyte)this.stream.ReadByte(); + } + + public byte[] ReadBytes(uint count) + { + byte[] buf = new byte[count]; + this.stream.Read(buf, 0, buf.Length); + return buf; + } + + public abstract short ReadInt16(); + + public abstract int ReadInt32(); + + public abstract uint ReadUInt32(); + + public abstract ushort ReadUInt16(); + + public abstract float ReadSingle(); + + public abstract double ReadDouble(); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Streams/TiffStreamFactory.cs b/src/ImageSharp/Formats/Tiff/Streams/TiffStreamFactory.cs new file mode 100644 index 000000000..7c9715380 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Streams/TiffStreamFactory.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The tiff data stream factory class. + /// + internal static class TiffStreamFactory + { + public static TiffStream CreateBySignature(Stream stream) + { + TiffByteOrder order = ReadByteOrder(stream); + return Create(order, stream); + } + + /// + /// Creates the specified byte order. + /// + /// The byte order. + /// The stream. + public static TiffStream Create(TiffByteOrder byteOrder, Stream stream) + { + if (byteOrder == TiffByteOrder.BigEndian) + { + return new TiffBigEndianStream(stream); + } + else if (byteOrder == TiffByteOrder.LittleEndian) + { + return new TiffLittleEndianStream(stream); + } + + throw new ArgumentOutOfRangeException(nameof(byteOrder)); + } + + /// + /// Reads the byte order of stream. + /// + /// The stream. + private static TiffByteOrder ReadByteOrder(Stream stream) + { + byte[] headerBytes = new byte[2]; + stream.Read(headerBytes, 0, 2); + if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) + { + return TiffByteOrder.LittleEndian; + } + else if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian) + { + return TiffByteOrder.BigEndian; + } + + throw new ImageFormatException("Invalid TIFF file header."); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs index e4bfc666a..74d11f1d4 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Image decoder for generating an image out of a TIFF stream. /// - public class TiffDecoder : IImageDecoder, ITiffDecoderOptions + public class TiffDecoder : IImageDecoder, ITiffDecoderOptions, IImageInfoDetector { /// /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. @@ -24,17 +24,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff { Guard.NotNull(stream, "stream"); - using (var decoder = new TiffDecoderCore(configuration, this)) + using (var decoder = new TiffDecoderCore(stream, configuration, this)) { - return decoder.Decode(stream); + return decoder.Decode(); } } /// - public Image Decode(Configuration configuration, Stream stream) - { - throw new System.NotImplementedException(); - } + public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); /// public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) @@ -48,5 +45,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff { throw new System.NotImplementedException(); } + + /// + public IImageInfo Identify(Configuration configuration, Stream stream) + { + throw new System.NotImplementedException(); + } + + /// + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index f134376ff..0d089287f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -3,9 +3,14 @@ using System; using System.Buffers; +using System.Collections.Generic; using System.IO; -using System.Text; +using System.Linq; +using System.Threading; +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 @@ -13,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Performs the tiff decoding operation. /// - internal class TiffDecoderCore : IDisposable + internal class TiffDecoderCore : IImageDecoderInternals, IDisposable { /// /// The global configuration @@ -30,7 +35,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// The configuration. /// The decoder options. - public TiffDecoderCore(Configuration configuration, ITiffDecoderOptions options) + private TiffDecoderCore(Configuration configuration, ITiffDecoderOptions options) { options = options ?? new TiffDecoder(); @@ -41,26 +46,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// Initializes a new instance of the class. /// - /// The input stream. - /// A flag indicating if the file is encoded in little-endian or big-endian format. + /// The stream. + /// The configuration. /// The decoder options. + public TiffDecoderCore(Stream stream, Configuration configuration, ITiffDecoderOptions options) + : this(configuration, options) + { + this.Stream = TiffStreamFactory.CreateBySignature(stream); + } + + /// + /// Initializes a new instance of the class. + /// + /// The byte order. + /// The input stream. /// The configuration. - public TiffDecoderCore(Stream stream, bool isLittleEndian, Configuration configuration, ITiffDecoderOptions options) + /// The decoder options. + public TiffDecoderCore(TiffByteOrder byteOrder, Stream stream, Configuration configuration, ITiffDecoderOptions options) : this(configuration, options) { - this.InputStream = stream; - this.IsLittleEndian = isLittleEndian; + this.Stream = TiffStreamFactory.Create(byteOrder, stream); } + /// + /// Gets the input stream. + /// + public TiffStream Stream { get; } + /// /// Gets or sets the number of bits for each sample of the pixel format used to encode the image. /// - public uint[] BitsPerSample { get; set; } + public ushort[] BitsPerSample { get; set; } /// /// Gets or sets the lookup table for RGB palette colored images. /// - public uint[] ColorMap { get; set; } + public ushort[] ColorMap { get; set; } /// /// Gets or sets the photometric interpretation implementation to use when decoding the image. @@ -72,451 +93,94 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffCompressionType CompressionType { get; set; } - /// - /// Gets the input stream. - /// - public Stream InputStream { get; private set; } - - /// - /// Gets a value indicating whether the file is encoded in little-endian or big-endian format. - /// - public bool IsLittleEndian { get; private set; } - /// /// Gets or sets the planar configuration type to use when decoding the image. /// public TiffPlanarConfiguration PlanarConfiguration { get; set; } /// - /// Calculates the size (in bytes) of the data contained within an IFD entry. + /// Gets or sets the photometric interpretation. /// - /// The IFD entry to calculate the size for. - /// The size of the data (in bytes). - public static uint GetSizeOfData(TiffIfdEntry entry) => SizeOfDataType(entry.Type) * entry.Count; + public TiffPhotometricInterpretation PhotometricInterpretation { get; set; } + + /// + public Configuration Configuration => this.configuration; + + /// + public Size Dimensions { get; private set; } /// /// Decodes the image from the specified and sets /// the data to image. /// /// The pixel format. - /// The stream, where the image should be. /// The decoded image. - public Image Decode(Stream stream) + public Image Decode() where TPixel : unmanaged, IPixel { - this.InputStream = stream; - - uint firstIfdOffset = this.ReadHeader(); - TiffIfd firstIfd = this.ReadIfd(firstIfdOffset); - Image image = this.DecodeImage(firstIfd); - - return image; - } - - /// - /// Dispose - /// - public void Dispose() - { - } + var reader = new DirectoryReader(this.Stream); + IEnumerable directories = reader.Read(); - /// - /// Reads the TIFF header from the input stream. - /// - /// The byte offset to the first IFD in the file. - /// - /// Thrown if the TIFF file header is invalid. - /// - public uint ReadHeader() - { - byte[] headerBytes = new byte[TiffConstants.SizeOfTiffHeader]; - this.InputStream.ReadFull(headerBytes, TiffConstants.SizeOfTiffHeader); - - if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) - { - this.IsLittleEndian = true; - } - else if (headerBytes[0] != TiffConstants.ByteOrderBigEndian && headerBytes[1] != TiffConstants.ByteOrderBigEndian) + var frames = new List>(); + foreach (IExifValue[] ifd in directories) { - throw new ImageFormatException("Invalid TIFF file header."); + ImageFrame frame = this.DecodeFrame(ifd); + frames.Add(frame); } - if (this.ToUInt16(headerBytes, 2) != TiffConstants.HeaderMagicNumber) - { - throw new ImageFormatException("Invalid TIFF file header."); - } + ImageMetadata metadata = frames.CreateMetadata(this.ignoreMetadata, this.Stream.ByteOrder); - uint firstIfdOffset = this.ToUInt32(headerBytes, 4); - if (firstIfdOffset == 0) + // todo: tiff frames can have different sizes { - throw new ImageFormatException("Invalid TIFF file header."); - } - - return firstIfdOffset; - } - - /// - /// Reads a from the input stream. - /// - /// The byte offset within the file to find the IFD. - /// A containing the retrieved data. - public TiffIfd ReadIfd(uint offset) - { - this.InputStream.Seek(offset, SeekOrigin.Begin); - - byte[] buffer = new byte[TiffConstants.SizeOfIfdEntry]; - - this.InputStream.ReadFull(buffer, 2); - ushort entryCount = this.ToUInt16(buffer, 0); - - TiffIfdEntry[] entries = new TiffIfdEntry[entryCount]; - for (int i = 0; i < entryCount; i++) - { - this.InputStream.ReadFull(buffer, TiffConstants.SizeOfIfdEntry); - - ushort tag = this.ToUInt16(buffer, 0); - TiffType type = (TiffType)this.ToUInt16(buffer, 2); - uint count = this.ToUInt32(buffer, 4); - byte[] value = new byte[] { buffer[8], buffer[9], buffer[10], buffer[11] }; - - entries[i] = new TiffIfdEntry(tag, type, count, value); + var root = frames.First(); + this.Dimensions = root.Size(); + foreach (var frame in frames) + { + if (frame.Size() != root.Size()) + { + throw new NotSupportedException("Images with different sizes are not supported"); + } + } } - this.InputStream.ReadFull(buffer, 4); - uint nextIfdOffset = this.ToUInt32(buffer, 0); + var image = new Image(this.configuration, metadata, frames); - return new TiffIfd(entries, nextIfdOffset); + return image; } /// - /// Decodes the image data from a specified IFD. + /// Dispose /// - /// The pixel format. - /// The IFD to read the image from. - /// The decoded image. - public Image DecodeImage(TiffIfd ifd) - where TPixel : unmanaged, IPixel + public void Dispose() { - if (!ifd.TryGetIfdEntry(TiffTags.ImageLength, out TiffIfdEntry imageLengthEntry) - || !ifd.TryGetIfdEntry(TiffTags.ImageWidth, out TiffIfdEntry imageWidthEntry)) - { - throw new ImageFormatException("The TIFF IFD does not specify the image dimensions."); - } - - int width = (int)this.ReadUnsignedInteger(ref imageWidthEntry); - int height = (int)this.ReadUnsignedInteger(ref imageLengthEntry); - - Image image = new Image(this.configuration, width, height); - - this.ReadMetadata(ifd, image); - this.ReadImageFormat(ifd); - - if (ifd.TryGetIfdEntry(TiffTags.RowsPerStrip, out TiffIfdEntry rowsPerStripEntry) - && ifd.TryGetIfdEntry(TiffTags.StripOffsets, out TiffIfdEntry stripOffsetsEntry) - && ifd.TryGetIfdEntry(TiffTags.StripByteCounts, out TiffIfdEntry stripByteCountsEntry)) - { - int rowsPerStrip = (int)this.ReadUnsignedInteger(ref rowsPerStripEntry); - uint[] stripOffsets = this.ReadUnsignedIntegerArray(ref stripOffsetsEntry); - uint[] stripByteCounts = this.ReadUnsignedIntegerArray(ref stripByteCountsEntry); - this.DecodeImageStrips(image, rowsPerStrip, stripOffsets, stripByteCounts); - } - - return image; + // nothing } /// - /// Reads the image metadata from a specified IFD. + /// Decodes the image data from a specified IFD. /// /// The pixel format. - /// The IFD to read the image from. - /// The image to write the metadata to. - public void ReadMetadata(TiffIfd ifd, Image image) + /// The IFD tags. + private ImageFrame DecodeFrame(IExifValue[] tags) where TPixel : unmanaged, IPixel { - TiffResolutionUnit resolutionUnit = (TiffResolutionUnit)this.ReadUnsignedInteger(ifd, TiffTags.ResolutionUnit, (uint)TiffResolutionUnit.Inch); - - if (resolutionUnit != TiffResolutionUnit.None) - { - double resolutionUnitFactor = resolutionUnit == TiffResolutionUnit.Centimeter ? 2.54 : 1.0; - - if (ifd.TryGetIfdEntry(TiffTags.XResolution, out TiffIfdEntry xResolutionEntry)) - { - Rational xResolution = this.ReadUnsignedRational(ref xResolutionEntry); - image.Metadata.HorizontalResolution = xResolution.ToDouble() * resolutionUnitFactor; - } - - if (ifd.TryGetIfdEntry(TiffTags.YResolution, out TiffIfdEntry yResolutionEntry)) - { - Rational yResolution = this.ReadUnsignedRational(ref yResolutionEntry); - image.Metadata.VerticalResolution = yResolution.ToDouble() * resolutionUnitFactor; - } - } + var coreMetadata = new ImageFrameMetadata(); + TiffFrameMetadata metadata = coreMetadata.GetTiffMetadata(); + metadata.Tags = tags; - if (!this.ignoreMetadata) - { - /* - if (ifd.TryGetIfdEntry(TiffTags.Artist, out TiffIfdEntry artistEntry)) - { - image.Metadata.Properties.Add(new ImageProperty(TiffMetadataNames.Artist, this.ReadString(ref artistEntry))); - } + this.VerifyAndParseOptions(metadata); - if (ifd.TryGetIfdEntry(TiffTags.Copyright, out TiffIfdEntry copyrightEntry)) - { - image.Metadata.Properties.Add(new ImageProperty(TiffMetadataNames.Copyright, this.ReadString(ref copyrightEntry))); - } + int width = (int)metadata.Width; + int height = (int)metadata.Height; + var frame = new ImageFrame(this.configuration, width, height, coreMetadata); - if (ifd.TryGetIfdEntry(TiffTags.DateTime, out TiffIfdEntry dateTimeEntry)) - { - image.Metadata.Properties.Add(new ImageProperty(TiffMetadataNames.DateTime, this.ReadString(ref dateTimeEntry))); - } + int rowsPerStrip = (int)metadata.RowsPerStrip; + uint[] stripOffsets = metadata.StripOffsets; + uint[] stripByteCounts = metadata.StripByteCounts; - if (ifd.TryGetIfdEntry(TiffTags.HostComputer, out TiffIfdEntry hostComputerEntry)) - { - image.Metadata.Properties.Add(new ImageProperty(TiffMetadataNames.HostComputer, this.ReadString(ref hostComputerEntry))); - } - - if (ifd.TryGetIfdEntry(TiffTags.ImageDescription, out TiffIfdEntry imageDescriptionEntry)) - { - image.Metadata.Properties.Add(new ImageProperty(TiffMetadataNames.ImageDescription, this.ReadString(ref imageDescriptionEntry))); - } - - if (ifd.TryGetIfdEntry(TiffTags.Make, out TiffIfdEntry makeEntry)) - { - image.Metadata.Properties.Add(new ImageProperty(TiffMetadataNames.Make, this.ReadString(ref makeEntry))); - } + this.DecodeImageStrips(frame, rowsPerStrip, stripOffsets, stripByteCounts); - if (ifd.TryGetIfdEntry(TiffTags.Model, out TiffIfdEntry modelEntry)) - { - image.Metadata.Properties.Add(new ImageProperty(TiffMetadataNames.Model, this.ReadString(ref modelEntry))); - } - - if (ifd.TryGetIfdEntry(TiffTags.Software, out TiffIfdEntry softwareEntry)) - { - image.Metadata.Properties.Add(new ImageProperty(TiffMetadataNames.Software, this.ReadString(ref softwareEntry))); - } - */ - } - } - - /// - /// Determines the TIFF compression and color types, and reads any associated parameters. - /// - /// The IFD to read the image format information for. - public void ReadImageFormat(TiffIfd ifd) - { - TiffCompression compression = (TiffCompression)this.ReadUnsignedInteger(ifd, TiffTags.Compression, (uint)TiffCompression.None); - - switch (compression) - { - case TiffCompression.None: - { - this.CompressionType = TiffCompressionType.None; - break; - } - - case TiffCompression.PackBits: - { - this.CompressionType = TiffCompressionType.PackBits; - break; - } - - case TiffCompression.Deflate: - case TiffCompression.OldDeflate: - { - this.CompressionType = TiffCompressionType.Deflate; - break; - } - - case TiffCompression.Lzw: - { - this.CompressionType = TiffCompressionType.Lzw; - break; - } - - default: - { - throw new NotSupportedException("The specified TIFF compression format is not supported."); - } - } - - this.PlanarConfiguration = (TiffPlanarConfiguration)this.ReadUnsignedInteger(ifd, TiffTags.PlanarConfiguration, (uint)TiffPlanarConfiguration.Chunky); - - TiffPhotometricInterpretation photometricInterpretation; - - if (ifd.TryGetIfdEntry(TiffTags.PhotometricInterpretation, out TiffIfdEntry photometricInterpretationEntry)) - { - photometricInterpretation = (TiffPhotometricInterpretation)this.ReadUnsignedInteger(ref photometricInterpretationEntry); - } - else - { - if (compression == TiffCompression.Ccitt1D) - { - photometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero; - } - else - { - throw new ImageFormatException("The TIFF photometric interpretation entry is missing."); - } - } - - if (ifd.TryGetIfdEntry(TiffTags.BitsPerSample, out TiffIfdEntry bitsPerSampleEntry)) - { - this.BitsPerSample = this.ReadUnsignedIntegerArray(ref bitsPerSampleEntry); - } - else - { - if (photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero || - photometricInterpretation == TiffPhotometricInterpretation.BlackIsZero) - { - this.BitsPerSample = new[] { 1u }; - } - else - { - throw new ImageFormatException("The TIFF BitsPerSample entry is missing."); - } - } - - switch (photometricInterpretation) - { - case TiffPhotometricInterpretation.WhiteIsZero: - { - if (this.BitsPerSample.Length == 1) - { - switch (this.BitsPerSample[0]) - { - case 8: - { - this.ColorType = TiffColorType.WhiteIsZero8; - break; - } - - case 4: - { - this.ColorType = TiffColorType.WhiteIsZero4; - break; - } - - case 1: - { - this.ColorType = TiffColorType.WhiteIsZero1; - break; - } - - default: - { - this.ColorType = TiffColorType.WhiteIsZero; - break; - } - } - } - else - { - throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); - } - - break; - } - - case TiffPhotometricInterpretation.BlackIsZero: - { - if (this.BitsPerSample.Length == 1) - { - switch (this.BitsPerSample[0]) - { - case 8: - { - this.ColorType = TiffColorType.BlackIsZero8; - break; - } - - case 4: - { - this.ColorType = TiffColorType.BlackIsZero4; - break; - } - - case 1: - { - this.ColorType = TiffColorType.BlackIsZero1; - break; - } - - default: - { - this.ColorType = TiffColorType.BlackIsZero; - break; - } - } - } - else - { - throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); - } - - break; - } - - case TiffPhotometricInterpretation.Rgb: - { - if (this.BitsPerSample.Length == 3) - { - if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) - { - if (this.BitsPerSample[0] == 8 && this.BitsPerSample[1] == 8 && this.BitsPerSample[2] == 8) - { - this.ColorType = TiffColorType.Rgb888; - } - else - { - this.ColorType = TiffColorType.Rgb; - } - } - else - { - this.ColorType = TiffColorType.RgbPlanar; - } - } - else - { - throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); - } - - break; - } - - case TiffPhotometricInterpretation.PaletteColor: - { - if (ifd.TryGetIfdEntry(TiffTags.ColorMap, out TiffIfdEntry colorMapEntry)) - { - this.ColorMap = this.ReadUnsignedIntegerArray(ref colorMapEntry); - - if (this.BitsPerSample.Length == 1) - { - switch (this.BitsPerSample[0]) - { - default: - { - this.ColorType = TiffColorType.PaletteColor; - break; - } - } - } - else - { - throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); - } - } - else - { - throw new ImageFormatException("The TIFF ColorMap entry is missing for a pallete color image."); - } - - break; - } - - default: - throw new NotSupportedException("The specified TIFF photometric interpretation is not supported."); - } + return frame; } /// @@ -526,7 +190,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// 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. - public int CalculateImageBufferSize(int width, int height, int plane) + private int CalculateImageBufferSize(int width, int height, int plane) { uint bitsPerPixel = 0; @@ -546,724 +210,60 @@ namespace SixLabors.ImageSharp.Formats.Tiff return bytesPerRow * height; } - /// - /// Decompresses an image block from the input stream into the specified buffer. - /// - /// The offset within the file of the image block. - /// The size (in bytes) of the compressed data. - /// The buffer to write the uncompressed data. - public void DecompressImageBlock(uint offset, uint byteCount, byte[] buffer) - { - this.InputStream.Seek(offset, SeekOrigin.Begin); - - switch (this.CompressionType) - { - case TiffCompressionType.None: - NoneTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer); - break; - case TiffCompressionType.PackBits: - PackBitsTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer); - break; - case TiffCompressionType.Deflate: - DeflateTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer); - break; - case TiffCompressionType.Lzw: - LzwTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer); - break; - default: - throw new InvalidOperationException(); - } - } - - /// - /// Decodes pixel data using the current photometric interpretation (chunky configuration). - /// - /// The pixel format. - /// 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 void ProcessImageBlockChunky(byte[] data, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel - { - switch (this.ColorType) - { - case TiffColorType.WhiteIsZero: - WhiteIsZeroTiffColor.Decode(data, this.BitsPerSample, pixels, left, top, width, height); - break; - case TiffColorType.WhiteIsZero1: - WhiteIsZero1TiffColor.Decode(data, pixels, left, top, width, height); - break; - case TiffColorType.WhiteIsZero4: - WhiteIsZero4TiffColor.Decode(data, pixels, left, top, width, height); - break; - case TiffColorType.WhiteIsZero8: - WhiteIsZero8TiffColor.Decode(data, pixels, left, top, width, height); - break; - case TiffColorType.BlackIsZero: - BlackIsZeroTiffColor.Decode(data, this.BitsPerSample, pixels, left, top, width, height); - break; - case TiffColorType.BlackIsZero1: - BlackIsZero1TiffColor.Decode(data, pixels, left, top, width, height); - break; - case TiffColorType.BlackIsZero4: - BlackIsZero4TiffColor.Decode(data, pixels, left, top, width, height); - break; - case TiffColorType.BlackIsZero8: - BlackIsZero8TiffColor.Decode(data, pixels, left, top, width, height); - break; - case TiffColorType.Rgb: - RgbTiffColor.Decode(data, this.BitsPerSample, pixels, left, top, width, height); - break; - case TiffColorType.Rgb888: - Rgb888TiffColor.Decode(data, pixels, left, top, width, height); - break; - case TiffColorType.PaletteColor: - PaletteTiffColor.Decode(data, this.BitsPerSample, this.ColorMap, pixels, left, top, width, height); - break; - default: - throw new InvalidOperationException(); - } - } - - /// - /// Decodes pixel data using the current photometric interpretation (planar configuration). - /// - /// The pixel format. - /// 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 void ProcessImageBlockPlanar(byte[][] data, Buffer2D pixels, int left, int top, int width, int height) - where TPixel : unmanaged, IPixel - { - switch (this.ColorType) - { - case TiffColorType.RgbPlanar: - RgbPlanarTiffColor.Decode(data, this.BitsPerSample, pixels, left, top, width, height); - break; - default: - throw new InvalidOperationException(); - } - } - - /// - /// Reads the data from a as an array of bytes. - /// - /// The to read. - /// The data. - public byte[] ReadBytes(ref TiffIfdEntry entry) - { - uint byteLength = GetSizeOfData(entry); - - if (entry.Value.Length < byteLength) - { - uint offset = this.ToUInt32(entry.Value, 0); - this.InputStream.Seek(offset, SeekOrigin.Begin); - - byte[] data = new byte[byteLength]; - this.InputStream.ReadFull(data, (int)byteLength); - entry.Value = data; - } - - return entry.Value; - } - - /// - /// Reads the data from a as an unsigned integer value. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a , or if - /// there is an array of items. - /// - public uint ReadUnsignedInteger(ref TiffIfdEntry entry) - { - if (entry.Count != 1) - { - throw new ImageFormatException($"Cannot read a single value from an array of multiple items."); - } - - switch (entry.Type) - { - case TiffType.Byte: - return (uint)this.ToByte(entry.Value, 0); - case TiffType.Short: - return (uint)this.ToUInt16(entry.Value, 0); - case TiffType.Long: - return this.ToUInt32(entry.Value, 0); - default: - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to an unsigned integer."); - } - } - - /// - /// Reads the data for a specified tag of a as an unsigned integer value. - /// - /// The to read from. - /// The tag ID to search for. - /// The default value if the entry is missing - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a , or if - /// there is an array of items. - /// - public uint ReadUnsignedInteger(TiffIfd ifd, ushort tag, uint defaultValue) - { - if (ifd.TryGetIfdEntry(tag, out TiffIfdEntry entry)) - { - return this.ReadUnsignedInteger(ref entry); - } - else - { - return defaultValue; - } - } - - /// - /// Reads the data from a as a signed integer value. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to an , or if - /// there is an array of items. - /// - public int ReadSignedInteger(ref TiffIfdEntry entry) - { - if (entry.Count != 1) - { - throw new ImageFormatException($"Cannot read a single value from an array of multiple items."); - } - - switch (entry.Type) - { - case TiffType.SByte: - return (int)this.ToSByte(entry.Value, 0); - case TiffType.SShort: - return (int)this.ToInt16(entry.Value, 0); - case TiffType.SLong: - return this.ToInt32(entry.Value, 0); - default: - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a signed integer."); - } - } - - /// - /// Reads the data from a as an array of unsigned integer values. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a . - /// - public uint[] ReadUnsignedIntegerArray(ref TiffIfdEntry entry) - { - byte[] bytes = this.ReadBytes(ref entry); - uint[] result = new uint[entry.Count]; - - switch (entry.Type) - { - case TiffType.Byte: - { - for (int i = 0; i < result.Length; i++) - { - result[i] = (uint)this.ToByte(bytes, i); - } - - break; - } - - case TiffType.Short: - { - for (int i = 0; i < result.Length; i++) - { - result[i] = (uint)this.ToUInt16(bytes, i * TiffConstants.SizeOfShort); - } - - break; - } - - case TiffType.Long: - { - for (int i = 0; i < result.Length; i++) - { - result[i] = this.ToUInt32(bytes, i * TiffConstants.SizeOfLong); - } - - break; - } - - default: - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to an unsigned integer."); - } - - return result; - } - - /// - /// Reads the data from a as an array of signed integer values. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to an . - /// - public int[] ReadSignedIntegerArray(ref TiffIfdEntry entry) - { - byte[] bytes = this.ReadBytes(ref entry); - int[] result = new int[entry.Count]; - - switch (entry.Type) - { - case TiffType.SByte: - { - for (int i = 0; i < result.Length; i++) - { - result[i] = (int)this.ToSByte(bytes, i); - } - - break; - } - - case TiffType.SShort: - { - for (int i = 0; i < result.Length; i++) - { - result[i] = (int)this.ToInt16(bytes, i * TiffConstants.SizeOfShort); - } - - break; - } - - case TiffType.SLong: - { - for (int i = 0; i < result.Length; i++) - { - result[i] = this.ToInt32(bytes, i * TiffConstants.SizeOfLong); - } - - break; - } - - default: - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a signed integer."); - } - - return result; - } - - /// - /// Reads the data from a as a value. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a . - /// - public string ReadString(ref TiffIfdEntry entry) - { - if (entry.Type != TiffType.Ascii) - { - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a string."); - } - - byte[] bytes = this.ReadBytes(ref entry); - - if (bytes[entry.Count - 1] != 0) - { - throw new ImageFormatException("The retrieved string is not null terminated."); - } - - return Encoding.UTF8.GetString(bytes, 0, (int)entry.Count - 1); - } - - /// - /// Reads the data from a as a value. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a , or if - /// there is an array of items. - /// - public Rational ReadUnsignedRational(ref TiffIfdEntry entry) - { - if (entry.Count != 1) - { - throw new ImageFormatException($"Cannot read a single value from an array of multiple items."); - } - - return this.ReadUnsignedRationalArray(ref entry)[0]; - } - - /// - /// Reads the data from a as a value. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a , or if - /// there is an array of items. - /// - public SignedRational ReadSignedRational(ref TiffIfdEntry entry) - { - if (entry.Count != 1) - { - throw new ImageFormatException($"Cannot read a single value from an array of multiple items."); - } - - return this.ReadSignedRationalArray(ref entry)[0]; - } - - /// - /// Reads the data from a as an array of values. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a . - /// - public Rational[] ReadUnsignedRationalArray(ref TiffIfdEntry entry) - { - if (entry.Type != TiffType.Rational) - { - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a Rational."); - } - - byte[] bytes = this.ReadBytes(ref entry); - Rational[] result = new Rational[entry.Count]; - - for (int i = 0; i < result.Length; i++) - { - uint numerator = this.ToUInt32(bytes, i * TiffConstants.SizeOfRational); - uint denominator = this.ToUInt32(bytes, (i * TiffConstants.SizeOfRational) + TiffConstants.SizeOfLong); - result[i] = new Rational(numerator, denominator); - } - - return result; - } - - /// - /// Reads the data from a as an array of values. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a . - /// - public SignedRational[] ReadSignedRationalArray(ref TiffIfdEntry entry) - { - if (entry.Type != TiffType.SRational) - { - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a SignedRational."); - } - - byte[] bytes = this.ReadBytes(ref entry); - SignedRational[] result = new SignedRational[entry.Count]; - - for (int i = 0; i < result.Length; i++) - { - int numerator = this.ToInt32(bytes, i * TiffConstants.SizeOfRational); - int denominator = this.ToInt32(bytes, (i * TiffConstants.SizeOfRational) + TiffConstants.SizeOfLong); - result[i] = new SignedRational(numerator, denominator); - } - - return result; - } - - /// - /// Reads the data from a as a value. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a , or if - /// there is an array of items. - /// - public float ReadFloat(ref TiffIfdEntry entry) - { - if (entry.Count != 1) - { - throw new ImageFormatException($"Cannot read a single value from an array of multiple items."); - } - - if (entry.Type != TiffType.Float) - { - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a float."); - } - - return this.ToSingle(entry.Value, 0); - } - - /// - /// Reads the data from a as a value. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a , or if - /// there is an array of items. - /// - public double ReadDouble(ref TiffIfdEntry entry) - { - if (entry.Count != 1) - { - throw new ImageFormatException($"Cannot read a single value from an array of multiple items."); - } - - return this.ReadDoubleArray(ref entry)[0]; - } - - /// - /// Reads the data from a as an array of values. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a . - /// - public float[] ReadFloatArray(ref TiffIfdEntry entry) - { - if (entry.Type != TiffType.Float) - { - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a float."); - } - - byte[] bytes = this.ReadBytes(ref entry); - float[] result = new float[entry.Count]; - - for (int i = 0; i < result.Length; i++) - { - result[i] = this.ToSingle(bytes, i * TiffConstants.SizeOfFloat); - } - - return result; - } - - /// - /// Reads the data from a as an array of values. - /// - /// The to read. - /// The data. - /// - /// Thrown if the data-type specified by the file cannot be converted to a . - /// - public double[] ReadDoubleArray(ref TiffIfdEntry entry) - { - if (entry.Type != TiffType.Double) - { - throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a double."); - } - - byte[] bytes = this.ReadBytes(ref entry); - double[] result = new double[entry.Count]; - - for (int i = 0; i < result.Length; i++) - { - result[i] = this.ToDouble(bytes, i * TiffConstants.SizeOfDouble); - } - - return result; - } - - /// - /// Calculates the size (in bytes) for the specified TIFF data-type. - /// - /// The data-type to calculate the size for. - /// The size of the data-type (in bytes). - private static uint SizeOfDataType(TiffType type) - { - switch (type) - { - case TiffType.Byte: - case TiffType.Ascii: - case TiffType.SByte: - case TiffType.Undefined: - return 1u; - case TiffType.Short: - case TiffType.SShort: - return 2u; - case TiffType.Long: - case TiffType.SLong: - case TiffType.Float: - case TiffType.Ifd: - return 4u; - case TiffType.Rational: - case TiffType.SRational: - case TiffType.Double: - return 8u; - default: - return 0u; - } - } - - /// - /// Converts buffer data into an using the correct endianness. - /// - /// The buffer. - /// The byte offset within the buffer. - /// The converted value. - private sbyte ToSByte(byte[] bytes, int offset) - { - return (sbyte)bytes[offset]; - } - - /// - /// Converts buffer data into an using the correct endianness. - /// - /// The buffer. - /// The byte offset within the buffer. - /// The converted value. - private short ToInt16(byte[] bytes, int offset) - { - if (this.IsLittleEndian) - { - return (short)(bytes[offset + 0] | (bytes[offset + 1] << 8)); - } - else - { - return (short)((bytes[offset + 0] << 8) | bytes[offset + 1]); - } - } - - /// - /// Converts buffer data into an using the correct endianness. - /// - /// The buffer. - /// The byte offset within the buffer. - /// The converted value. - private int ToInt32(byte[] bytes, int offset) - { - if (this.IsLittleEndian) - { - return bytes[offset + 0] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24); - } - else - { - return (bytes[offset + 0] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]; - } - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The buffer. - /// The byte offset within the buffer. - /// The converted value. - private byte ToByte(byte[] bytes, int offset) - { - return bytes[offset]; - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The buffer. - /// The byte offset within the buffer. - /// The converted value. - private uint ToUInt32(byte[] bytes, int offset) - { - return (uint)this.ToInt32(bytes, offset); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The buffer. - /// The byte offset within the buffer. - /// The converted value. - private ushort ToUInt16(byte[] bytes, int offset) - { - return (ushort)this.ToInt16(bytes, offset); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The buffer. - /// The byte offset within the buffer. - /// The converted value. - private float ToSingle(byte[] bytes, int offset) - { - byte[] buffer = new byte[4]; - Array.Copy(bytes, offset, buffer, 0, 4); - - if (this.IsLittleEndian != BitConverter.IsLittleEndian) - { - Array.Reverse(buffer); - } - - return BitConverter.ToSingle(buffer, 0); - } - - /// - /// Converts buffer data into a using the correct endianness. - /// - /// The buffer. - /// The byte offset within the buffer. - /// The converted value. - private double ToDouble(byte[] bytes, int offset) - { - byte[] buffer = new byte[8]; - Array.Copy(bytes, offset, buffer, 0, 8); - - if (this.IsLittleEndian != BitConverter.IsLittleEndian) - { - Array.Reverse(buffer); - } - - return BitConverter.ToDouble(buffer, 0); - } - /// /// Decodes the image data for strip encoded data. /// /// The pixel format. - /// The image to decode data into. + /// 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 DecodeImageStrips(Image image, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts) + private void DecodeImageStrips(ImageFrame frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts) where TPixel : unmanaged, IPixel { int stripsPerPixel = this.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? 1 : this.BitsPerSample.Length; int stripsPerPlane = stripOffsets.Length / stripsPerPixel; - Buffer2D pixels = image.GetRootFramePixelBuffer(); + Buffer2D pixels = frame.PixelBuffer; byte[][] stripBytes = new byte[stripsPerPixel][]; for (int stripIndex = 0; stripIndex < stripBytes.Length; stripIndex++) { - int uncompressedStripSize = this.CalculateImageBufferSize(image.Width, rowsPerStrip, stripIndex); + int uncompressedStripSize = this.CalculateImageBufferSize(frame.Width, rowsPerStrip, stripIndex); stripBytes[stripIndex] = ArrayPool.Shared.Rent(uncompressedStripSize); } try { + TiffColorDecoder chunkyDecoder = null; + RgbPlanarTiffColor planarDecoder = null; + if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) + { + chunkyDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap); + } + else + { + planarDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap); + } + for (int i = 0; i < stripsPerPlane; i++) { - int stripHeight = i < stripsPerPlane - 1 || image.Height % rowsPerStrip == 0 ? rowsPerStrip : image.Height % rowsPerStrip; + 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; - this.DecompressImageBlock(stripOffsets[stripIndex], stripByteCounts[stripIndex], stripBytes[planeIndex]); + CompressionFactory.DecompressImageBlock(this.Stream.InputStream, this.CompressionType, stripOffsets[stripIndex], stripByteCounts[stripIndex], stripBytes[planeIndex]); } if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) { - this.ProcessImageBlockChunky(stripBytes[0], pixels, 0, rowsPerStrip * i, image.Width, stripHeight); + chunkyDecoder.Decode(stripBytes[0], pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); } else { - this.ProcessImageBlockPlanar(stripBytes, pixels, 0, rowsPerStrip * i, image.Width, stripHeight); + planarDecoder.Decode(stripBytes, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); } } } @@ -1275,5 +275,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } } + + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + throw new NotImplementedException(); + } + + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs new file mode 100644 index 000000000..4aa24f24d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderHelpers.cs @@ -0,0 +1,344 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using SixLabors.ImageSharp.Formats.Tiff; +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 helper methods. + /// + internal static class TiffDecoderHelpers + { + public static ImageMetadata CreateMetadata(this IList> frames, bool ignoreMetadata, TiffByteOrder byteOrder) + where TPixel : unmanaged, IPixel + { + var coreMetadata = new ImageMetadata(); + TiffMetadata tiffMetadata = coreMetadata.GetTiffMetadata(); + tiffMetadata.ByteOrder = byteOrder; + + TiffFrameMetadata rootFrameMetadata = frames.First().Metadata.GetTiffMetadata(); + switch (rootFrameMetadata.ResolutionUnit) + { + case TiffResolutionUnit.None: + coreMetadata.ResolutionUnits = PixelResolutionUnit.AspectRatio; + break; + case TiffResolutionUnit.Inch: + coreMetadata.ResolutionUnits = PixelResolutionUnit.PixelsPerInch; + break; + case TiffResolutionUnit.Centimeter: + coreMetadata.ResolutionUnits = PixelResolutionUnit.PixelsPerCentimeter; + break; + } + + if (rootFrameMetadata.HorizontalResolution != null) + { + coreMetadata.HorizontalResolution = rootFrameMetadata.HorizontalResolution.Value; + } + + if (rootFrameMetadata.VerticalResolution != null) + { + coreMetadata.VerticalResolution = rootFrameMetadata.VerticalResolution.Value; + } + + if (!ignoreMetadata) + { + foreach (ImageFrame frame in frames) + { + TiffFrameMetadata frameMetadata = frame.Metadata.GetTiffMetadata(); + + if (tiffMetadata.XmpProfile == null) + { + byte[] buf = frameMetadata.GetArrayValue(ExifTag.XMP, true); + if (buf != null) + { + tiffMetadata.XmpProfile = buf; + } + } + + if (coreMetadata.IptcProfile == null) + { + byte[] buf = frameMetadata.GetArrayValue(ExifTag.IPTC, true); + if (buf != null) + { + coreMetadata.IptcProfile = new IptcProfile(buf); + } + } + + if (coreMetadata.IccProfile == null) + { + byte[] buf = frameMetadata.GetArrayValue(ExifTag.IccProfile, true); + if (buf != null) + { + coreMetadata.IccProfile = new IccProfile(buf); + } + } + } + } + + return coreMetadata; + } + + /// + /// Determines the TIFF compression and color types, and reads any associated parameters. + /// + /// The options. + /// The IFD entries container to read the image format information for. + public static void VerifyAndParseOptions(this TiffDecoderCore options, TiffFrameMetadata entries) + { + if (entries.ExtraSamples != null) + { + throw new NotSupportedException("ExtraSamples is not supported."); + } + + if (entries.FillOrder != TiffFillOrder.MostSignificantBitFirst) + { + throw new NotSupportedException("The lower-order bits of the byte FillOrder is not supported."); + } + + if (entries.GetArrayValue(ExifTag.TileOffsets, true) != null) + { + throw new NotSupportedException("The Tile images is not supported."); + } + + ParseCompression(options, entries.Compression); + + options.PlanarConfiguration = entries.PlanarConfiguration; + + ParsePhotometric(options, entries); + + ParseBitsPerSample(options, entries); + + ParseColorType(options, entries); + } + + private static void ParseColorType(this TiffDecoderCore options, TiffFrameMetadata entries) + { + switch (options.PhotometricInterpretation) + { + case TiffPhotometricInterpretation.WhiteIsZero: + { + if (options.BitsPerSample.Length == 1) + { + switch (options.BitsPerSample[0]) + { + case 8: + { + options.ColorType = TiffColorType.WhiteIsZero8; + break; + } + + case 4: + { + options.ColorType = TiffColorType.WhiteIsZero4; + break; + } + + case 1: + { + options.ColorType = TiffColorType.WhiteIsZero1; + break; + } + + default: + { + options.ColorType = TiffColorType.WhiteIsZero; + break; + } + } + } + else + { + throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + break; + } + + case TiffPhotometricInterpretation.BlackIsZero: + { + if (options.BitsPerSample.Length == 1) + { + switch (options.BitsPerSample[0]) + { + case 8: + { + options.ColorType = TiffColorType.BlackIsZero8; + break; + } + + case 4: + { + options.ColorType = TiffColorType.BlackIsZero4; + break; + } + + case 1: + { + options.ColorType = TiffColorType.BlackIsZero1; + break; + } + + default: + { + options.ColorType = TiffColorType.BlackIsZero; + break; + } + } + } + else + { + throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + break; + } + + case TiffPhotometricInterpretation.Rgb: + { + if (options.BitsPerSample.Length == 3) + { + if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky) + { + if (options.BitsPerSample[0] == 8 && options.BitsPerSample[1] == 8 && options.BitsPerSample[2] == 8) + { + options.ColorType = TiffColorType.Rgb888; + } + else + { + options.ColorType = TiffColorType.Rgb; + } + } + else + { + options.ColorType = TiffColorType.RgbPlanar; + } + } + else + { + throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + break; + } + + case TiffPhotometricInterpretation.PaletteColor: + { + options.ColorMap = entries.ColorMap; + if (options.ColorMap != null) + { + if (options.BitsPerSample.Length == 1) + { + switch (options.BitsPerSample[0]) + { + default: + { + options.ColorType = TiffColorType.PaletteColor; + break; + } + } + } + else + { + throw new NotSupportedException("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + } + else + { + throw new ImageFormatException("The TIFF ColorMap entry is missing for a palette color image."); + } + + break; + } + + default: + throw new NotSupportedException("The specified TIFF photometric interpretation is not supported: " + options.PhotometricInterpretation); + } + } + + private static void ParseBitsPerSample(this TiffDecoderCore options, TiffFrameMetadata entries) + { + options.BitsPerSample = entries.BitsPerSample; + if (options.BitsPerSample == null) + { + if (options.PhotometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero + || options.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero) + { + options.BitsPerSample = new[] { (ushort)1 }; + } + else + { + throw new ImageFormatException("The TIFF BitsPerSample entry is missing."); + } + } + } + + private static void ParsePhotometric(this TiffDecoderCore options, TiffFrameMetadata entries) + { + /* + if (!entries.TryGetSingleNumber(ExifTag.PhotometricInterpretation, out uint photometricInterpretation)) + { + if (entries.Compression == TiffCompression.Ccitt1D) + { + photometricInterpretation = (uint)TiffPhotometricInterpretation.WhiteIsZero; + } + else + { + throw new ImageFormatException("The TIFF photometric interpretation entry is missing."); + } + } + + options.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretation; + /* */ + + // There is no default for PhotometricInterpretation, and it is required. + options.PhotometricInterpretation = entries.PhotometricInterpretation; + } + + private static void ParseCompression(this TiffDecoderCore options, TiffCompression compression) + { + switch (compression) + { + case TiffCompression.None: + { + options.CompressionType = TiffCompressionType.None; + break; + } + + case TiffCompression.PackBits: + { + options.CompressionType = TiffCompressionType.PackBits; + break; + } + + case TiffCompression.Deflate: + case TiffCompression.OldDeflate: + { + options.CompressionType = TiffCompressionType.Deflate; + break; + } + + case TiffCompression.Lzw: + { + options.CompressionType = TiffCompressionType.Lzw; + break; + } + + default: + { + throw new NotSupportedException("The specified TIFF compression format is not supported: " + compression); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 2fa7e7925..8aa0edb97 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff @@ -45,9 +46,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - using (TiffWriter writer = new TiffWriter(stream)) + using (var writer = new TiffWriter(stream)) { long firstIfdMarker = this.WriteHeader(writer); + //// todo: multiframing is not support long nextIfdMarker = this.WriteImage(writer, image, firstIfdMarker); } } @@ -59,8 +61,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The marker to write the first IFD offset. public long WriteHeader(TiffWriter writer) { - ushort byteOrderMarker = BitConverter.IsLittleEndian ? TiffConstants.ByteOrderLittleEndianShort - : TiffConstants.ByteOrderBigEndianShort; + ushort byteOrderMarker = BitConverter.IsLittleEndian + ? TiffConstants.ByteOrderLittleEndianShort + : TiffConstants.ByteOrderBigEndianShort; writer.Write(byteOrderMarker); writer.Write((ushort)42); @@ -75,7 +78,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The to write data to. /// The IFD entries to write to the file. /// The marker to write the next IFD offset (if present). - public long WriteIfd(TiffWriter writer, List entries) + public long WriteIfd(TiffWriter writer, List entries) { if (entries.Count == 0) { @@ -83,27 +86,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff } uint dataOffset = (uint)writer.Position + (uint)(6 + (entries.Count * 12)); - List largeDataBlocks = new List(); + var largeDataBlocks = new List(); - entries.Sort((a, b) => a.Tag - b.Tag); + entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag); writer.Write((ushort)entries.Count); - foreach (TiffIfdEntry entry in entries) + foreach (ExifValue entry in entries) { - writer.Write(entry.Tag); - writer.Write((ushort)entry.Type); - writer.Write(entry.Count); - - if (entry.Value.Length <= 4) + writer.Write((ushort)entry.Tag); + writer.Write((ushort)entry.DataType); + writer.Write(ExifWriter.GetNumberOfComponents(entry)); + + uint lenght = ExifWriter.GetLength(entry); + var raw = new byte[lenght]; + int sz = ExifWriter.WriteValue(entry, raw, 0); + DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written"); + if (raw.Length <= 4) { - writer.WritePadded(entry.Value); + writer.WritePadded(raw); } else { - largeDataBlocks.Add(entry.Value); + largeDataBlocks.Add(raw); writer.Write(dataOffset); - dataOffset += (uint)(entry.Value.Length + (entry.Value.Length % 2)); + dataOffset += (uint)(raw.Length + (raw.Length % 2)); } } @@ -133,10 +140,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff public long WriteImage(TiffWriter writer, Image image, long ifdOffset) where TPixel : unmanaged, IPixel { - List ifdEntries = new List(); + var ifdEntries = new List(); this.AddImageFormat(image, ifdEntries); - this.AddMetadata(image, ifdEntries); writer.WriteMarker(ifdOffset, (uint)writer.Position); long nextIfdMarker = this.WriteIfd(writer, ifdEntries); @@ -144,83 +150,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff return nextIfdMarker; } - /// - /// Adds image metadata to the specified IFD. - /// - /// The pixel format. - /// The to encode from. - /// The metadata entries to add to the IFD. - public void AddMetadata(Image image, List ifdEntries) - where TPixel : unmanaged, IPixel - { - ifdEntries.AddUnsignedRational(TiffTags.XResolution, new Rational(image.Metadata.HorizontalResolution)); - ifdEntries.AddUnsignedRational(TiffTags.YResolution, new Rational(image.Metadata.VerticalResolution)); - ifdEntries.AddUnsignedShort(TiffTags.ResolutionUnit, (uint)TiffResolutionUnit.Inch); - - /* - foreach (ImageProperty metadata in image.Metadata.Properties) - { - switch (metadata.Name) - { - case TiffMetadataNames.Artist: - { - ifdEntries.AddAscii(TiffTags.Artist, metadata.Value); - break; - } - - case TiffMetadataNames.Copyright: - { - ifdEntries.AddAscii(TiffTags.Copyright, metadata.Value); - break; - } - - case TiffMetadataNames.DateTime: - { - ifdEntries.AddAscii(TiffTags.DateTime, metadata.Value); - break; - } - - case TiffMetadataNames.HostComputer: - { - ifdEntries.AddAscii(TiffTags.HostComputer, metadata.Value); - break; - } - - case TiffMetadataNames.ImageDescription: - { - ifdEntries.AddAscii(TiffTags.ImageDescription, metadata.Value); - break; - } - - case TiffMetadataNames.Make: - { - ifdEntries.AddAscii(TiffTags.Make, metadata.Value); - break; - } - - case TiffMetadataNames.Model: - { - ifdEntries.AddAscii(TiffTags.Model, metadata.Value); - break; - } - - case TiffMetadataNames.Software: - { - ifdEntries.AddAscii(TiffTags.Software, metadata.Value); - break; - } - } - } */ - } - /// /// Adds image format information to the specified IFD. /// /// The pixel format. /// The to encode from. /// The image format entries to add to the IFD. - public void AddImageFormat(Image image, List ifdEntries) - where TPixel : unmanaged, IPixel + public void AddImageFormat(Image image, List ifdEntries) + where TPixel : unmanaged, IPixel { throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Tiff/TiffFormat.cs b/src/ImageSharp/Formats/Tiff/TiffFormat.cs index 1ee5c89cc..0628ef530 100644 --- a/src/ImageSharp/Formats/Tiff/TiffFormat.cs +++ b/src/ImageSharp/Formats/Tiff/TiffFormat.cs @@ -1,15 +1,14 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; -using SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff { /// /// Encapsulates the means to encode and decode Tiff images. /// - public class TiffFormat : IImageFormat + public class TiffFormat : IImageFormat { private TiffFormat() { @@ -31,5 +30,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// 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 000000000..bffbd1818 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs @@ -0,0 +1,324 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Provides Tiff specific metadata information for the frame. + /// + public class TiffFrameMetadata : IDeepCloneable + { + private const TiffResolutionUnit DefaultResolutionUnit = TiffResolutionUnit.Inch; + + private const TiffPlanarConfiguration DefaultPlanarConfiguration = TiffPlanarConfiguration.Chunky; + + /// + /// Initializes a new instance of the class. + /// + public TiffFrameMetadata() + { + } + + /// + /// Gets or sets the Tiff directory tags list. + /// + public IList Tags { get; set; } + + /// Gets a general indication of the kind of data contained in this subfile. + /// A general indication of the kind of data contained in this subfile. + public TiffNewSubfileType NewSubfileType => this.GetSingleEnum(ExifTag.SubfileType, TiffNewSubfileType.FullImage); + + /// Gets a general indication of the kind of data contained in this subfile. + /// A general indication of the kind of data contained in this subfile. + public TiffSubfileType? SubfileType => this.GetSingleEnumNullable(ExifTag.OldSubfileType); + + /// + /// Gets the number of columns in the image, i.e., the number of pixels per row. + /// + public uint Width => this.GetSingleUInt(ExifTag.ImageWidth); + + /// + /// Gets the number of rows of pixels in the image. + /// + public uint Height => this.GetSingleUInt(ExifTag.ImageLength); + + /// + /// Gets the number of bits per component. + /// + public ushort[] BitsPerSample => this.GetArrayValue(ExifTag.BitsPerSample, true); + + /// Gets the compression scheme used on the image data. + /// The compression scheme used on the image data. + public TiffCompression Compression => this.GetSingleEnum(ExifTag.Compression); + + /// + /// Gets the color space of the image data. + /// + public TiffPhotometricInterpretation PhotometricInterpretation => this.GetSingleEnum(ExifTag.PhotometricInterpretation); + + /// + /// Gets the logical order of bits within a byte. + /// + internal TiffFillOrder FillOrder => this.GetSingleEnum(ExifTag.FillOrder, TiffFillOrder.MostSignificantBitFirst); + + /// + /// Gets the a string that describes the subject of the image. + /// + public string ImageDescription => this.GetString(ExifTag.ImageDescription); + + /// + /// Gets the scanner manufacturer. + /// + public string Make => this.GetString(ExifTag.Make); + + /// + /// Gets the scanner model name or number. + /// + public string Model => this.GetString(ExifTag.Model); + + /// Gets for each strip, the byte offset of that strip.. + public uint[] StripOffsets => this.GetArrayValue(ExifTag.StripOffsets); + + /// + /// Gets the number of rows per strip. + /// + public uint RowsPerStrip => this.GetSingleUInt(ExifTag.RowsPerStrip); + + /// + /// Gets for each strip, the number of bytes in the strip after compression. + /// + public uint[] StripByteCounts => this.GetArrayValue(ExifTag.StripByteCounts); + + /// Gets the resolution of the image in x- direction. + /// The density of the image in x- direction. + public double? HorizontalResolution + { + get + { + if (this.ResolutionUnit != TiffResolutionUnit.None) + { + double resolutionUnitFactor = this.ResolutionUnit == TiffResolutionUnit.Centimeter ? 2.54 : 1.0; + + if (this.TryGetSingle(ExifTag.XResolution, out Rational xResolution)) + { + return xResolution.ToDouble() * resolutionUnitFactor; + } + } + + return null; + } + } + + /// + /// Gets the resolution of the image in y- direction. + /// + /// The density of the image in y- direction. + public double? VerticalResolution + { + get + { + if (this.ResolutionUnit != TiffResolutionUnit.None) + { + double resolutionUnitFactor = this.ResolutionUnit == TiffResolutionUnit.Centimeter ? 2.54 : 1.0; + + if (this.TryGetSingle(ExifTag.YResolution, out Rational yResolution)) + { + return yResolution.ToDouble() * resolutionUnitFactor; + } + } + + return null; + } + } + + /// + /// Gets how the components of each pixel are stored. + /// + public TiffPlanarConfiguration PlanarConfiguration => this.GetSingleEnum(ExifTag.PlanarConfiguration, DefaultPlanarConfiguration); + + /// + /// Gets the unit of measurement for XResolution and YResolution. + /// + public TiffResolutionUnit ResolutionUnit => this.GetSingleEnum(ExifTag.ResolutionUnit, DefaultResolutionUnit); + + /// + /// Gets the name and version number of the software package(s) used to create the image. + /// + public string Software => this.GetString(ExifTag.Software); + + /// + /// Gets the date and time of image creation. + /// + public string DateTime => this.GetString(ExifTag.DateTime); + + /// + /// Gets the person who created the image. + /// + public string Artist => this.GetString(ExifTag.Artist); + + /// + /// Gets the computer and/or operating system in use at the time of image creation. + /// + public string HostComputer => this.GetString(ExifTag.HostComputer); + + /// + /// Gets a color map for palette color images. + /// + public ushort[] ColorMap => this.GetArrayValue(ExifTag.ColorMap, true); + + /// + /// Gets the description of extra components. + /// + public ushort[] ExtraSamples => this.GetArrayValue(ExifTag.ExtraSamples, true); + + /// + /// Gets the copyright notice. + /// + public string Copyright => this.GetString(ExifTag.Copyright); + + internal T[] GetArrayValue(ExifTag tag, bool optional = false) + where T : struct + { + if (this.TryGetArrayValue(tag, out T[] result)) + { + return result; + } + + if (!optional) + { + throw new ArgumentException("Required tag is not founded: " + tag, nameof(tag)); + } + + return null; + } + + private bool TryGetArrayValue(ExifTag tag, out T[] result) + where T : struct + { + foreach (IExifValue entry in this.Tags) + { + if (entry.Tag == tag) + { + DebugGuard.IsTrue(entry.IsArray, "Expected array entry"); + + result = (T[])entry.GetValue(); + return true; + } + } + + result = null; + return false; + } + + private string GetString(ExifTag tag) + { + foreach (IExifValue entry in this.Tags) + { + if (entry.Tag == tag) + { + DebugGuard.IsTrue(entry.DataType == ExifDataType.Ascii, "Expected string entry"); + object value = entry.GetValue(); + DebugGuard.IsTrue(value is string, "Expected string entry"); + + return (string)value; + } + } + + return null; + } + + private TEnum? GetSingleEnumNullable(ExifTag tag) + where TEnum : struct + where TTagValue : struct + { + if (!this.TryGetSingle(tag, out TTagValue value)) + { + return null; + } + + return (TEnum)(object)value; + } + + private TEnum GetSingleEnum(ExifTag tag, TEnum? defaultValue = null) + where TEnum : struct + where TTagValue : struct + { + if (!this.TryGetSingle(tag, out TTagValue value)) + { + if (defaultValue != null) + { + return defaultValue.Value; + } + + throw new ArgumentException("Required tag is not founded: " + tag, nameof(tag)); + } + + return (TEnum)(object)value; + } + + /* + private TEnum GetSingleEnum(ExifTag tag, TEnum? defaultValue = null) + where TEnum : struct + where TEnumParent : struct + where TTagValue : struct + { + if (!this.TryGetSingle(tag, out TTagValue value)) + { + if (defaultValue != null) + { + return defaultValue.Value; + } + + throw new ArgumentException("Required tag is not founded: " + tag, nameof(tag)); + } + + return (TEnum)(object)(TEnumParent)Convert.ChangeType(value, typeof(TEnumParent)); + } */ + + private uint GetSingleUInt(ExifTag tag) + { + if (this.TryGetSingle(tag, out uint result)) + { + return result; + } + + throw new ArgumentException("Required tag is not founded: " + tag, nameof(tag)); + } + + private bool TryGetSingle(ExifTag tag, out T result) + where T : struct + { + foreach (IExifValue entry in this.Tags) + { + if (entry.Tag == tag) + { + DebugGuard.IsTrue(!entry.IsArray, "Expected non array entry"); + + object value = entry.GetValue(); + + result = (T)value; + return true; + } + } + + result = default; + return false; + } + + /// + public IDeepCloneable DeepClone() + { + var tags = new List(); + foreach (IExifValue entry in this.Tags) + { + tags.Add(entry.DeepClone()); + } + + return new TiffFrameMetadata() { Tags = tags }; + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfd.cs b/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfd.cs index 65d53e153..693b3abfc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfd.cs +++ b/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfd.cs @@ -1,63 +1,104 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Collections.Generic; +using System.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + namespace SixLabors.ImageSharp.Formats.Tiff { /// - /// Data structure for holding details of each TIFF IFD. + /// The TIFF IFD reader class. /// - internal struct TiffIfd + internal class DirectoryReader { - /// - /// An array of the entries within this IFD. - /// - public TiffIfdEntry[] Entries; - - /// - /// Offset (in bytes) to the next IFD, or zero if this is the last IFD. - /// - public uint NextIfdOffset; - - /// - /// Initializes a new instance of the struct. - /// - /// An array of the entries within the IFD. - /// Offset (in bytes) to the next IFD, or zero if this is the last IFD. - public TiffIfd(TiffIfdEntry[] entries, uint nextIfdOffset) + private readonly TiffStream stream; + + private readonly EntryReader tagReader; + + private uint nextIfdOffset; + + public DirectoryReader(TiffStream stream) { - this.Entries = entries; - this.NextIfdOffset = nextIfdOffset; + this.stream = stream; + this.tagReader = new EntryReader(stream); } - /// - /// Gets the child with the specified tag ID. - /// - /// The tag ID to search for. - /// The resulting , or null if it does not exists. - public TiffIfdEntry? GetIfdEntry(ushort tag) + public IEnumerable Read() { - for (int i = 0; i < this.Entries.Length; i++) + if (this.ReadHeader()) { - if (this.Entries[i].Tag == tag) - { - return this.Entries[i]; - } + return this.ReadIfds(); } return null; } - /// - /// Gets the child with the specified tag ID. - /// - /// The tag ID to search for. - /// The resulting , if it exists. - /// A flag indicating whether the requested entry exists. - public bool TryGetIfdEntry(ushort tag, out TiffIfdEntry entry) + private bool ReadHeader() + { + ushort magic = this.stream.ReadUInt16(); + if (magic != TiffConstants.HeaderMagicNumber) + { + throw new ImageFormatException("Invalid TIFF header magic number: " + magic); + } + + uint firstIfdOffset = this.stream.ReadUInt32(); + if (firstIfdOffset == 0) + { + throw new ImageFormatException("Invalid TIFF file header."); + } + + this.nextIfdOffset = firstIfdOffset; + + return true; + } + + private IEnumerable ReadIfds() { - TiffIfdEntry? nullableEntry = this.GetIfdEntry(tag); - entry = nullableEntry ?? default(TiffIfdEntry); - return nullableEntry.HasValue; + var list = new List(); + while (this.nextIfdOffset != 0) + { + this.stream.Seek(this.nextIfdOffset); + IExifValue[] ifd = this.ReadIfd(); + list.Add(ifd); + } + + this.tagReader.LoadExtendedData(); + + return list; + } + + private IExifValue[] ReadIfd() + { + long pos = this.stream.Position; + + ushort entryCount = this.stream.ReadUInt16(); + var entries = new List(entryCount); + for (int i = 0; i < entryCount; i++) + { + IExifValue tag = this.tagReader.ReadNext(); + if (tag != null) + { + entries.Add(tag); + } + } + + this.nextIfdOffset = this.stream.ReadUInt32(); + + int ifdSize = 2 + (entryCount * TiffConstants.SizeOfIfdEntry) + 4; + int readedBytes = (int)(this.stream.Position - pos); + int leftBytes = ifdSize - readedBytes; + if (leftBytes > 0) + { + this.stream.Skip(leftBytes); + } + else if (leftBytes < 0) + { + throw new InvalidDataException("Out of range of IFD structure."); + } + + return entries.ToArray(); } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfdEntry.cs b/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfdEntry.cs index b98c3b862..fe0ab4ccb 100644 --- a/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfdEntry.cs +++ b/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfdEntry.cs @@ -1,46 +1,310 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + namespace SixLabors.ImageSharp.Formats.Tiff { - /// - /// Data structure for holding details of each TIFF IFD entry. - /// - internal struct TiffIfdEntry + internal class EntryReader { - /// - /// The Tag ID for this entry. See for typical values. - /// - public ushort Tag; + private readonly TiffStream stream; - /// - /// The data-type of this entry. - /// - public TiffType Type; + private readonly SortedDictionary extValueLoaders = new SortedDictionary(); /// - /// The number of array items in this entry, or one if only a single value. + /// Initializes a new instance of the class. /// - public uint Count; + /// The stream. + public EntryReader(TiffStream stream) + { + this.stream = stream; + } - /// - /// The raw byte data for this entry. - /// - public byte[] Value; + public IExifValue ReadNext() + { + var tagId = (ExifTagValue)this.stream.ReadUInt16(); + var dataType = (ExifDataType)EnumUtils.Parse(this.stream.ReadUInt16(), ExifDataType.Unknown); + uint count = this.stream.ReadUInt32(); - /// - /// Initializes a new instance of the struct. - /// - /// The Tag ID for this entry. - /// The data-type of this entry. - /// The number of array items in this entry. - /// The raw byte data for this entry. - public TiffIfdEntry(ushort tag, TiffType type, uint count, byte[] value) - { - this.Tag = tag; - this.Type = type; - this.Count = count; - this.Value = value; + ExifDataType rawDataType = dataType; + dataType = LongOrShortFiltering(tagId, dataType); + bool isArray = GetIsArray(tagId, count); + + ExifValue entry = ExifValues.Create(tagId, dataType, isArray); + if (rawDataType == ExifDataType.Undefined && count == 0) + { + // todo: investgate + count = 4; + } + + if (this.ReadValueOrOffset(entry, rawDataType, count)) + { + return entry; + } + + return null; // new UnkownExifTag(tagId); + } + + public void LoadExtendedData() + { + foreach (Action action in this.extValueLoaders.Values) + { + action(); + } + } + + private static bool HasExtData(ExifValue tag, uint count) => ExifDataTypes.GetSize(tag.DataType) * count > 4; + + private static bool SetValue(ExifValue entry, object value) + { + if (!entry.IsArray && entry.DataType != ExifDataType.Ascii) + { + DebugGuard.IsTrue(((Array)value).Length == 1, "Expected a length is 1"); + + var single = ((Array)value).GetValue(0); + return entry.TrySetValue(single); + } + + return entry.TrySetValue(value); + } + + private static ExifDataType LongOrShortFiltering(ExifTagValue tagId, ExifDataType dataType) + { + switch (tagId) + { + case ExifTagValue.ImageWidth: + case ExifTagValue.ImageLength: + case ExifTagValue.StripOffsets: + case ExifTagValue.RowsPerStrip: + case ExifTagValue.StripByteCounts: + case ExifTagValue.TileWidth: + case ExifTagValue.TileLength: + case ExifTagValue.TileOffsets: + case ExifTagValue.TileByteCounts: + case ExifTagValue.OldSubfileType: // by spec SHORT, but can be LONG + return ExifDataType.Long; + + default: + return dataType; + } + } + + private static bool GetIsArray(ExifTagValue tagId, uint count) + { + switch (tagId) + { + case ExifTagValue.BitsPerSample: + case ExifTagValue.StripOffsets: + case ExifTagValue.StripByteCounts: + case ExifTagValue.TileOffsets: + case ExifTagValue.TileByteCounts: + case ExifTagValue.ColorMap: + case ExifTagValue.ExtraSamples: + return true; + + default: + return count > 1; + } + } + + private bool ReadValueOrOffset(ExifValue entry, ExifDataType rawDataType, uint count) + { + if (HasExtData(entry, count)) + { + uint offset = this.stream.ReadUInt32(); + this.extValueLoaders.Add(offset, () => + { + this.ReadExtValue(entry, rawDataType, offset, count); + }); + + return true; + } + + long pos = this.stream.Position; + object value = this.ReadData(entry.DataType, rawDataType, count); + if (value == null) + { + // read unknown type value + value = this.stream.ReadBytes(4); + } + else + { + int leftBytes = 4 - (int)(this.stream.Position - pos); + if (leftBytes > 0) + { + this.stream.Skip(leftBytes); + } + else if (leftBytes < 0) + { + throw new InvalidDataException("Out of range of IFD entry structure."); + } + } + + return SetValue(entry, value); + } + + private void ReadExtValue(ExifValue entry, ExifDataType rawDataType, uint offset, uint count) + { + DebugGuard.IsTrue(HasExtData(entry, count), "Excepted extended data"); + DebugGuard.MustBeGreaterThanOrEqualTo(offset, (uint)TiffConstants.SizeOfTiffHeader, nameof(offset)); + + this.stream.Seek(offset); + var value = this.ReadData(entry.DataType, rawDataType, count); + + SetValue(entry, value); + + DebugGuard.IsTrue(entry.DataType == ExifDataType.Ascii || count > 1 ^ !entry.IsArray, "Invalid tag"); + DebugGuard.IsTrue(entry.GetValue() != null, "Invalid tag"); + } + + private object ReadData(ExifDataType entryDataType, ExifDataType rawDataType, uint count) + { + switch (rawDataType) + { + case ExifDataType.Byte: + case ExifDataType.Undefined: + { + return this.stream.ReadBytes(count); + } + + case ExifDataType.SignedByte: + { + sbyte[] res = new sbyte[count]; + byte[] buf = this.stream.ReadBytes(count); + Array.Copy(buf, res, buf.Length); + return res; + } + + case ExifDataType.Short: + { + if (entryDataType == ExifDataType.Long) + { + uint[] buf = new uint[count]; + for (int i = 0; i < buf.Length; i++) + { + buf[i] = this.stream.ReadUInt16(); + } + + return buf; + } + else + { + ushort[] buf = new ushort[count]; + for (int i = 0; i < buf.Length; i++) + { + buf[i] = this.stream.ReadUInt16(); + } + + return buf; + } + } + + case ExifDataType.SignedShort: + { + short[] buf = new short[count]; + for (int i = 0; i < buf.Length; i++) + { + buf[i] = this.stream.ReadInt16(); + } + + return buf; + } + + case ExifDataType.Long: + { + uint[] buf = new uint[count]; + for (int i = 0; i < buf.Length; i++) + { + buf[i] = this.stream.ReadUInt32(); + } + + return buf; + } + + case ExifDataType.SignedLong: + { + int[] buf = new int[count]; + for (int i = 0; i < buf.Length; i++) + { + buf[i] = this.stream.ReadInt32(); + } + + return buf; + } + + case ExifDataType.Ascii: + { + byte[] buf = this.stream.ReadBytes(count); + + if (buf[buf.Length - 1] != 0) + { + throw new ImageFormatException("The retrieved string is not null terminated."); + } + + return Encoding.UTF8.GetString(buf, 0, buf.Length - 1); + } + + case ExifDataType.SingleFloat: + { + float[] buf = new float[count]; + for (int i = 0; i < buf.Length; i++) + { + buf[i] = this.stream.ReadSingle(); + } + + return buf; + } + + case ExifDataType.DoubleFloat: + { + double[] buf = new double[count]; + for (int i = 0; i < buf.Length; i++) + { + buf[i] = this.stream.ReadDouble(); + } + + return buf; + } + + case ExifDataType.Rational: + { + var buf = new Rational[count]; + for (int i = 0; i < buf.Length; i++) + { + uint numerator = this.stream.ReadUInt32(); + uint denominator = this.stream.ReadUInt32(); + buf[i] = new Rational(numerator, denominator); + } + + return buf; + } + + case ExifDataType.SignedRational: + { + var buf = new SignedRational[count]; + for (int i = 0; i < buf.Length; i++) + { + int numerator = this.stream.ReadInt32(); + int denominator = this.stream.ReadInt32(); + buf[i] = new SignedRational(numerator, denominator); + } + + return buf; + } + + case ExifDataType.Ifd: + { + return this.stream.ReadUInt32(); + } + + default: + return null; + } } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs new file mode 100644 index 000000000..ae25480ed --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections; +using System.Collections.Generic; + +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; + this.XmpProfile = other.XmpProfile; + } + + /// + /// Gets or sets the byte order. + /// + public TiffByteOrder ByteOrder { get; set; } + + /// + /// Gets or sets the XMP profile. + /// + public byte[] XmpProfile { get; set; } + + /// + public IDeepCloneable DeepClone() => new TiffMetadata(this); + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfdEntryCreator.cs b/src/ImageSharp/Formats/Tiff/__obsolete/TiffIfdEntryCreator.cs similarity index 100% rename from src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfdEntryCreator.cs rename to src/ImageSharp/Formats/Tiff/__obsolete/TiffIfdEntryCreator.cs diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadataNames.cs b/src/ImageSharp/Formats/Tiff/__obsolete/TiffMetadataNames.cs similarity index 100% rename from src/ImageSharp/Formats/Tiff/TiffMetadataNames.cs rename to src/ImageSharp/Formats/Tiff/__obsolete/TiffMetadataNames.cs diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffTags.cs b/src/ImageSharp/Formats/Tiff/__obsolete/TiffTagId.cs similarity index 100% rename from src/ImageSharp/Formats/Tiff/Constants/TiffTags.cs rename to src/ImageSharp/Formats/Tiff/__obsolete/TiffTagId.cs diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffType.cs b/src/ImageSharp/Formats/Tiff/__obsolete/TiffTagType.cs similarity index 100% rename from src/ImageSharp/Formats/Tiff/Constants/TiffType.cs rename to src/ImageSharp/Formats/Tiff/__obsolete/TiffTagType.cs diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index c3d9618c8..720d6c1b7 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -19,6 +19,12 @@ SixLabors.ImageSharp + + + + + + diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs index 13e67554c..733eb4a79 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs @@ -75,6 +75,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// /// 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/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index a240c1392..e7a01b070 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -260,9 +260,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } else { - this.WriteValue(value, destination, newOffset); + WriteValue(value, destination, newOffset); } newOffset += 4; @@ -362,7 +362,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } } - 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 e20867b43..fdde66c51 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// 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.Long.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs index 8aae08160..68156fbb3 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs @@ -11,6 +11,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag SubfileType { get; } = new ExifTag(ExifTagValue.SubfileType); + /// + /// Gets the RowsPerStrip exif tag. + /// + public static ExifTag RowsPerStrip { get; } = new ExifTag(ExifTagValue.RowsPerStrip); + /// /// Gets the SubIFDOffset exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs index ac4b0a1bf..5d0a106ab 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs @@ -56,6 +56,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag StripRowCounts { get; } = new ExifTag(ExifTagValue.StripRowCounts); + /// + /// Gets the StripByteCounts exif tag. + /// + /// + public static ExifTag StripByteCounts { get; } = new ExifTag(ExifTagValue.StripByteCounts); + /// /// Gets the IntergraphRegisters exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs index e07a32598..3d13a82dc 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs @@ -24,7 +24,16 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif PrimaryChromaticities = 0x013F, /// - /// ColorMap + /// A color map for palette color images. + /// See Section 8: Baseline Fields. /// ColorMap = 0x0140, @@ -390,6 +437,14 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif [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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif BatteryLevel = 0x828F, /// - /// Copyright + /// Copyright notice. + /// See Section 8: Baseline Fields. /// Copyright = 0x8298, @@ -668,38 +739,70 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// PixelScale = 0x830E, + /// + /// IPTC (International Press Telecommunications Council) metadata. + /// See IPTC 4.1 specification. + /// + IPTC = 0x83BB, + /// /// IntergraphPacketData /// @@ -737,6 +846,40 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// 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 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// 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/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index 2d8aa9260..e47d5da25 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -9,10 +9,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 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); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Tiff/ImageExtensionsTest.cs new file mode 100644 index 000000000..af82b4026 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/ImageExtensionsTest.cs @@ -0,0 +1,172 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Category", "Tiff.BlackBox.Encoder")] + [Trait("Category", "Tiff")] + public class ImageExtensionsTest + { + [Theory] + [WithFile(TestImages.Tiff.RgbUncompressed, PixelTypes.Rgba32)] + public void ThrowsSavingNotImplemented(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Assert.Throws(() => + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsTiff_Path.tiff"); + using var image = provider.GetImage(new TiffDecoder()); + image.SaveAsTiff(file); + }); + } + + [Fact(Skip = "Saving not implemented")] + public void SaveAsTiff_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsTiff_Path.tiff"); + + using (var image = new Image(10, 10)) + { + image.SaveAsTiff(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact(Skip = "Saving not implemented")] + public async Task SaveAsTiffAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsTiffAsync_Path.tiff"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsTiffAsync(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact(Skip = "Saving not implemented")] + public void SaveAsTiff_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsTiff_Path_Encoder.tiff"); + + using (var image = new Image(10, 10)) + { + image.SaveAsTiff(file, new TiffEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact(Skip = "Saving not implemented")] + public async Task SaveAsTiffAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsTiffAsync_Path_Encoder.tiff"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsTiffAsync(file, new TiffEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact(Skip = "Saving not implemented")] + public void SaveAsTiff_Stream() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsTiff(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact(Skip = "Saving not implemented")] + public async Task SaveAsTiffAsync_StreamAsync() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsTiffAsync(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact(Skip = "Saving not implemented")] + public void SaveAsTiff_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsTiff(memoryStream, new TiffEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + + [Fact(Skip = "Saving not implemented")] + public async Task SaveAsTiffAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsTiffAsync(memoryStream, new TiffEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/tiff", mime.DefaultMimeType); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs index 20fd53f41..ba7728b0f 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs @@ -19,52 +19,52 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff private static Rgba32 Bit0 = new Rgba32(0, 0, 0, 255); private static Rgba32 Bit1 = new Rgba32(255, 255, 255, 255); - private static byte[] Bilevel_Bytes4x4 = new byte[] { 0b01010000, + private static readonly byte[] Bilevel_Bytes4x4 = new byte[] { 0b01010000, 0b11110000, 0b01110000, 0b10010000 }; - private static Rgba32[][] Bilevel_Result4x4 = new[] { new[] { Bit0, Bit1, Bit0, Bit1 }, + private static readonly Rgba32[][] Bilevel_Result4x4 = new[] { new[] { Bit0, Bit1, Bit0, Bit1 }, new[] { Bit1, Bit1, Bit1, Bit1 }, new[] { Bit0, Bit1, Bit1, Bit1 }, new[] { Bit1, Bit0, Bit0, Bit1 }}; - private static byte[] Bilevel_Bytes12x4 = new byte[] { 0b01010101, 0b01010000, + private static readonly byte[] Bilevel_Bytes12x4 = new byte[] { 0b01010101, 0b01010000, 0b11111111, 0b11111111, 0b01101001, 0b10100000, 0b10010000, 0b01100000}; - private static Rgba32[][] Bilevel_Result12x4 = new[] { new[] { Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1 }, + private static readonly Rgba32[][] Bilevel_Result12x4 = new[] { 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 byte[] Grayscale4_Bytes4x4 = new byte[] { 0x8F, 0x0F, + private static readonly byte[] Grayscale4_Bytes4x4 = new byte[] { 0x8F, 0x0F, 0xFF, 0xFF, 0x08, 0x8F, 0xF0, 0xF8 }; - private static Rgba32[][] Grayscale4_Result4x4 = new[] { new[] { Gray8, GrayF, Gray0, GrayF }, + private static readonly Rgba32[][] Grayscale4_Result4x4 = new[] { new[] { Gray8, GrayF, Gray0, GrayF }, new[] { GrayF, GrayF, GrayF, GrayF }, new[] { Gray0, Gray8, Gray8, GrayF }, new[] { GrayF, Gray0, GrayF, Gray8 }}; - private static byte[] Grayscale4_Bytes3x4 = new byte[] { 0x8F, 0x00, + private static readonly byte[] Grayscale4_Bytes3x4 = new byte[] { 0x8F, 0x00, 0xFF, 0xF0, 0x08, 0x80, 0xF0, 0xF0 }; - private static Rgba32[][] Grayscale4_Result3x4 = new[] { new[] { Gray8, GrayF, Gray0 }, + private static readonly Rgba32[][] Grayscale4_Result3x4 = new[] { new[] { Gray8, GrayF, Gray0 }, new[] { GrayF, GrayF, GrayF }, new[] { Gray0, Gray8, Gray8 }, new[] { GrayF, Gray0, GrayF }}; - private static byte[] Grayscale8_Bytes4x4 = new byte[] { 128, 255, 000, 255, + private static readonly byte[] Grayscale8_Bytes4x4 = new byte[] { 128, 255, 000, 255, 255, 255, 255, 255, 000, 128, 128, 255, 255, 000, 255, 128 }; - private static Rgba32[][] Grayscale8_Result4x4 = new[] { new[] { Gray128, Gray255, Gray000, Gray255 }, + private static readonly Rgba32[][] Grayscale8_Result4x4 = new[] { new[] { Gray128, Gray255, Gray000, Gray255 }, new[] { Gray255, Gray255, Gray255, Gray255 }, new[] { Gray000, Gray128, Gray128, Gray255 }, new[] { Gray255, Gray000, Gray255, Gray128 }}; @@ -125,7 +125,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { AssertDecode(expectedResult, pixels => { - BlackIsZeroTiffColor.Decode(inputData, new[] { (uint)bitsPerSample }, pixels, left, top, width, height); + new BlackIsZeroTiffColor(new[] { (ushort)bitsPerSample }).Decode(inputData, pixels, left, top, width, height); }); } @@ -135,7 +135,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { AssertDecode(expectedResult, pixels => { - BlackIsZero1TiffColor.Decode(inputData, pixels, left, top, width, height); + new BlackIsZero1TiffColor().Decode(inputData, pixels, left, top, width, height); }); } @@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { AssertDecode(expectedResult, pixels => { - BlackIsZero4TiffColor.Decode(inputData, pixels, left, top, width, height); + new BlackIsZero4TiffColor().Decode(inputData, pixels, left, top, width, height); }); } @@ -155,7 +155,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { AssertDecode(expectedResult, pixels => { - BlackIsZero8TiffColor.Decode(inputData, pixels, left, top, width, height); + 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 index 4f6bef0cf..98c7e6498 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PaletteTiffColorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PaletteTiffColorTests.cs @@ -12,25 +12,25 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { public static uint[][] Palette4_ColorPalette { get => GeneratePalette(16); } - public static uint[] Palette4_ColorMap { get => GenerateColorMap(Palette4_ColorPalette); } + public static ushort[] Palette4_ColorMap { get => GenerateColorMap(Palette4_ColorPalette); } - private static byte[] Palette4_Bytes4x4 = new byte[] { 0x01, 0x23, + private static readonly byte[] Palette4_Bytes4x4 = new byte[] { 0x01, 0x23, 0x4A, 0xD2, 0x12, 0x34, 0xAB, 0xEF }; - private static Rgba32[][] Palette4_Result4x4 = GenerateResult(Palette4_ColorPalette, + private static readonly Rgba32[][] Palette4_Result4x4 = GenerateResult(Palette4_ColorPalette, new[] { new[] { 0x00, 0x01, 0x02, 0x03 }, new[] { 0x04, 0x0A, 0x0D, 0x02 }, new[] { 0x01, 0x02, 0x03, 0x04 }, new[] { 0x0A, 0x0B, 0x0E, 0x0F }}); - private static byte[] Palette4_Bytes3x4 = new byte[] { 0x01, 0x20, + private static readonly byte[] Palette4_Bytes3x4 = new byte[] { 0x01, 0x20, 0x4A, 0xD0, 0x12, 0x30, 0xAB, 0xE0 }; - private static Rgba32[][] Palette4_Result3x4 = GenerateResult(Palette4_ColorPalette, + private static readonly Rgba32[][] Palette4_Result3x4 = GenerateResult(Palette4_ColorPalette, new[] { new[] { 0x00, 0x01, 0x02 }, new[] { 0x04, 0x0A, 0x0D }, new[] { 0x01, 0x02, 0x03 }, @@ -57,14 +57,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public static uint[][] Palette8_ColorPalette { get => GeneratePalette(256); } - public static uint[] Palette8_ColorMap { get => GenerateColorMap(Palette8_ColorPalette); } + public static ushort[] Palette8_ColorMap { get => GenerateColorMap(Palette8_ColorPalette); } - private static byte[] Palette8_Bytes4x4 = new byte[] { 000, 001, 002, 003, + private static readonly byte[] Palette8_Bytes4x4 = new byte[] { 000, 001, 002, 003, 100, 110, 120, 130, 000, 255, 128, 255, 050, 100, 150, 200 }; - private static Rgba32[][] Palette8_Result4x4 = GenerateResult(Palette8_ColorPalette, + private static readonly Rgba32[][] Palette8_Result4x4 = GenerateResult(Palette8_ColorPalette, new[] { new[] { 000, 001, 002, 003 }, new[] { 100, 110, 120, 130 }, new[] { 000, 255, 128, 255 }, @@ -85,11 +85,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [MemberData(nameof(Palette4_Data))] [MemberData(nameof(Palette8_Data))] - public void Decode_WritesPixelData(byte[] inputData, int bitsPerSample, uint[] colorMap, int left, int top, int width, int height, Rgba32[][] expectedResult) + public void Decode_WritesPixelData(byte[] inputData, ushort bitsPerSample, ushort[] colorMap, int left, int top, int width, int height, Rgba32[][] expectedResult) { AssertDecode(expectedResult, pixels => { - PaletteTiffColor.Decode(inputData, new[] { (uint)bitsPerSample }, colorMap, pixels, left, top, width, height); + new PaletteTiffColor(new[] { (ushort)bitsPerSample }, colorMap).Decode(inputData, pixels, left, top, width, height); }); } @@ -105,16 +105,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff return palette; } - private static uint[] GenerateColorMap(uint[][] colorPalette) + private static ushort[] GenerateColorMap(uint[][] colorPalette) { int colorCount = colorPalette.Length; - uint[] colorMap = new uint[colorCount * 3]; + ushort[] colorMap = new ushort[colorCount * 3]; for (int i = 0; i < colorCount; i++) { - colorMap[colorCount * 0 + i] = colorPalette[i][0]; - colorMap[colorCount * 1 + i] = colorPalette[i][1]; - colorMap[colorCount * 2 + i] = colorPalette[i][2]; + 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; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs index da48086bb..3faedfa10 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs @@ -2,8 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Generic; -using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -48,18 +46,22 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { int resultWidth = expectedResult[0].Length; int resultHeight = expectedResult.Length; - Image image = new Image(resultWidth, resultHeight); - image.Mutate(x => x.BackgroundColor(DefaultColor)); - Buffer2D pixels = image.GetRootFramePixelBuffer(); - decodeAction(pixels); - - for (int y = 0; y < resultHeight; y++) + using (Image image = new Image(resultWidth, resultHeight)) { - for (int x = 0; x < resultWidth; x++) + image.Mutate(x => x.BackgroundColor(DefaultColor)); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + decodeAction(pixels); + + for (int y = 0; y < resultHeight; y++) { - Assert.True(expectedResult[y][x] == pixels[x, y], - $"Pixel ({x}, {y}) should be {expectedResult[y][x]} but was {pixels[x, 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 index cc025a452..c0e328c62 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs @@ -72,17 +72,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { get { - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 0, 0, 4, 4, Rgb4_Result4x4 }; - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 0, 0, 4, 4, Offset(Rgb4_Result4x4, 0, 0, 6, 6) }; - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 1, 0, 4, 4, Offset(Rgb4_Result4x4, 1, 0, 6, 6) }; - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 0, 1, 4, 4, Offset(Rgb4_Result4x4, 0, 1, 6, 6) }; - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 1, 1, 4, 4, Offset(Rgb4_Result4x4, 1, 1, 6, 6) }; - - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 0, 0, 3, 4, Rgb4_Result3x4 }; - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 0, 0, 3, 4, Offset(Rgb4_Result3x4, 0, 0, 6, 6) }; - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 1, 0, 3, 4, Offset(Rgb4_Result3x4, 1, 0, 6, 6) }; - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 0, 1, 3, 4, Offset(Rgb4_Result3x4, 0, 1, 6, 6) }; - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 1, 1, 3, 4, Offset(Rgb4_Result3x4, 1, 1, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Rgb4_Result4x4 }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Offset(Rgb4_Result4x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 1, 0, 4, 4, Offset(Rgb4_Result4x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 0, 1, 4, 4, Offset(Rgb4_Result4x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 1, 1, 4, 4, Offset(Rgb4_Result4x4, 1, 1, 6, 6) }; + + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Rgb4_Result3x4 }; + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Offset(Rgb4_Result3x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 1, 0, 3, 4, Offset(Rgb4_Result3x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 0, 1, 3, 4, Offset(Rgb4_Result3x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 1, 1, 3, 4, Offset(Rgb4_Result3x4, 1, 1, 6, 6) }; } } @@ -126,11 +126,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { get { - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 0, 0, 4, 4, Rgb8_Result4x4 }; - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 0, 0, 4, 4, Offset(Rgb8_Result4x4, 0, 0, 6, 6) }; - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 1, 0, 4, 4, Offset(Rgb8_Result4x4, 1, 0, 6, 6) }; - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 0, 1, 4, 4, Offset(Rgb8_Result4x4, 0, 1, 6, 6) }; - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 1, 1, 4, 4, Offset(Rgb8_Result4x4, 1, 1, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Rgb8_Result4x4 }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Offset(Rgb8_Result4x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 1, 0, 4, 4, Offset(Rgb8_Result4x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 0, 1, 4, 4, Offset(Rgb8_Result4x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 1, 1, 4, 4, Offset(Rgb8_Result4x4, 1, 1, 6, 6) }; } } @@ -174,11 +174,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { get { - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 0, 0, 4, 4, Rgb484_Result4x4 }; - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 0, 0, 4, 4, Offset(Rgb484_Result4x4, 0, 0, 6, 6) }; - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 1, 0, 4, 4, Offset(Rgb484_Result4x4, 1, 0, 6, 6) }; - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 0, 1, 4, 4, Offset(Rgb484_Result4x4, 0, 1, 6, 6) }; - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 1, 1, 4, 4, Offset(Rgb484_Result4x4, 1, 1, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Rgb484_Result4x4 }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Offset(Rgb484_Result4x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 1, 0, 4, 4, Offset(Rgb484_Result4x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 0, 1, 4, 4, Offset(Rgb484_Result4x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 1, 1, 4, 4, Offset(Rgb484_Result4x4, 1, 1, 6, 6) }; } } @@ -186,11 +186,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [MemberData(nameof(Rgb4_Data))] [MemberData(nameof(Rgb8_Data))] [MemberData(nameof(Rgb484_Data))] - public void Decode_WritesPixelData(byte[][] inputData, uint[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + public void Decode_WritesPixelData(byte[][] inputData, ushort[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) { AssertDecode(expectedResult, pixels => { - RgbPlanarTiffColor.Decode(inputData, bitsPerSample, pixels, left, top, width, height); + new RgbPlanarTiffColor(bitsPerSample).Decode(inputData, 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 index 5683e4752..f1ba32c5d 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs @@ -48,17 +48,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { get { - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 0, 0, 4, 4, Rgb4_Result4x4 }; - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 0, 0, 4, 4, Offset(Rgb4_Result4x4, 0, 0, 6, 6) }; - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 1, 0, 4, 4, Offset(Rgb4_Result4x4, 1, 0, 6, 6) }; - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 0, 1, 4, 4, Offset(Rgb4_Result4x4, 0, 1, 6, 6) }; - yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 1, 1, 4, 4, Offset(Rgb4_Result4x4, 1, 1, 6, 6) }; - - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 0, 0, 3, 4, Rgb4_Result3x4 }; - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 0, 0, 3, 4, Offset(Rgb4_Result3x4, 0, 0, 6, 6) }; - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 1, 0, 3, 4, Offset(Rgb4_Result3x4, 1, 0, 6, 6) }; - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 0, 1, 3, 4, Offset(Rgb4_Result3x4, 0, 1, 6, 6) }; - yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 1, 1, 3, 4, Offset(Rgb4_Result3x4, 1, 1, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Rgb4_Result4x4 }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Offset(Rgb4_Result4x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 1, 0, 4, 4, Offset(Rgb4_Result4x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 0, 1, 4, 4, Offset(Rgb4_Result4x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new ushort[] { 4, 4, 4 }, 1, 1, 4, 4, Offset(Rgb4_Result4x4, 1, 1, 6, 6) }; + + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Rgb4_Result3x4 }; + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Offset(Rgb4_Result3x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 1, 0, 3, 4, Offset(Rgb4_Result3x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 0, 1, 3, 4, Offset(Rgb4_Result3x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4_Bytes3x4, new ushort[] { 4, 4, 4 }, 1, 1, 3, 4, Offset(Rgb4_Result3x4, 1, 1, 6, 6) }; } } @@ -90,11 +90,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { get { - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 0, 0, 4, 4, Rgb8_Result4x4 }; - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 0, 0, 4, 4, Offset(Rgb8_Result4x4, 0, 0, 6, 6) }; - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 1, 0, 4, 4, Offset(Rgb8_Result4x4, 1, 0, 6, 6) }; - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 0, 1, 4, 4, Offset(Rgb8_Result4x4, 0, 1, 6, 6) }; - yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 1, 1, 4, 4, Offset(Rgb8_Result4x4, 1, 1, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Rgb8_Result4x4 }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Offset(Rgb8_Result4x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 1, 0, 4, 4, Offset(Rgb8_Result4x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 0, 1, 4, 4, Offset(Rgb8_Result4x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new ushort[] { 8, 8, 8 }, 1, 1, 4, 4, Offset(Rgb8_Result4x4, 1, 1, 6, 6) }; } } @@ -126,11 +126,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { get { - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 0, 0, 4, 4, Rgb484_Result4x4 }; - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 0, 0, 4, 4, Offset(Rgb484_Result4x4, 0, 0, 6, 6) }; - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 1, 0, 4, 4, Offset(Rgb484_Result4x4, 1, 0, 6, 6) }; - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 0, 1, 4, 4, Offset(Rgb484_Result4x4, 0, 1, 6, 6) }; - yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 1, 1, 4, 4, Offset(Rgb484_Result4x4, 1, 1, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Rgb484_Result4x4 }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Offset(Rgb484_Result4x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 1, 0, 4, 4, Offset(Rgb484_Result4x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 0, 1, 4, 4, Offset(Rgb484_Result4x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new ushort[] { 4, 8, 4 }, 1, 1, 4, 4, Offset(Rgb484_Result4x4, 1, 1, 6, 6) }; } } @@ -138,21 +138,21 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [MemberData(nameof(Rgb4_Data))] [MemberData(nameof(Rgb8_Data))] [MemberData(nameof(Rgb484_Data))] - public void Decode_WritesPixelData(byte[] inputData, uint[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + public void Decode_WritesPixelData(byte[] inputData, ushort[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) { AssertDecode(expectedResult, pixels => { - RgbTiffColor.Decode(inputData, bitsPerSample, pixels, left, top, width, height); + new RgbTiffColor(bitsPerSample).Decode(inputData, pixels, left, top, width, height); }); } [Theory] [MemberData(nameof(Rgb8_Data))] - public void Decode_WritesPixelData_8Bit(byte[] inputData, uint[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + public void Decode_WritesPixelData_8Bit(byte[] inputData, ushort[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) { AssertDecode(expectedResult, pixels => { - Rgb888TiffColor.Decode(inputData, pixels, left, top, width, height); + 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 index 5334e2984..faea296d0 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs @@ -19,52 +19,52 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff private static Rgba32 Bit0 = new Rgba32(255, 255, 255, 255); private static Rgba32 Bit1 = new Rgba32(0, 0, 0, 255); - private static byte[] Bilevel_Bytes4x4 = new byte[] { 0b01010000, + private static readonly byte[] Bilevel_Bytes4x4 = new byte[] { 0b01010000, 0b11110000, 0b01110000, 0b10010000 }; - private static Rgba32[][] Bilevel_Result4x4 = new[] { new[] { Bit0, Bit1, Bit0, Bit1 }, + private static readonly Rgba32[][] Bilevel_Result4x4 = new[] { new[] { Bit0, Bit1, Bit0, Bit1 }, new[] { Bit1, Bit1, Bit1, Bit1 }, new[] { Bit0, Bit1, Bit1, Bit1 }, new[] { Bit1, Bit0, Bit0, Bit1 }}; - private static byte[] Bilevel_Bytes12x4 = new byte[] { 0b01010101, 0b01010000, + private static readonly byte[] Bilevel_Bytes12x4 = new byte[] { 0b01010101, 0b01010000, 0b11111111, 0b11111111, 0b01101001, 0b10100000, 0b10010000, 0b01100000}; - private static Rgba32[][] Bilevel_Result12x4 = new[] { new[] { Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1 }, + private static readonly Rgba32[][] Bilevel_Result12x4 = new[] { 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 byte[] Grayscale4_Bytes4x4 = new byte[] { 0x8F, 0x0F, + private static readonly byte[] Grayscale4_Bytes4x4 = new byte[] { 0x8F, 0x0F, 0xFF, 0xFF, 0x08, 0x8F, 0xF0, 0xF8 }; - private static Rgba32[][] Grayscale4_Result4x4 = new[] { new[] { Gray8, GrayF, Gray0, GrayF }, + private static readonly Rgba32[][] Grayscale4_Result4x4 = new[] { new[] { Gray8, GrayF, Gray0, GrayF }, new[] { GrayF, GrayF, GrayF, GrayF }, new[] { Gray0, Gray8, Gray8, GrayF }, new[] { GrayF, Gray0, GrayF, Gray8 }}; - private static byte[] Grayscale4_Bytes3x4 = new byte[] { 0x8F, 0x00, + private static readonly byte[] Grayscale4_Bytes3x4 = new byte[] { 0x8F, 0x00, 0xFF, 0xF0, 0x08, 0x80, 0xF0, 0xF0 }; - private static Rgba32[][] Grayscale4_Result3x4 = new[] { new[] { Gray8, GrayF, Gray0 }, + private static readonly Rgba32[][] Grayscale4_Result3x4 = new[] { new[] { Gray8, GrayF, Gray0 }, new[] { GrayF, GrayF, GrayF }, new[] { Gray0, Gray8, Gray8 }, new[] { GrayF, Gray0, GrayF }}; - private static byte[] Grayscale8_Bytes4x4 = new byte[] { 128, 255, 000, 255, + private static readonly byte[] Grayscale8_Bytes4x4 = new byte[] { 128, 255, 000, 255, 255, 255, 255, 255, 000, 128, 128, 255, 255, 000, 255, 128 }; - private static Rgba32[][] Grayscale8_Result4x4 = new[] { new[] { Gray128, Gray255, Gray000, Gray255 }, + private static readonly Rgba32[][] Grayscale8_Result4x4 = new[] { new[] { Gray128, Gray255, Gray000, Gray255 }, new[] { Gray255, Gray255, Gray255, Gray255 }, new[] { Gray000, Gray128, Gray128, Gray255 }, new[] { Gray255, Gray000, Gray255, Gray128 }}; @@ -125,7 +125,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { AssertDecode(expectedResult, pixels => { - WhiteIsZeroTiffColor.Decode(inputData, new[] { (uint)bitsPerSample }, pixels, left, top, width, height); + new WhiteIsZeroTiffColor(new[] { (ushort)bitsPerSample }).Decode(inputData, pixels, left, top, width, height); }); } @@ -135,7 +135,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { AssertDecode(expectedResult, pixels => { - WhiteIsZero1TiffColor.Decode(inputData, pixels, left, top, width, height); + new WhiteIsZero1TiffColor().Decode(inputData, pixels, left, top, width, height); }); } @@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { AssertDecode(expectedResult, pixels => { - WhiteIsZero4TiffColor.Decode(inputData, pixels, left, top, width, height); + new WhiteIsZero4TiffColor().Decode(inputData, pixels, left, top, width, height); }); } @@ -155,7 +155,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { AssertDecode(expectedResult, pixels => { - WhiteIsZero8TiffColor.Decode(inputData, pixels, left, top, width, height); + 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 index b4d2a4894..58b917937 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -1,12 +1,9 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. // ReSharper disable InconsistentNaming - -using System.IO; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Png; +using System; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -15,13 +12,26 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Tiff { - [Trait("Category", "Tiff.BlackBox")] + [Trait("Category", "Tiff.BlackBox.Decoder")] + [Trait("Category", "Tiff")] public class TiffDecoderTests { - public static readonly string[] CommonTestImages = TestImages.Tiff.All; + public static readonly string[] SingleTestImages = TestImages.Tiff.All; + + public static readonly string[] MultiframeTestImages = TestImages.Tiff.Multiframes; + + public static readonly string[] NotSupportedImages = TestImages.Tiff.NotSupported; [Theory] - [WithFileCollection(nameof(CommonTestImages), PixelTypes.Rgba32)] + [WithFileCollection(nameof(NotSupportedImages), PixelTypes.Rgba32)] + public void ThrowsNotSupported(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Assert.Throws(() => provider.GetImage(new TiffDecoder())); + } + + [Theory] + [WithFileCollection(nameof(SingleTestImages), PixelTypes.Rgba32)] public void Decode(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -31,5 +41,22 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.CompareToOriginal(provider, ImageComparer.Exact, new MagickReferenceDecoder()); } } + + [Theory] + [WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)] + public void DecodeMultiframe(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new TiffDecoder())) + { + Assert.True(image.Frames.Count > 1); + + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, new MagickReferenceDecoder()); + + image.DebugSaveMultiFrame(provider); + image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, new MagickReferenceDecoder()); + } + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs new file mode 100644 index 000000000..6c210eb1e --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Category", "Tiff.BlackBox")] + [Trait("Category", "Tiff")] + public class TiffMetadataTests + { + public static readonly string[] MetadataImages = TestImages.Tiff.Metadata; + + [Theory] + [WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32, false)] + [WithFile(TestImages.Tiff.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(); + Assert.NotNull(meta); + if (ignoreMetadata) + { + Assert.Null(meta.XmpProfile); + } + else + { + Assert.NotNull(meta.XmpProfile); + } + } + } + + [Theory] + [WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)] + public void BaselineTags(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new TiffDecoder())) + { + TiffMetadata meta = image.Metadata.GetTiffMetadata(); + + Assert.NotNull(meta); + Assert.Equal(TiffByteOrder.LittleEndian, meta.ByteOrder); + Assert.Equal(PixelResolutionUnit.PixelsPerInch, image.Metadata.ResolutionUnits); + Assert.Equal(10, image.Metadata.HorizontalResolution); + Assert.Equal(10, image.Metadata.VerticalResolution); + + TiffFrameMetadata frame = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + Assert.Equal(32u, frame.Width); + Assert.Equal(32u, frame.Height); + Assert.Equal(new ushort[] { 8, 8, 8 }, frame.BitsPerSample); + Assert.Equal(TiffCompression.Lzw, frame.Compression); + Assert.Equal(TiffPhotometricInterpretation.Rgb, frame.PhotometricInterpretation); + Assert.Equal("This is Название", frame.ImageDescription); + Assert.Equal("This is Изготовитель камеры", frame.Make); + Assert.Equal("This is Модель камеры", frame.Model); + Assert.Equal(new uint[] { 8 }, frame.StripOffsets); + Assert.Equal(32u, frame.RowsPerStrip); + Assert.Equal(new uint[] { 750 }, frame.StripByteCounts); + Assert.Equal(10, frame.HorizontalResolution); + Assert.Equal(10, frame.VerticalResolution); + Assert.Equal(TiffPlanarConfiguration.Chunky, frame.PlanarConfiguration); + Assert.Equal(TiffResolutionUnit.Inch, frame.ResolutionUnit); + Assert.Equal("IrfanView", frame.Software); + Assert.Equal(null, frame.DateTime); + Assert.Equal("This is;Автор", frame.Artist); + Assert.Equal(null, frame.HostComputer); + Assert.Equal(null, frame.ColorMap); + Assert.Equal(null, frame.ExtraSamples); + Assert.Equal("This is Авторские права", frame.Copyright); + } + } + + [Theory] + [WithFile(TestImages.Tiff.MultiframeDeflateWithPreview, PixelTypes.Rgba32)] + public void SubfileType(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new TiffDecoder())) + { + TiffMetadata meta = image.Metadata.GetTiffMetadata(); + Assert.NotNull(meta); + + Assert.Equal(2, image.Frames.Count); + + TiffFrameMetadata frame0 = image.Frames[0].Metadata.GetTiffMetadata(); + Assert.Equal(TiffNewSubfileType.FullImage, frame0.NewSubfileType); + Assert.Equal(null, frame0.SubfileType); + Assert.Equal(255u, frame0.Width); + Assert.Equal(255u, frame0.Height); + + TiffFrameMetadata frame1 = image.Frames[1].Metadata.GetTiffMetadata(); + Assert.Equal(TiffNewSubfileType.Preview, frame1.NewSubfileType); + Assert.Equal(TiffSubfileType.Preview, frame1.SubfileType); + Assert.Equal(255u, frame1.Width); + Assert.Equal(255u, frame1.Height); + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/Tiff/ITiffGenDataSource.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/ITiffGenDataSource.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/Tiff/ITiffGenDataSource.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/ITiffGenDataSource.cs diff --git a/tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenDataBlock.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenDataBlock.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenDataBlock.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenDataBlock.cs diff --git a/tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenDataReference.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenDataReference.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenDataReference.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenDataReference.cs diff --git a/tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenEntry.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenEntry.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenEntry.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenEntry.cs diff --git a/tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenExtensions.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenExtensions.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenExtensions.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenExtensions.cs diff --git a/tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenHeader.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenHeader.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenHeader.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenHeader.cs diff --git a/tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenIfd.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenIfd.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenIfd.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenIfd.cs diff --git a/tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenIfdExtensions.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffGenIfdExtensions.cs diff --git a/tests/ImageSharp.Tests/TestUtilities/Tiff/TiffIfdParser.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffIfdParser.cs similarity index 100% rename from tests/ImageSharp.Tests/TestUtilities/Tiff/TiffIfdParser.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TestUtilities/Tiff/TiffIfdParser.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderHeaderTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderHeaderTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderHeaderTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderIfdEntryTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderIfdEntryTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderIfdEntryTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderIfdEntryTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderIfdTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderIfdTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderIfdTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderIfdTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderImageTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderImageTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderImageTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderImageTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderMetadataTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderMetadataTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffDecoderMetadataTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderIfdTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffEncoderIfdTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderIfdTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffEncoderIfdTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffEncoderMetadataTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMetadataTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffEncoderMetadataTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffIfd/TiffIfdEntryCreatorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffIfd/TiffIfdEntryCreatorTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffIfd/TiffIfdEntryCreatorTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffIfd/TiffIfdEntryCreatorTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffIfd/TiffIfdEntryTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffIfd/TiffIfdEntryTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffIfd/TiffIfdEntryTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffIfd/TiffIfdEntryTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffIfd/TiffIfdTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffIfd/TiffIfdTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffIfd/TiffIfdTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffIfd/TiffIfdTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffImageFormatDetectorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffImageFormatDetectorTests.cs similarity index 100% rename from tests/ImageSharp.Tests/Formats/Tiff/TiffImageFormatDetectorTests.cs rename to tests/ImageSharp.Tests/Formats/Tiff/__obsolete/TiffImageFormatDetectorTests.cs diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 0761b0978..a297d4143 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -12,6 +12,12 @@ true + + + + + + diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index de8278a33..f3af4dabd 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; @@ -9,6 +10,7 @@ using System.Threading.Tasks; using ImageMagick; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs @@ -54,30 +56,37 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel { - using var magickImage = new MagickImage(stream); - var result = new Image(configuration, magickImage.Width, magickImage.Height); - MemoryGroup resultPixels = result.GetRootFramePixelBuffer().FastMemoryGroup; - - using (IPixelCollection pixels = magickImage.GetPixelsUnsafe()) + using var magickImageCollection = new MagickImageCollection(stream); + var framesList = new List>(); + foreach (IMagickImage magicFrame in magickImageCollection) { - if (magickImage.Depth == 8) - { - byte[] data = pixels.ToByteArray(PixelMapping.RGBA); + var frame = new ImageFrame(configuration, magicFrame.Width, magicFrame.Height); + framesList.Add(frame); - FromRgba32Bytes(configuration, data, resultPixels); - } - else if (magickImage.Depth == 16) + MemoryGroup framePixels = frame.PixelBuffer.FastMemoryGroup; + using (IPixelCollection pixels = magicFrame.GetPixelsUnsafe()) { - ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); - Span bytes = MemoryMarshal.Cast(data.AsSpan()); - FromRgba64Bytes(configuration, bytes, resultPixels); - } - else - { - throw new InvalidOperationException(); + if (magicFrame.Depth == 8) + { + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); + + FromRgba32Bytes(configuration, data, framePixels); + } + else if (magicFrame.Depth == 16) + { + ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); + Span bytes = MemoryMarshal.Cast(data.AsSpan()); + FromRgba64Bytes(configuration, bytes, framePixels); + } + else + { + throw new InvalidOperationException(); + } } } + var result = new Image(configuration, new ImageMetadata(), framesList); + return result; } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 073db1efe..de9b8a02e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -537,6 +537,31 @@ namespace SixLabors.ImageSharp.Tests 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)