diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs index d183a33bd..986d585d3 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs @@ -34,17 +34,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// The number of bytes to read from the input stream. /// The height of the strip. /// The output buffer for uncompressed data. - public void Decompress(BufferedReadStream stream, uint stripOffset, uint stripByteCount, int stripHeight, Span buffer) + public void Decompress(BufferedReadStream stream, ulong stripOffset, ulong stripByteCount, int stripHeight, Span buffer) { - if (stripByteCount > int.MaxValue) - { - TiffThrowHelper.ThrowImageFormatException("The StripByteCount value is too big."); - } + DebugGuard.MustBeLessThanOrEqualTo(stripOffset, (ulong)long.MaxValue, nameof(stripOffset)); + DebugGuard.MustBeLessThanOrEqualTo(stripByteCount, (ulong)long.MaxValue, nameof(stripByteCount)); - stream.Seek(stripOffset, SeekOrigin.Begin); + stream.Seek((long)stripOffset, SeekOrigin.Begin); this.Decompress(stream, (int)stripByteCount, stripHeight, buffer); - if (stripOffset + stripByteCount < stream.Position) + if ((long)stripOffset + (long)stripByteCount < stream.Position) { TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip."); } diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs index b54545141..b5548c707 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs @@ -35,6 +35,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants /// public const ushort HeaderMagicNumber = 42; + /// + /// The big tiff header magic number + /// + public const ushort BigTiffHeaderMagicNumber = 43; + + /// + /// The big tiff bytesize of offsets value. + /// + public const ushort BigTiffBytesize = 8; + /// /// RowsPerStrip default value, which is effectively infinity. /// diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs index 26cb12d42..cce1d278c 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Tiff @@ -14,23 +15,27 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// internal class DirectoryReader { - private readonly Stream stream; + private const int DirectoryMax = 65534; - private uint nextIfdOffset; + private readonly Stream stream; - private const int DirectoryMax = 65534; + private readonly MemoryAllocator allocator; - // used for sequential read big values (actual for multiframe big files) - // todo: different tags can link to the same data (stream offset) - investigate - private readonly SortedList lazyLoaders = new(new DuplicateKeyComparer()); + private ulong nextIfdOffset; - public DirectoryReader(Stream stream) => this.stream = stream; + public DirectoryReader(Stream stream, MemoryAllocator allocator) + { + this.stream = stream; + this.allocator = allocator; + } /// /// Gets the byte order. /// public ByteOrder ByteOrder { get; private set; } + public bool IsBigTiff { get; private set; } + /// /// Reads image file directories. /// @@ -38,14 +43,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff public IEnumerable Read() { this.ByteOrder = ReadByteOrder(this.stream); - this.nextIfdOffset = new HeaderReader(this.stream, this.ByteOrder).ReadFileHeader(); - return this.ReadIfds(); + var headerReader = new HeaderReader(this.stream, this.ByteOrder); + headerReader.ReadFileHeader(); + + this.nextIfdOffset = headerReader.FirstIfdOffset; + this.IsBigTiff = headerReader.IsBigTiff; + + return this.ReadIfds(headerReader.IsBigTiff); } private static ByteOrder ReadByteOrder(Stream stream) { - var headerBytes = new byte[2]; - stream.Read(headerBytes, 0, 2); + Span headerBytes = stackalloc byte[2]; + stream.Read(headerBytes); if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) { return ByteOrder.LittleEndian; @@ -59,16 +69,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff throw TiffThrowHelper.ThrowInvalidHeader(); } - private IEnumerable ReadIfds() + private IEnumerable ReadIfds(bool isBigTiff) { var readers = new List(); - while (this.nextIfdOffset != 0 && this.nextIfdOffset < this.stream.Length) + while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length) { - var reader = new EntryReader(this.stream, this.ByteOrder, this.nextIfdOffset, this.lazyLoaders); - reader.ReadTags(); + var reader = new EntryReader(this.stream, this.ByteOrder, this.allocator); + reader.ReadTags(isBigTiff, this.nextIfdOffset); - this.nextIfdOffset = reader.NextIfdOffset; + if (reader.BigValues.Count > 0) + { + reader.BigValues.Sort((t1, t2) => t1.Offset.CompareTo(t2.Offset)); + + // this means that most likely all elements are placed before next IFD + if (reader.BigValues[0].Offset < reader.NextIfdOffset) + { + reader.ReadBigValues(); + } + } + this.nextIfdOffset = reader.NextIfdOffset; readers.Add(reader); if (readers.Count >= DirectoryMax) @@ -77,36 +97,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - // Sequential reading big values. - foreach (Action loader in this.lazyLoaders.Values) - { - loader(); - } - - var list = new List(); + var list = new List(readers.Count); foreach (EntryReader reader in readers) { + reader.ReadBigValues(); var profile = new ExifProfile(reader.Values, reader.InvalidTags); list.Add(profile); } return list; } - - /// - /// used for possibility add a duplicate offsets (but tags don't duplicate). - /// - /// The type of the key. - private class DuplicateKeyComparer : IComparer - where TKey : IComparable - { - public int Compare(TKey x, TKey y) - { - int result = x.CompareTo(y); - - // Handle equality as being greater. - return (result == 0) ? 1 : result; - } - } } } diff --git a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs index be4f59bff..922b7bf07 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs @@ -1,64 +1,78 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Tiff { internal class EntryReader : BaseExifReader { - private readonly uint startOffset; - - private readonly SortedList lazyLoaders; - - public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList lazyLoaders) - : base(stream) - { + public EntryReader(Stream stream, ByteOrder byteOrder, MemoryAllocator allocator) + : base(stream, allocator) => this.IsBigEndian = byteOrder == ByteOrder.BigEndian; - this.startOffset = ifdOffset; - this.lazyLoaders = lazyLoaders; - } public List Values { get; } = new(); - public uint NextIfdOffset { get; private set; } + public ulong NextIfdOffset { get; private set; } - public void ReadTags() + public void ReadTags(bool isBigTiff, ulong ifdOffset) { - this.ReadValues(this.Values, this.startOffset); - this.NextIfdOffset = this.ReadUInt32(); + if (!isBigTiff) + { + this.ReadValues(this.Values, (uint)ifdOffset); + this.NextIfdOffset = this.ReadUInt32(); + + this.ReadSubIfd(this.Values); + } + else + { + this.ReadValues64(this.Values, ifdOffset); + this.NextIfdOffset = this.ReadUInt64(); - this.ReadSubIfd(this.Values); + //// this.ReadSubIfd64(this.Values); + } } - protected override void RegisterExtLoader(uint offset, Action reader) => - this.lazyLoaders.Add(offset, reader); + public void ReadBigValues() => this.ReadBigValues(this.Values); } internal class HeaderReader : BaseExifReader { public HeaderReader(Stream stream, ByteOrder byteOrder) - : base(stream) => + : base(stream, null) => this.IsBigEndian = byteOrder == ByteOrder.BigEndian; - public uint FirstIfdOffset { get; private set; } + public bool IsBigTiff { get; private set; } - public uint ReadFileHeader() + public ulong FirstIfdOffset { get; private set; } + + public void ReadFileHeader() { ushort magic = this.ReadUInt16(); - if (magic != TiffConstants.HeaderMagicNumber) + if (magic == TiffConstants.HeaderMagicNumber) { - TiffThrowHelper.ThrowInvalidHeader(); + this.IsBigTiff = false; + this.FirstIfdOffset = this.ReadUInt32(); + return; } + else if (magic == TiffConstants.BigTiffHeaderMagicNumber) + { + this.IsBigTiff = true; - this.FirstIfdOffset = this.ReadUInt32(); - return this.FirstIfdOffset; - } + ushort bytesize = this.ReadUInt16(); + ushort reserve = this.ReadUInt16(); + if (bytesize == TiffConstants.BigTiffBytesize && reserve == 0) + { + this.FirstIfdOffset = this.ReadUInt64(); + return; + } + } - protected override void RegisterExtLoader(uint offset, Action reader) => throw new NotSupportedException(); + TiffThrowHelper.ThrowInvalidHeader(); + } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 55af87005..177a93d24 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Buffers; using System.Collections.Generic; using System.Linq; @@ -41,6 +42,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// private ByteOrder byteOrder; + /// + /// Indicating whether is BigTiff format. + /// + private bool isBigTiff; + /// /// Initializes a new instance of the class. /// @@ -141,10 +147,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff where TPixel : unmanaged, IPixel { this.inputStream = stream; - var reader = new DirectoryReader(stream); + var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator); IEnumerable directories = reader.Read(); this.byteOrder = reader.ByteOrder; + this.isBigTiff = reader.IsBigTiff; var frames = new List>(); foreach (ExifProfile ifd in directories) @@ -154,7 +161,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff frames.Add(frame); } - ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder); + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff); // TODO: Tiff frames can have different sizes ImageFrame root = frames[0]; @@ -174,13 +181,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.inputStream = stream; - var reader = new DirectoryReader(stream); + var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator); IEnumerable directories = reader.Read(); ExifProfile rootFrameExifProfile = directories.First(); var rootMetadata = TiffFrameMetadata.Parse(rootFrameExifProfile); - ImageMetadata metadata = TiffDecoderMetadataCreator.Create(reader.ByteOrder, rootFrameExifProfile); + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(reader.ByteOrder, reader.IsBigTiff, rootFrameExifProfile); int width = GetImageWidth(rootFrameExifProfile); int height = GetImageHeight(rootFrameExifProfile); @@ -211,21 +218,58 @@ namespace SixLabors.ImageSharp.Formats.Tiff var frame = new ImageFrame(this.Configuration, width, height, imageFrameMetaData); int rowsPerStrip = tags.GetValue(ExifTag.RowsPerStrip) != null ? (int)tags.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; - Number[] stripOffsets = tags.GetValue(ExifTag.StripOffsets)?.Value; - Number[] stripByteCounts = tags.GetValue(ExifTag.StripByteCounts)?.Value; + + var stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue(); + var stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue(); + + IMemoryOwner stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span stripOffsets); + IMemoryOwner stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span stripByteCounts); if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { - this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken); + this.DecodeStripsPlanar( + frame, + rowsPerStrip, + stripOffsets, + stripByteCounts, + cancellationToken); } else { - this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken); + this.DecodeStripsChunky( + frame, + rowsPerStrip, + stripOffsets, + stripByteCounts, + cancellationToken); } + stripOffsetsMemory?.Dispose(); + stripByteCountsMemory?.Dispose(); return frame; } + private IMemoryOwner ConvertNumbers(Array array, out Span span) + { + if (array is Number[] numbers) + { + IMemoryOwner memory = this.memoryAllocator.Allocate(numbers.Length); + span = memory.GetSpan(); + for (int i = 0; i < numbers.Length; i++) + { + span[i] = (uint)numbers[i]; + } + + return memory; + } + else + { + DebugGuard.IsTrue(array is ulong[], $"Expected {nameof(UInt64)} array."); + span = (ulong[])array; + return null; + } + } + /// /// Calculates the size (in bytes) for a pixel buffer using the determined color format. /// @@ -276,7 +320,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// An array of byte offsets to each strip in the image. /// An array of the size of each strip (in bytes). /// The token to monitor cancellation. - private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken) + private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int stripsPerPixel = this.BitsPerSample.Channels; @@ -329,10 +373,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff { decompressor.Decompress( this.inputStream, - (uint)stripOffsets[stripIndex], - (uint)stripByteCounts[stripIndex], + stripOffsets[stripIndex], + stripByteCounts[stripIndex], stripHeight, stripBuffers[planeIndex].GetSpan()); + stripIndex += stripsPerPlane; } @@ -357,7 +402,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The strip offsets. /// The strip byte counts. /// The token to monitor cancellation. - private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken) + private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. @@ -370,7 +415,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff int bitsPerPixel = this.BitsPerPixel; using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean); - System.Span stripBufferSpan = stripBuffer.GetSpan(); + Span stripBufferSpan = stripBuffer.GetSpan(); Buffer2D pixels = frame.PixelBuffer; using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( @@ -413,7 +458,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } - decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripHeight, stripBufferSpan); + decompressor.Decompress( + this.inputStream, + stripOffsets[stripIndex], + stripByteCounts[stripIndex], + stripHeight, + stripBufferSpan); colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight); } @@ -432,6 +482,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageWidth"); } + DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth)); + return (int)width.Value; } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index 6f8a81a82..4c4023ace 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// internal static class TiffDecoderMetadataCreator { - public static ImageMetadata Create(List> frames, bool ignoreMetadata, ByteOrder byteOrder) + public static ImageMetadata Create(List> frames, bool ignoreMetadata, ByteOrder byteOrder, bool isBigTiff) where TPixel : unmanaged, IPixel { if (frames.Count < 1) @@ -26,13 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowImageFormatException("Expected at least one frame."); } - var imageMetaData = new ImageMetadata(); - ExifProfile exifProfileRootFrame = frames[0].Metadata.ExifProfile; - - SetResolution(imageMetaData, exifProfileRootFrame); - - TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); - tiffMetadata.ByteOrder = byteOrder; + ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0].Metadata.ExifProfile); if (!ignoreMetadata) { @@ -56,13 +50,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff return imageMetaData; } - public static ImageMetadata Create(ByteOrder byteOrder, ExifProfile exifProfile) + public static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ExifProfile exifProfile) { var imageMetaData = new ImageMetadata(); SetResolution(imageMetaData, exifProfile); TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); tiffMetadata.ByteOrder = byteOrder; + tiffMetadata.FormatType = isBigTiff ? TiffFormatType.BigTIFF : TiffFormatType.Default; return imageMetaData; } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index a8420fabb..a187d444a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -24,12 +24,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The IFD entries container to read the image format information for current frame. public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata) { - if (exifProfile.GetValue(ExifTag.TileOffsets)?.Value != null) + if (exifProfile.GetValueInternal(ExifTag.TileOffsets) is not null || exifProfile.GetValueInternal(ExifTag.TileByteCounts) is not null) { TiffThrowHelper.ThrowNotSupported("Tiled images are not supported."); } - if (exifProfile.GetValue(ExifTag.ExtraSamples)?.Value != null) + if (exifProfile.GetValueInternal(ExifTag.ExtraSamples) is not null) { TiffThrowHelper.ThrowNotSupported("ExtraSamples is not supported."); } @@ -95,12 +95,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff private static void VerifyRequiredFieldsArePresent(ExifProfile exifProfile, TiffFrameMetadata frameMetadata) { - if (exifProfile.GetValue(ExifTag.StripOffsets) == null) + if (exifProfile.GetValueInternal(ExifTag.StripOffsets) is null) { TiffThrowHelper.ThrowImageFormatException("StripOffsets are missing and are required for decoding the TIFF image!"); } - if (exifProfile.GetValue(ExifTag.StripByteCounts) == null) + if (exifProfile.GetValueInternal(ExifTag.StripByteCounts) is null) { TiffThrowHelper.ThrowImageFormatException("StripByteCounts are missing and are required for decoding the TIFF image!"); } @@ -384,7 +384,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff private static void ParseCompression(this TiffDecoderCore options, TiffCompression? compression, ExifProfile exifProfile) { - switch (compression) + // Default 1 (No compression) https://www.awaresystems.be/imaging/tiff/tifftags/compression.html + switch (compression ?? TiffCompression.None) { case TiffCompression.None: { @@ -441,7 +442,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff default: { - TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported"); + TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported"); break; } } diff --git a/src/ImageSharp/Formats/Tiff/TiffFormatType.cs b/src/ImageSharp/Formats/Tiff/TiffFormatType.cs new file mode 100644 index 000000000..320386e4c --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffFormatType.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The TIFF format type enum. + /// + public enum TiffFormatType + { + /// + /// The TIFF file format type. + /// + Default, + + /// + /// The BigTIFF format type. + /// + BigTIFF + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs b/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs index fc2ad3e13..51280b4bf 100644 --- a/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff public sealed class TiffImageFormatDetector : IImageFormatDetector { /// - public int HeaderSize => 4; + public int HeaderSize => 8; /// public IImageFormat DetectFormat(ReadOnlySpan header) @@ -26,9 +26,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff private bool IsSupportedFileFormat(ReadOnlySpan header) { - return header.Length >= this.HeaderSize && - ((header[0] == 0x49 && header[1] == 0x49 && header[2] == 0x2A && header[3] == 0x00) || // Little-endian - (header[0] == 0x4D && header[1] == 0x4D && header[2] == 0x00 && header[3] == 0x2A)); // Big-endian + if (header.Length >= this.HeaderSize) + { + if (header[0] == 0x49 && header[1] == 0x49) + { + // Little-endian + if (header[2] == 0x2A && header[3] == 0x00) + { + // tiff + return true; + } + else if (header[2] == 0x2B && header[3] == 0x00 + && header[4] == 8 && header[5] == 0 && header[6] == 0 && header[7] == 0) + { + // big tiff + return true; + } + } + else if (header[0] == 0x4D && header[1] == 0x4D) + { + // Big-endian + if (header[2] == 0 && header[3] == 0x2A) + { + // tiff + return true; + } + else + if (header[2] == 0 && header[3] == 0x2B + && header[4] == 0 && header[5] == 8 && header[6] == 0 && header[7] == 0) + { + // big tiff + return true; + } + } + } + + return false; } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index cf1ab3754..f5124a78a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -26,6 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public ByteOrder ByteOrder { get; set; } + /// + /// Gets or sets the format type. + /// + public TiffFormatType FormatType { get; set; } + /// public IDeepCloneable DeepClone() => new TiffMetadata(this); } diff --git a/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs b/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs index 3c541a786..8223c445a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs +++ b/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// The error message for the exception. [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowImageFormatException(string errorMessage) => throw new ImageFormatException(errorMessage); + public static Exception ThrowImageFormatException(string errorMessage) => throw new ImageFormatException(errorMessage); [MethodImpl(InliningOptions.ColdPath)] public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs index 733eb4a79..0185afb50 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs @@ -80,6 +80,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// /// Reference to an IFD (32-bit (4-byte) unsigned integer). /// - Ifd = 13 + Ifd = 13, + + /// + /// A 64-bit (8-byte) unsigned integer. + /// + Long8 = 16, + + /// + /// A 64-bit (8-byte) signed integer (2's complement notation). + /// + SignedLong8 = 17, + + /// + /// Reference to an IFD (64-bit (8-byte) unsigned integer). + /// + Ifd8 = 18, } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs index 4f75999bb..729efb390 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs @@ -32,10 +32,14 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifDataType.Long: case ExifDataType.SignedLong: case ExifDataType.SingleFloat: + case ExifDataType.Ifd: return 4; case ExifDataType.DoubleFloat: case ExifDataType.Rational: case ExifDataType.SignedRational: + case ExifDataType.Long8: + case ExifDataType.SignedLong8: + case ExifDataType.Ifd8: return 8; default: throw new NotSupportedException(dataType.ToString()); diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 6e671b3ec..2fcd1cc07 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -9,15 +10,19 @@ using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { internal class ExifReader : BaseExifReader { - private readonly List loaders = new List(); - public ExifReader(byte[] exifData) - : base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData)))) + : this(exifData, null) + { + } + + public ExifReader(byte[] exifData, MemoryAllocator allocator) + : base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData))), allocator) { } @@ -47,16 +52,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif this.ReadSubIfd(values); - foreach (Action loader in this.loaders) - { - loader(); - } + this.ReadBigValues(values); return values; } - protected override void RegisterExtLoader(uint offset, Action loader) => this.loaders.Add(loader); - private void GetThumbnail(uint offset) { if (offset == 0) @@ -86,19 +86,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// internal abstract class BaseExifReader { - private readonly byte[] offsetBuffer = new byte[4]; + private readonly byte[] buf8 = new byte[8]; private readonly byte[] buf4 = new byte[4]; private readonly byte[] buf2 = new byte[2]; + private readonly MemoryAllocator allocator; private readonly Stream data; private List invalidTags; - private uint exifOffset; - - private uint gpsOffset; + private List subIfds; - protected BaseExifReader(Stream stream) => + protected BaseExifReader(Stream stream, MemoryAllocator allocator) + { this.data = stream ?? throw new ArgumentNullException(nameof(stream)); + this.allocator = allocator; + } private delegate TDataType ConverterMethod(ReadOnlySpan data); @@ -119,7 +121,51 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif public bool IsBigEndian { get; protected set; } - protected abstract void RegisterExtLoader(uint offset, Action loader); + public List<(ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif)> BigValues { get; } = new(); + + protected void ReadBigValues(List values) + { + if (this.BigValues.Count == 0) + { + return; + } + + int maxSize = 0; + foreach ((ulong offset, ExifDataType dataType, ulong numberOfComponents, ExifValue exif) in this.BigValues) + { + ulong size = numberOfComponents * ExifDataTypes.GetSize(dataType); + DebugGuard.MustBeLessThanOrEqualTo(size, int.MaxValue, nameof(size)); + + if ((int)size > maxSize) + { + maxSize = (int)size; + } + } + + if (this.allocator != null) + { + // tiff, bigTiff + using IMemoryOwner memory = this.allocator.Allocate(maxSize); + Span buf = memory.GetSpan(); + foreach ((ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag in this.BigValues) + { + ulong size = tag.NumberOfComponents * ExifDataTypes.GetSize(tag.DataType); + this.ReadBigValue(values, tag, buf.Slice(0, (int)size)); + } + } + else + { + // embedded exif + Span buf = maxSize <= 256 ? stackalloc byte[256] : new byte[maxSize]; + foreach ((ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag in this.BigValues) + { + ulong size = tag.NumberOfComponents * ExifDataTypes.GetSize(tag.DataType); + this.ReadBigValue(values, tag, buf.Slice(0, (int)size)); + } + } + + this.BigValues.Clear(); + } /// /// Reads the values to the values collection. @@ -136,22 +182,45 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif this.Seek(offset); int count = this.ReadUInt16(); + Span offsetBuffer = stackalloc byte[4]; for (int i = 0; i < count; i++) { - this.ReadValue(values); + this.ReadValue(values, offsetBuffer); } } protected void ReadSubIfd(List values) { - if (this.exifOffset != 0) + if (this.subIfds is not null) { - this.ReadValues(values, this.exifOffset); + foreach (ulong subIfdOffset in this.subIfds) + { + this.ReadValues(values, (uint)subIfdOffset); + } } + } + + protected void ReadValues64(List values, ulong offset) + { + DebugGuard.MustBeLessThanOrEqualTo(offset, (ulong)this.data.Length, "By spec UInt64.MaxValue is supported, but .Net Stream.Length can Int64.MaxValue."); - if (this.gpsOffset != 0) + this.Seek(offset); + ulong count = this.ReadUInt64(); + + Span offsetBuffer = stackalloc byte[8]; + for (ulong i = 0; i < count; i++) + { + this.ReadValue64(values, offsetBuffer); + } + } + + protected void ReadBigValue(IList values, (ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag, Span buffer) + { + this.Seek(tag.Offset); + if (this.TryReadSpan(buffer)) { - this.ReadValues(values, this.gpsOffset); + object value = this.ConvertValue(tag.DataType, buffer, tag.NumberOfComponents > 1 || tag.Exif.IsArray); + this.Add(values, tag.Exif, value); } } @@ -186,7 +255,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return Encoding.UTF8.GetString(buffer); } - private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, uint numberOfComponents) + private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, bool isArray) { if (buffer.Length == 0) { @@ -200,88 +269,104 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifDataType.Ascii: return this.ConvertToString(buffer); case ExifDataType.Byte: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToByte(buffer); } return buffer.ToArray(); case ExifDataType.DoubleFloat: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToDouble(buffer); } return ToArray(dataType, buffer, this.ConvertToDouble); case ExifDataType.Long: - if (numberOfComponents == 1) + case ExifDataType.Ifd: + if (!isArray) { return this.ConvertToUInt32(buffer); } return ToArray(dataType, buffer, this.ConvertToUInt32); case ExifDataType.Rational: - if (numberOfComponents == 1) + if (!isArray) { return this.ToRational(buffer); } return ToArray(dataType, buffer, this.ToRational); case ExifDataType.Short: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToShort(buffer); } return ToArray(dataType, buffer, this.ConvertToShort); case ExifDataType.SignedByte: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToSignedByte(buffer); } return ToArray(dataType, buffer, this.ConvertToSignedByte); case ExifDataType.SignedLong: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToInt32(buffer); } return ToArray(dataType, buffer, this.ConvertToInt32); case ExifDataType.SignedRational: - if (numberOfComponents == 1) + if (!isArray) { return this.ToSignedRational(buffer); } return ToArray(dataType, buffer, this.ToSignedRational); case ExifDataType.SignedShort: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToSignedShort(buffer); } return ToArray(dataType, buffer, this.ConvertToSignedShort); case ExifDataType.SingleFloat: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToSingle(buffer); } return ToArray(dataType, buffer, this.ConvertToSingle); + case ExifDataType.Long8: + case ExifDataType.Ifd8: + if (!isArray) + { + return this.ConvertToUInt64(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToUInt64); + case ExifDataType.SignedLong8: + if (!isArray) + { + return this.ConvertToInt64(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToUInt64); case ExifDataType.Undefined: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToByte(buffer); } return buffer.ToArray(); default: - throw new NotSupportedException(); + throw new NotSupportedException($"Data type {dataType} is not supported."); } } - private void ReadValue(List values) + private void ReadValue(List values, Span offsetBuffer) { // 2 | 2 | 4 | 4 // tag | type | count | value offset @@ -295,7 +380,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif uint numberOfComponents = this.ReadUInt32(); - this.TryReadSpan(this.offsetBuffer); + this.TryReadSpan(offsetBuffer); // Ensure that the data type is valid if (dataType == ExifDataType.Unknown) @@ -305,9 +390,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif // Issue #132: ExifDataType == Undefined is treated like a byte array. // If numberOfComponents == 0 this value can only be handled as an inline value and must fallback to 4 (bytes) - if (dataType == ExifDataType.Undefined && numberOfComponents == 0) + if (numberOfComponents == 0) { - numberOfComponents = 4; + numberOfComponents = 4 / ExifDataTypes.GetSize(dataType); } ExifValue exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); @@ -321,7 +406,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif uint size = numberOfComponents * ExifDataTypes.GetSize(dataType); if (size > 4) { - uint newOffset = this.ConvertToUInt32(this.offsetBuffer); + uint newOffset = this.ConvertToUInt32(offsetBuffer); // Ensure that the new index does not overrun the data. if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length) @@ -330,21 +415,85 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return; } - this.RegisterExtLoader(newOffset, () => - { - byte[] dataBuffer = new byte[size]; - this.Seek(newOffset); - if (this.TryReadSpan(dataBuffer)) - { - object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents); - this.Add(values, exifValue, value); - } - }); + this.BigValues.Add((newOffset, dataType, numberOfComponents, exifValue)); } else { - object value = this.ConvertValue(dataType, this.offsetBuffer, numberOfComponents); + object value = this.ConvertValue(dataType, offsetBuffer.Slice(0, (int)size), numberOfComponents > 1 || exifValue.IsArray); + this.Add(values, exifValue, value); + } + } + private void ReadValue64(List values, Span offsetBuffer) + { + if ((this.data.Length - this.data.Position) < 20) + { + return; + } + + var tag = (ExifTagValue)this.ReadUInt16(); + ExifDataType dataType = EnumUtils.Parse(this.ReadUInt16(), ExifDataType.Unknown); + + ulong numberOfComponents = this.ReadUInt64(); + + this.TryReadSpan(offsetBuffer); + + if (dataType == ExifDataType.Unknown) + { + return; + } + + if (numberOfComponents == 0) + { + numberOfComponents = 8 / ExifDataTypes.GetSize(dataType); + } + + // The StripOffsets, StripByteCounts, TileOffsets, and TileByteCounts tags are allowed to have the datatype TIFF_LONG8 in BigTIFF. + // Old datatypes TIFF_LONG, and TIFF_SHORT where allowed in the TIFF 6.0 specification, are still valid in BigTIFF, too. + // Likewise, tags that point to other IFDs, like e.g. the SubIFDs tag, are now allowed to have the datatype TIFF_IFD8 in BigTIFF. + // Again, the old datatypes TIFF_IFD, and the hardly recommendable TIFF_LONG, are still valid, too. + // https://www.awaresystems.be/imaging/tiff/bigtiff.html + ExifValue exifValue = null; + switch (tag) + { + case ExifTagValue.StripOffsets: + exifValue = new ExifLong8Array(ExifTagValue.StripOffsets); + break; + case ExifTagValue.StripByteCounts: + exifValue = new ExifLong8Array(ExifTagValue.StripByteCounts); + break; + case ExifTagValue.TileOffsets: + exifValue = new ExifLong8Array(ExifTagValue.TileOffsets); + break; + case ExifTagValue.TileByteCounts: + exifValue = new ExifLong8Array(ExifTagValue.TileByteCounts); + break; + default: + exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); + break; + } + + if (exifValue is null) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + ulong size = numberOfComponents * ExifDataTypes.GetSize(dataType); + if (size > 8) + { + ulong newOffset = this.ConvertToUInt64(offsetBuffer); + if (newOffset > ulong.MaxValue || newOffset > ((ulong)this.data.Length - size)) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + this.BigValues.Add((newOffset, dataType, numberOfComponents, exifValue)); + } + else + { + object value = this.ConvertValue(dataType, offsetBuffer.Slice(0, (int)size), numberOfComponents > 1 || exifValue.IsArray); this.Add(values, exifValue, value); } } @@ -368,11 +517,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif if (exif.Tag == ExifTag.SubIFDOffset) { - this.exifOffset = (uint)value; + this.AddSubIfd(value); } else if (exif.Tag == ExifTag.GPSIFDOffset) { - this.gpsOffset = (uint)value; + this.AddSubIfd(value); } else { @@ -383,8 +532,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif private void AddInvalidTag(ExifTag tag) => (this.invalidTags ??= new List()).Add(tag); - private void Seek(long pos) - => this.data.Seek(pos, SeekOrigin.Begin); + private void AddSubIfd(object val) + => (this.subIfds ??= new List()).Add(Convert.ToUInt64(val)); + + private void Seek(ulong pos) + => this.data.Seek((long)pos, SeekOrigin.Begin); private bool TryReadSpan(Span span) { @@ -398,6 +550,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return read == length; } + protected ulong ReadUInt64() => + this.TryReadSpan(this.buf8) + ? this.ConvertToUInt64(this.buf8) + : default; + // Known as Long in Exif Specification. protected uint ReadUInt32() => this.TryReadSpan(this.buf4) @@ -408,6 +565,30 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif ? this.ConvertToShort(this.buf2) : default; + private long ConvertToInt64(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadInt64BigEndian(buffer) + : BinaryPrimitives.ReadInt64LittleEndian(buffer); + } + + private ulong ConvertToUInt64(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadUInt64BigEndian(buffer) + : BinaryPrimitives.ReadUInt64LittleEndian(buffer); + } + private double ConvertToDouble(ReadOnlySpan buffer) { if (buffer.Length < 8) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index e7a01b070..498eec952 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -150,6 +150,20 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return offset + 4; } + private static int WriteInt64(long value, Span destination, int offset) + { + BinaryPrimitives.WriteInt64LittleEndian(destination.Slice(offset, 8), value); + + return offset + 8; + } + + private static int WriteUInt64(ulong value, Span destination, int offset) + { + BinaryPrimitives.WriteUInt64LittleEndian(destination.Slice(offset, 8), value); + + return offset + 8; + } + private static int WriteInt32(int value, Span destination, int offset) { BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, 4), value); @@ -390,6 +404,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } return WriteUInt32((uint)value, destination, offset); + case ExifDataType.Long8: + return WriteUInt64((ulong)value, destination, offset); + case ExifDataType.SignedLong8: + return WriteInt64((long)value, destination, offset); case ExifDataType.Rational: WriteRational(destination.Slice(offset, 8), (Rational)value); return offset + 8; diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs index ac4b0a1bf..390599b73 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs @@ -65,5 +65,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// Gets the TimeZoneOffset exif tag. /// public static ExifTag TimeZoneOffset { get; } = new ExifTag(ExifTagValue.TimeZoneOffset); + + /// + /// Gets the offset to child IFDs exif tag. + /// + public static ExifTag SubIFDs { get; } = new ExifTag(ExifTagValue.SubIFDs); } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs new file mode 100644 index 000000000..a4a1dd3df --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifLong8 : ExifValue + { + public ExifLong8(ExifTag tag) + : base(tag) + { + } + + public ExifLong8(ExifTagValue tag) + : base(tag) + { + } + + private ExifLong8(ExifLong8 value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Long8; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + if (intValue >= uint.MinValue) + { + this.Value = (uint)intValue; + return true; + } + + return false; + case uint uintValue: + this.Value = uintValue; + + return true; + case long intValue: + if (intValue >= 0) + { + this.Value = (ulong)intValue; + return true; + } + + return false; + default: + + return false; + } + } + + public override IExifValue DeepClone() => new ExifLong8(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs new file mode 100644 index 000000000..eced4e3de --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs @@ -0,0 +1,171 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifLong8Array : ExifArrayValue + { + public ExifLong8Array(ExifTagValue tag) + : base(tag) + { + } + + private ExifLong8Array(ExifLong8Array value) + : base(value) + { + } + + public override ExifDataType DataType + { + get + { + if (this.Value is not null) + { + foreach (ulong value in this.Value) + { + if (value > uint.MaxValue) + { + return ExifDataType.Long8; + } + } + } + + return ExifDataType.Long; + } + } + + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int val: + return this.SetSingle((ulong)Numerics.Clamp(val, 0, int.MaxValue)); + + case uint val: + return this.SetSingle((ulong)val); + + case short val: + return this.SetSingle((ulong)Numerics.Clamp(val, 0, short.MaxValue)); + + case ushort val: + return this.SetSingle((ulong)val); + + case long val: + return this.SetSingle((ulong)Numerics.Clamp(val, 0, long.MaxValue)); + + case long[] array: + { + if (value.GetType() == typeof(ulong[])) + { + return this.SetArray((ulong[])value); + } + + return this.SetArray(array); + } + + case int[] array: + { + if (value.GetType() == typeof(uint[])) + { + return this.SetArray((uint[])value); + } + + return this.SetArray(array); + } + + case short[] array: + { + if (value.GetType() == typeof(ushort[])) + { + return this.SetArray((ushort[])value); + } + + return this.SetArray(array); + } + } + + return false; + } + + public override IExifValue DeepClone() => new ExifLong8Array(this); + + private bool SetSingle(ulong value) + { + this.Value = new[] { value }; + return true; + } + + private bool SetArray(long[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)(values[i] < 0 ? 0 : values[i]); + } + + this.Value = numbers; + return true; + } + + private bool SetArray(ulong[] values) + { + this.Value = values; + return true; + } + + private bool SetArray(int[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)Numerics.Clamp(values[i], 0, int.MaxValue); + } + + this.Value = numbers; + return true; + } + + private bool SetArray(uint[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(short[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)Numerics.Clamp(values[i], 0, short.MaxValue); + } + + this.Value = numbers; + return true; + } + + private bool SetArray(ushort[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)values[i]; + } + + this.Value = numbers; + return true; + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs index 2d3a93aed..bf9c2cf9a 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs @@ -19,16 +19,14 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { get { - if (this.Value is null) + if (this.Value is not null) { - return ExifDataType.Short; - } - - for (int i = 0; i < this.Value.Length; i++) - { - if (this.Value[i] > ushort.MaxValue) + foreach (Number value in this.Value) { - return ExifDataType.Long; + if (value > ushort.MaxValue) + { + return ExifDataType.Long; + } } } @@ -54,13 +52,25 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ushort val: return this.SetSingle(val); case int[] array: + { + // workaround for inconsistent covariance of value-typed arrays + if (value.GetType() == typeof(uint[])) + { + return this.SetArray((uint[])value); + } + return this.SetArray(array); - case uint[] array: - return this.SetArray(array); + } + case short[] array: + { + if (value.GetType() == typeof(ushort[])) + { + return this.SetArray((ushort[])value); + } + return this.SetArray(array); - case ushort[] array: - return this.SetArray(array); + } } return false; diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs new file mode 100644 index 000000000..8362dcf2c --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifSignedLong8 : ExifValue + { + public ExifSignedLong8(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLong8(ExifSignedLong8 value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong8; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override IExifValue DeepClone() => new ExifSignedLong8(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs new file mode 100644 index 000000000..34ce20c8f --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifSignedLong8Array : ExifArrayValue + { + public ExifSignedLong8Array(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLong8Array(ExifSignedLong8Array value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong8; + + public override IExifValue DeepClone() => new ExifSignedLong8Array(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index af1eee2dc..33fb90cc0 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -9,7 +9,7 @@ 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) => Create(tag, dataType, numberOfComponents != 1); + public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, ulong numberOfComponents) => Create(tag, dataType, numberOfComponents != 1); public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, bool isArray) { @@ -19,10 +19,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifDataType.DoubleFloat: return isArray ? (ExifValue)new ExifDoubleArray(tag) : new ExifDouble(tag); case ExifDataType.SingleFloat: return isArray ? (ExifValue)new ExifFloatArray(tag) : new ExifFloat(tag); case ExifDataType.Long: return isArray ? (ExifValue)new ExifLongArray(tag) : new ExifLong(tag); + case ExifDataType.Long8: return isArray ? (ExifValue)new ExifLong8Array(tag) : new ExifLong8(tag); case ExifDataType.Rational: return isArray ? (ExifValue)new ExifRationalArray(tag) : new ExifRational(tag); case ExifDataType.Short: return isArray ? (ExifValue)new ExifShortArray(tag) : new ExifShort(tag); case ExifDataType.SignedByte: return isArray ? (ExifValue)new ExifSignedByteArray(tag) : new ExifSignedByte(tag); case ExifDataType.SignedLong: return isArray ? (ExifValue)new ExifSignedLongArray(tag) : new ExifSignedLong(tag); + case ExifDataType.SignedLong8: return isArray ? (ExifValue)new ExifSignedLong8Array(tag) : new ExifSignedLong8(tag); case ExifDataType.SignedRational: return isArray ? (ExifValue)new ExifSignedRationalArray(tag) : new ExifSignedRational(tag); case ExifDataType.SignedShort: return isArray ? (ExifValue)new ExifSignedShortArray(tag) : new ExifSignedShort(tag); case ExifDataType.Ascii: return new ExifString(tag); @@ -90,6 +92,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifTagValue.StripRowCounts: return new ExifLongArray(ExifTag.StripRowCounts); case ExifTagValue.IntergraphRegisters: return new ExifLongArray(ExifTag.IntergraphRegisters); case ExifTagValue.TimeZoneOffset: return new ExifLongArray(ExifTag.TimeZoneOffset); + case ExifTagValue.SubIFDs: return new ExifLongArray(ExifTag.SubIFDs); case ExifTagValue.ImageWidth: return new ExifNumber(ExifTag.ImageWidth); case ExifTagValue.ImageLength: return new ExifNumber(ExifTag.ImageLength); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs new file mode 100644 index 000000000..7a02c91b8 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +using System; +using System.IO; +using System.Linq; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; +using static SixLabors.ImageSharp.Tests.TestImages.BigTiff; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Collection("RunSerial")] + [Trait("Format", "Tiff")] + public class BigTiffDecoderTests : TiffDecoderBaseTester + { + [Theory] + [WithFile(BigTIFF, PixelTypes.Rgba32)] + [WithFile(BigTIFFLong, PixelTypes.Rgba32)] + [WithFile(BigTIFFLong8, PixelTypes.Rgba32)] + [WithFile(BigTIFFMotorola, PixelTypes.Rgba32)] + [WithFile(BigTIFFMotorolaLongStrips, PixelTypes.Rgba32)] + [WithFile(BigTIFFSubIFD4, PixelTypes.Rgba32)] + [WithFile(BigTIFFSubIFD8, PixelTypes.Rgba32)] + [WithFile(Indexed4_Deflate, PixelTypes.Rgba32)] + [WithFile(Indexed8_LZW, PixelTypes.Rgba32)] + [WithFile(MinIsBlack, PixelTypes.Rgba32)] + [WithFile(MinIsWhite, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(BigTIFFLong8Tiles, PixelTypes.Rgba32)] + public void ThrowsNotSupported(TestImageProvider provider) + where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder)); + + [Theory] + [WithFile(Damaged_MinIsWhite_RLE, PixelTypes.Rgba32)] + [WithFile(Damaged_MinIsBlack_RLE, PixelTypes.Rgba32)] + public void DamagedFiles(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Assert.Throws(() => TestTiffDecoder(provider)); + + using Image image = provider.GetImage(TiffDecoder); + ExifProfile exif = image.Frames.RootFrame.Metadata.ExifProfile; + + // PhotometricInterpretation is required tag: https://www.awaresystems.be/imaging/tiff/tifftags/photometricinterpretation.html + Assert.Null(exif.GetValueInternal(ExifTag.PhotometricInterpretation)); + } + + [Theory] + [InlineData(BigTIFF, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFLong, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFLong8, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFMotorola, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFMotorolaLongStrips, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFSubIFD4, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFSubIFD8, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(Indexed4_Deflate, 4, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(Indexed8_LZW, 8, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(MinIsWhite, 1, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(MinIsBlack, 1, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)] + public void Identify(string imagePath, int expectedPixelSize, int expectedWidth, int expectedHeight, double expectedHResolution, double expectedVResolution, PixelResolutionUnit expectedResolutionUnit) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo info = Image.Identify(stream); + + Assert.Equal(expectedPixelSize, info.PixelType?.BitsPerPixel); + Assert.Equal(expectedWidth, info.Width); + Assert.Equal(expectedHeight, info.Height); + Assert.NotNull(info.Metadata); + Assert.Equal(expectedHResolution, info.Metadata.HorizontalResolution); + Assert.Equal(expectedVResolution, info.Metadata.VerticalResolution); + Assert.Equal(expectedResolutionUnit, info.Metadata.ResolutionUnits); + + TiffMetadata tiffmeta = info.Metadata.GetTiffMetadata(); + Assert.NotNull(tiffmeta); + Assert.Equal(TiffFormatType.BigTIFF, tiffmeta.FormatType); + } + } + + [Theory] + [InlineData(BigTIFFLong, ImageSharp.ByteOrder.LittleEndian)] + [InlineData(BigTIFFMotorola, ImageSharp.ByteOrder.BigEndian)] + public void ByteOrder(string imagePath, ByteOrder expectedByteOrder) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo info = Image.Identify(stream); + + Assert.NotNull(info.Metadata); + Assert.Equal(expectedByteOrder, info.Metadata.GetTiffMetadata().ByteOrder); + + stream.Seek(0, SeekOrigin.Begin); + + using var img = Image.Load(stream); + Assert.Equal(expectedByteOrder, img.Metadata.GetTiffMetadata().ByteOrder); + } + } + + [Theory] + [WithFile(BigTIFFSubIFD8, PixelTypes.Rgba32)] + public void TiffDecoder_SubIfd8(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder); + + ExifProfile meta = image.Frames.RootFrame.Metadata.ExifProfile; + + Assert.Equal(0, meta.InvalidTags.Count); + Assert.Equal(6, meta.Values.Count); + Assert.Equal(64, (int)meta.GetValue(ExifTag.ImageWidth).Value); + Assert.Equal(64, (int)meta.GetValue(ExifTag.ImageLength).Value); + Assert.Equal(64, (int)meta.GetValue(ExifTag.RowsPerStrip).Value); + + Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.ImageWidth)); + Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripOffsets)); + Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripByteCounts)); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs new file mode 100644 index 000000000..9f5b78cc3 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Writers; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class BigTiffMetadataTests + { + [Fact] + public void ExifLong8() + { + var long8 = new ExifLong8(ExifTagValue.StripByteCounts); + + Assert.True(long8.TrySetValue(0)); + Assert.Equal(0UL, long8.GetValue()); + + Assert.True(long8.TrySetValue(100u)); + Assert.Equal(100UL, long8.GetValue()); + + Assert.True(long8.TrySetValue(ulong.MaxValue)); + Assert.Equal(ulong.MaxValue, long8.GetValue()); + + Assert.False(long8.TrySetValue(-65)); + Assert.Equal(ulong.MaxValue, long8.GetValue()); + } + + [Fact] + public void ExifSignedLong8() + { + var long8 = new ExifSignedLong8(ExifTagValue.ImageID); + + Assert.False(long8.TrySetValue(0)); + + Assert.True(long8.TrySetValue(0L)); + Assert.Equal(0L, long8.GetValue()); + + Assert.True(long8.TrySetValue(-100L)); + Assert.Equal(-100L, long8.GetValue()); + Assert.Equal(ExifDataType.SignedLong8, long8.DataType); + + Assert.True(long8.TrySetValue(long.MaxValue)); + Assert.Equal(long.MaxValue, long8.GetValue()); + Assert.Equal(ExifDataType.SignedLong8, long8.DataType); + } + + [Fact] + public void ExifLong8Array() + { + var long8 = new ExifLong8Array(ExifTagValue.StripOffsets); + + Assert.True(long8.TrySetValue((short)-123)); + Assert.Equal(new[] { 0UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue((ushort)123)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue((short)123)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(123)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(123u)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(123L)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(123UL)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(new short[] { -1, 2, -3, 4 })); + Assert.Equal(new ulong[] { 0, 2UL, 0, 4UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(new[] { 1, 2, 3, 4 })); + Assert.Equal(new[] { 1UL, 2UL, 3UL, 4UL }, long8.GetValue()); + Assert.Equal(ExifDataType.Long, long8.DataType); + + Assert.True(long8.TrySetValue(new[] { 1, 2, 3, 4, long.MaxValue })); + Assert.Equal(new[] { 1UL, 2UL, 3UL, 4UL, (ulong)long.MaxValue }, long8.GetValue()); + Assert.Equal(ExifDataType.Long8, long8.DataType); + } + + [Fact] + public void ExifSignedLong8Array() + { + var long8 = new ExifSignedLong8Array(ExifTagValue.StripOffsets); + + Assert.True(long8.TrySetValue(new[] { 0L })); + Assert.Equal(new[] { 0L }, long8.GetValue()); + Assert.Equal(ExifDataType.SignedLong8, long8.DataType); + + Assert.True(long8.TrySetValue(new[] { -1L, 2L, long.MinValue, 4L })); + Assert.Equal(new[] { -1L, 2L, long.MinValue, 4L }, long8.GetValue()); + Assert.Equal(ExifDataType.SignedLong8, long8.DataType); + } + + [Fact] + public void NotCoveredTags() + { + using var input = new Image(10, 10); + + var testTags = new Dictionary + { + { new ExifTag((ExifTagValue)0xdd01), (ExifDataType.SingleFloat, new float[] { 1.2f, 2.3f, 4.5f }) }, + { new ExifTag((ExifTagValue)0xdd02), (ExifDataType.SingleFloat, 2.345f) }, + { new ExifTag((ExifTagValue)0xdd03), (ExifDataType.DoubleFloat, new double[] { 4.5, 6.7 }) }, + { new ExifTag((ExifTagValue)0xdd04), (ExifDataType.DoubleFloat, 8.903) }, + { new ExifTag((ExifTagValue)0xdd05), (ExifDataType.SignedByte, (sbyte)-3) }, + { new ExifTag((ExifTagValue)0xdd06), (ExifDataType.SignedByte, new sbyte[] { -3, 0, 5 }) }, + { new ExifTag((ExifTagValue)0xdd07), (ExifDataType.SignedLong, new int[] { int.MinValue, 1, int.MaxValue }) }, + { new ExifTag((ExifTagValue)0xdd08), (ExifDataType.Long, new uint[] { 0, 1, uint.MaxValue }) }, + { new ExifTag((ExifTagValue)0xdd09), (ExifDataType.SignedShort, (short)-1234) }, + { new ExifTag((ExifTagValue)0xdd10), (ExifDataType.Short, (ushort)1234) }, + }; + + // arrange + var values = new List(); + foreach (KeyValuePair tag in testTags) + { + ExifValue newExifValue = ExifValues.Create((ExifTagValue)(ushort)tag.Key, tag.Value.DataType, tag.Value.Value is Array); + + Assert.True(newExifValue.TrySetValue(tag.Value.Value)); + values.Add(newExifValue); + } + + input.Frames.RootFrame.Metadata.ExifProfile = new ExifProfile(values, Array.Empty()); + + // act + var encoder = new TiffEncoder(); + using var memStream = new MemoryStream(); + input.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(memStream); + ImageFrameMetadata loadedFrameMetadata = output.Frames.RootFrame.Metadata; + foreach (KeyValuePair tag in testTags) + { + IExifValue exifValue = loadedFrameMetadata.ExifProfile.GetValueInternal(tag.Key); + Assert.NotNull(exifValue); + object value = exifValue.GetValue(); + + Assert.Equal(tag.Value.DataType, exifValue.DataType); + { + Assert.Equal(value, tag.Value.Value); + } + } + } + + [Fact] + public void NotCoveredTags64bit() + { + var testTags = new Dictionary + { + { new ExifTag((ExifTagValue)0xdd11), (ExifDataType.Long8, ulong.MaxValue) }, + { new ExifTag((ExifTagValue)0xdd12), (ExifDataType.SignedLong8, long.MaxValue) }, + //// WriteIfdTags64Bit: arrays aren't support (by our code) + ////{ new ExifTag((ExifTagValue)0xdd13), (ExifDataType.Long8, new ulong[] { 0, 1234, 56789UL, ulong.MaxValue }) }, + ////{ new ExifTag((ExifTagValue)0xdd14), (ExifDataType.SignedLong8, new long[] { -1234, 56789L, long.MaxValue }) }, + }; + + var values = new List(); + foreach (KeyValuePair tag in testTags) + { + ExifValue newExifValue = ExifValues.Create((ExifTagValue)(ushort)tag.Key, tag.Value.DataType, tag.Value.Value is Array); + + Assert.True(newExifValue.TrySetValue(tag.Value.Value)); + values.Add(newExifValue); + } + + // act + byte[] inputBytes = WriteIfdTags64Bit(values); + Configuration config = Configuration.Default; + var reader = new EntryReader( + new MemoryStream(inputBytes), + BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian, + config.MemoryAllocator); + + reader.ReadTags(true, 0); + + List outputTags = reader.Values; + + // assert + foreach (KeyValuePair tag in testTags) + { + IExifValue exifValue = outputTags.Find(t => t.Tag == tag.Key); + Assert.NotNull(exifValue); + object value = exifValue.GetValue(); + + Assert.Equal(tag.Value.DataType, exifValue.DataType); + { + Assert.Equal(value, tag.Value.Value); + } + } + } + + private static byte[] WriteIfdTags64Bit(List values) + { + byte[] buffer = new byte[8]; + var ms = new MemoryStream(); + var writer = new TiffStreamWriter(ms); + WriteLong8(writer, buffer, (ulong)values.Count); + + foreach (IExifValue entry in values) + { + writer.Write((ushort)entry.Tag); + writer.Write((ushort)entry.DataType); + WriteLong8(writer, buffer, ExifWriter.GetNumberOfComponents(entry)); + + uint length = ExifWriter.GetLength(entry); + + Assert.True(length <= 8); + + if (length <= 8) + { + int sz = ExifWriter.WriteValue(entry, buffer, 0); + DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written"); + + // write padded + writer.BaseStream.Write(buffer.AsSpan(0, sz)); + int d = sz % 8; + if (d != 0) + { + writer.BaseStream.Write(new byte[d]); + } + } + } + + WriteLong8(writer, buffer, 0); + + return ms.ToArray(); + } + + private static void WriteLong8(TiffStreamWriter writer, byte[] buffer, ulong value) + { + if (writer.IsLittleEndian) + { + BinaryPrimitives.WriteUInt64LittleEndian(buffer, value); + } + else + { + BinaryPrimitives.WriteUInt64BigEndian(buffer, value); + } + + writer.BaseStream.Write(buffer); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs new file mode 100644 index 000000000..aeadae255 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + public abstract class TiffDecoderBaseTester + { + protected static TiffDecoder TiffDecoder => new TiffDecoder(); + + protected static MagickReferenceDecoder ReferenceDecoder => new MagickReferenceDecoder(); + + protected static void TestTiffDecoder(TestImageProvider provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + image.CompareToOriginal( + provider, + useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), + referenceDecoder ?? ReferenceDecoder); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index f8256ead9..ea0544acf 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -4,32 +4,25 @@ // ReSharper disable InconsistentNaming using System; using System.IO; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff { [Trait("Format", "Tiff")] - public class TiffDecoderTests + public class TiffDecoderTests : TiffDecoderBaseTester { public static readonly string[] MultiframeTestImages = Multiframes; - private static TiffDecoder TiffDecoder => new(); - - private static MagickReferenceDecoder ReferenceDecoder => new(); - [Theory] [WithFile(RgbUncompressedTiled, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] public void ThrowsNotSupported(TestImageProvider provider) - where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder)); + where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder)); [Theory] [InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)] @@ -406,16 +399,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSaveMultiFrame(provider); image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, ReferenceDecoder); } - - private static void TestTiffDecoder(TestImageProvider provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(TiffDecoder); - image.DebugSave(provider); - image.CompareToOriginal( - provider, - useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), - referenceDecoder ?? ReferenceDecoder); - } } } diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index ebc096852..3ee20cbd1 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -185,9 +185,9 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif 2 x due to use of non-standard padding tag 0xEA1C listed in EXIF Tool. We can read those values but adhere strictly to the 2.3.1 specification when writing. (TODO: Support 2.3.2) https://exiftool.org/TagNames/EXIF.html */ - [InlineData(TestImageWriteFormat.Jpeg, 16)] - [InlineData(TestImageWriteFormat.Png, 16)] - [InlineData(TestImageWriteFormat.WebpLossless, 16)] + [InlineData(TestImageWriteFormat.Jpeg, 18)] + [InlineData(TestImageWriteFormat.Png, 18)] + [InlineData(TestImageWriteFormat.WebpLossless, 18)] public void SetValue(TestImageWriteFormat imageFormat, int expectedProfileValueCount) { Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); @@ -557,7 +557,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif // todo: duplicate tags Assert.Equal(2, profile.Values.Count(v => (ushort)v.Tag == 59932)); - Assert.Equal(16, profile.Values.Count); + Assert.Equal(18, profile.Values.Count); foreach (IExifValue value in profile.Values) { diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs index 3358b1f97..a5ca115d4 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs @@ -394,6 +394,34 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values Assert.Equal(expected, typed.Value); } + [Fact] + public void NumberTests() + { + Number value1 = ushort.MaxValue; + Number value2 = ushort.MaxValue; + Assert.True(value1 == value2); + + value2 = short.MaxValue; + Assert.True(value1 != value2); + + value1 = -1; + value2 = -2; + Assert.True(value1 > value2); + + value1 = -6; + Assert.True(value1 <= value2); + + value1 = 10; + value2 = 10; + Assert.True(value1 >= value2); + + Assert.True(value1.Equals(value2)); + Assert.True(value1.GetHashCode() == value2.GetHashCode()); + + value1 = 1; + Assert.False(value1.Equals(value2)); + } + [Theory] [MemberData(nameof(NumberTags))] public void ExifNumberTests(ExifTag tag) @@ -408,6 +436,9 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values var typed = (ExifNumber)value; Assert.Equal(expected, typed.Value); + + typed.Value = ushort.MaxValue + 1; + Assert.True(expected < typed.Value); } [Theory] @@ -422,6 +453,15 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values var typed = (ExifNumberArray)value; Assert.Equal(expected, typed.Value); + + Assert.True(value.TrySetValue(int.MaxValue)); + Assert.Equal(new[] { (Number)int.MaxValue }, value.GetValue()); + + Assert.True(value.TrySetValue(new[] { 1u, 2u, 5u })); + Assert.Equal(new[] { (Number)1u, (Number)2u, (Number)5u }, value.GetValue()); + + Assert.True(value.TrySetValue(new[] { (short)1, (short)2, (short)5 })); + Assert.Equal(new[] { (Number)(short)1, (Number)(short)2, (Number)(short)5 }, value.GetValue()); } [Theory] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index fc63e5b1f..3f28a6116 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -872,6 +872,29 @@ namespace SixLabors.ImageSharp.Tests public static readonly string[] Metadata = { SampleMetadata }; } + public static class BigTiff + { + public const string Base = "Tiff/BigTiff/"; + + public const string BigTIFF = Base + "BigTIFF.tif"; + public const string BigTIFFLong = Base + "BigTIFFLong.tif"; + public const string BigTIFFLong8 = Base + "BigTIFFLong8.tif"; + public const string BigTIFFLong8Tiles = Base + "BigTIFFLong8Tiles.tif"; + public const string BigTIFFMotorola = Base + "BigTIFFMotorola.tif"; + public const string BigTIFFMotorolaLongStrips = Base + "BigTIFFMotorolaLongStrips.tif"; + + public const string BigTIFFSubIFD4 = Base + "BigTIFFSubIFD4.tif"; + public const string BigTIFFSubIFD8 = Base + "BigTIFFSubIFD8.tif"; + + public const string Indexed4_Deflate = Base + "BigTIFF_Indexed4_Deflate.tif"; + public const string Indexed8_LZW = Base + "BigTIFF_Indexed8_LZW.tif"; + public const string MinIsBlack = Base + "BigTIFF_MinIsBlack.tif"; + public const string MinIsWhite = Base + "BigTIFF_MinIsWhite.tif"; + + public const string Damaged_MinIsWhite_RLE = Base + "BigTIFF_MinIsWhite_RLE.tif"; + public const string Damaged_MinIsBlack_RLE = Base + "BigTIFF_MinIsBlack_RLE.tif"; + } + public static class Pbm { public const string BlackAndWhitePlain = "Pbm/blackandwhite_plain.pbm"; diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF.tif new file mode 100644 index 000000000..b27834479 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddb202145a9bce7670cc372ee578de5a53cd52cc8d5ae8a9ebdc9f9c4f4a7e81 +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFLong.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong.tif new file mode 100644 index 000000000..b390c0a97 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90178643a159ec50335e9314836df924233debeb100763af0f77cd1be3cf58ab +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8.tif new file mode 100644 index 000000000..e1815c750 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b38e61ccb01e10e26fb10c335fc6fca9ad087b0fb0df833e54bb02d1246e20d5 +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8Tiles.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8Tiles.tif new file mode 100644 index 000000000..c69218948 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8Tiles.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2d92b0c430cefc390f13961e00950ee7246b013335594dd249ba823eb3c3fdb +size 12564 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorola.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorola.tif new file mode 100644 index 000000000..68c2b3a41 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorola.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ace8a27dbed9f918993615e545a12310b84ad94bc6af8e256258e69731f1c7ce +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorolaLongStrips.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorolaLongStrips.tif new file mode 100644 index 000000000..cc62e0362 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorolaLongStrips.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c5ebdc3774955d3b47644f57bd023e764a93ca2271118152ae920b34c1784bb +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFSamples.md b/tests/Images/Input/Tiff/BigTiff/BigTIFFSamples.md new file mode 100644 index 000000000..0401109fd --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFSamples.md @@ -0,0 +1,220 @@ +These images were created by [AWare Systems](http://www.awaresystems.be/). + +# Index + +[Classic.tif](#classictif) +[BigTIFF.tif](#bigtifftif) +[BigTIFFMotorola.tif](#bigtiffmotorolatif) +[BigTIFFLong.tif](#bigtifflongtif) +[BigTIFFLong8.tif](#bigtifflong8tif) +[BigTIFFMotorolaLongStrips.tif](#bigtiffmotorolalongstripstif) +[BigTIFFLong8Tiles.tif](#bigtifflong8tilestif) +[BigTIFFSubIFD4.tif](#bigtiffsubifd4tif) +[BigTIFFSubIFD8.tif](#bigtiffsubifd8tif) + +# Classic.tif + +Classic.tif is a basic Classic TIFF file. All files in this package have the same actual image content, so this TIFF file serves as a reference. + +Format: Classic TIFF +Byte Order: Intel +Ifd Offset: 12302 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 8 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 + +# BigTIFF.tif + +BigTIFF.tif ressembles Classic.tif as close as possible. Except that it's a BigTIFF, that is... + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 + +# BigTIFFMotorola.tif + +BigTIFFMotorola.tif reverses the byte order. + +Format: BigTIFF +Byte Order: Motorola +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 + +# BigTIFFLong.tif + +All previous TIFFs specify DataType Short for StripOffsets and StripByteCounts tags. This BigTIFF instead specifies DataType Long, for these tags. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Long): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Long): 12288 + +# BigTIFFLong8.tif + +This next one specifies DataType Long8, for StripOffsets and StripByteCounts tags. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Long8): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Long8): 12288 + +# BigTIFFMotorolaLongStrips.tif + +This BigTIFF has Motorola byte order, plus, it's divided over two strips. StripOffsets and StripByteCounts tags have DataType Long, so their actual value fits inside the IFD. + +Format: BigTIFF +Byte Order: Motorola +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (2 Long): 16, 6160 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (2 Long): 6144, 6144 + +# BigTIFFLong8Tiles.tif + +BigTIFFLong8Tiles.tif is a tiled BigTIFF. TileOffsets and TileByteCounts tags specify DataType Long8. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 12368 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    SamplesPerPixel (1 Short): 3 +    TileWidth (1 Short): 32 +    TileLength (1 Short): 32 +    TileOffsets (4 Long8): 16, 3088, 6160, 9232 +    TileByteCounts (4 Long8): 3072, 3072, 3072, 3072 + +# BigTIFFSubIFD4.tif + +This BigTIFF contains two pages, the second page showing almost the same image content as the first, except that the black square is white, and text color is black. Both pages point to a downsample SubIFD, using SubIFDs DataType TIFF_IFD. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 15572 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 3284 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 +    SubIFDs (1 IFD): 3088 +SubIfd Offset: 3088 +    NewSubFileType (1 Long): 1 +    ImageWidth (1 Short): 32 +    ImageLength (1 Short): 32 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (1 Short): 3072 +Ifd Offset: 31324 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 19036 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 +    SubIFDs (1 IFD): 18840 +SubIfd Offset: 18840 +    NewSubFileType (1 Long): 1 +    ImageWidth (1 Short): 32 +    ImageLength (1 Short): 32 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 15768 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (1 Short): 3072 + +# BigTIFFSubIFD8.tif + +BigTIFFSubIFD4.tif is very much the same as BigTIFFSubIFD4.tif, except that the new DataType TIFF_IFD8 is used for the SubIFDs tag. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 15572 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 3284 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 +    SubIFDs (1 IFD8): 3088 +SubIfd Offset: 3088 +    NewSubFileType (1 Long): 1 +    ImageWidth (1 Short): 32 +    ImageLength (1 Short): 32 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (1 Short): 3072 +Ifd Offset: 31324 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 19036 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 +    SubIFDs (1 IFD8): 18840 +SubIfd Offset: 18840 +    NewSubFileType (1 Long): 1 +    ImageWidth (1 Short): 32 +    ImageLength (1 Short): 32 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 15768 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (1 Short): 3072 \ No newline at end of file diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD4.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD4.tif new file mode 100644 index 000000000..cd7bdf0b5 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD4.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85c8da46abc2284f0ddac10bd03a7c98ee51024840b7e43f41f1c6a09bc02e7e +size 31520 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD8.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD8.tif new file mode 100644 index 000000000..bb0b3bf18 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD8.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93e5ac30f507bec7936746ad6e109631c09f9b2332081e986063219ad2452a4a +size 31520 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed4_Deflate.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed4_Deflate.tif new file mode 100644 index 000000000..bc736790a --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed4_Deflate.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38ba3717b284d7914243609576d0f9b75d732692bf05e2e1ec8b119feb1409fd +size 687 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed8_LZW.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed8_LZW.tif new file mode 100644 index 000000000..edff7c40a --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed8_LZW.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca404b3ec5560b82169855f0ae69e64c6bc7286117b95fc0e0d505e5e356fa0e +size 2548 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack.tif new file mode 100644 index 000000000..491b4092e --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac0471c1600f6e5fb47037dab07172aff524abc866a40c9ec54279bd49cbef77 +size 517 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack_RLE.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack_RLE.tif new file mode 100644 index 000000000..d15188c17 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack_RLE.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:038a298bcace02810054af650f490b6858863c8755e41b786605aa807b43350a +size 509 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite.tif new file mode 100644 index 000000000..fb26e58a4 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a95b0b46bf4f75babb86d9ec74694e6d684087504be214df48a6c8a54338834c +size 517 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite_RLE.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite_RLE.tif new file mode 100644 index 000000000..d15188c17 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite_RLE.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:038a298bcace02810054af650f490b6858863c8755e41b786605aa807b43350a +size 509 diff --git a/tests/Images/Input/Tiff/BigTiff/Classic.tif b/tests/Images/Input/Tiff/BigTiff/Classic.tif new file mode 100644 index 000000000..1fa6be78b --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/Classic.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfa7fcf6927be5de644beb238067479e8d834d0cbe2257b00302f5dde84a1c1a +size 12404 diff --git a/tests/Images/Input/Tiff/BigTiff/readme.md b/tests/Images/Input/Tiff/BigTiff/readme.md new file mode 100644 index 000000000..29f9c8ea9 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/readme.md @@ -0,0 +1,5 @@ +#### BigTIFF samples. + +For details: [BigTIFFSamples.md](BigTIFFSamples.md) + +Downloaded from https://www.awaresystems.be/imaging/tiff/bigtiff.html \ No newline at end of file