Browse Source

Merge pull request #1760 from IldarKhayrutdinov/bigtiff

Support BigTiff decoding
pull/1924/head
James Jackson-South 4 years ago
committed by GitHub
parent
commit
5fa5e50bad
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs
  2. 10
      src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
  3. 77
      src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
  4. 68
      src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs
  5. 80
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  6. 13
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  7. 13
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  8. 21
      src/ImageSharp/Formats/Tiff/TiffFormatType.cs
  9. 41
      src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs
  10. 5
      src/ImageSharp/Formats/Tiff/TiffMetadata.cs
  11. 2
      src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs
  12. 17
      src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs
  13. 4
      src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs
  14. 287
      src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
  15. 18
      src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
  16. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
  17. 66
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs
  18. 171
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs
  19. 34
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs
  20. 26
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs
  21. 22
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs
  22. 5
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs
  23. 130
      tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs
  24. 260
      tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs
  25. 30
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs
  26. 22
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  27. 8
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs
  28. 40
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs
  29. 23
      tests/ImageSharp.Tests/TestImages.cs
  30. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFF.tif
  31. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFFLong.tif
  32. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFFLong8.tif
  33. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFFLong8Tiles.tif
  34. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFFMotorola.tif
  35. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFFMotorolaLongStrips.tif
  36. 220
      tests/Images/Input/Tiff/BigTiff/BigTIFFSamples.md
  37. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD4.tif
  38. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD8.tif
  39. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed4_Deflate.tif
  40. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed8_LZW.tif
  41. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack.tif
  42. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack_RLE.tif
  43. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite.tif
  44. 3
      tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite_RLE.tif
  45. 3
      tests/Images/Input/Tiff/BigTiff/Classic.tif
  46. 5
      tests/Images/Input/Tiff/BigTiff/readme.md

12
src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs

@ -34,17 +34,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
/// <param name="stripByteCount">The number of bytes to read from the input stream.</param>
/// <param name="stripHeight">The height of the strip.</param>
/// <param name="buffer">The output buffer for uncompressed data.</param>
public void Decompress(BufferedReadStream stream, uint stripOffset, uint stripByteCount, int stripHeight, Span<byte> buffer)
public void Decompress(BufferedReadStream stream, ulong stripOffset, ulong stripByteCount, int stripHeight, Span<byte> 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.");
}

10
src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs

@ -35,6 +35,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants
/// </summary>
public const ushort HeaderMagicNumber = 42;
/// <summary>
/// The big tiff header magic number
/// </summary>
public const ushort BigTiffHeaderMagicNumber = 43;
/// <summary>
/// The big tiff bytesize of offsets value.
/// </summary>
public const ushort BigTiffBytesize = 8;
/// <summary>
/// RowsPerStrip default value, which is effectively infinity.
/// </summary>

77
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
/// </summary>
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<uint, Action> lazyLoaders = new(new DuplicateKeyComparer<uint>());
private ulong nextIfdOffset;
public DirectoryReader(Stream stream) => this.stream = stream;
public DirectoryReader(Stream stream, MemoryAllocator allocator)
{
this.stream = stream;
this.allocator = allocator;
}
/// <summary>
/// Gets the byte order.
/// </summary>
public ByteOrder ByteOrder { get; private set; }
public bool IsBigTiff { get; private set; }
/// <summary>
/// Reads image file directories.
/// </summary>
@ -38,14 +43,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff
public IEnumerable<ExifProfile> 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<byte> 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<ExifProfile> ReadIfds()
private IEnumerable<ExifProfile> ReadIfds(bool isBigTiff)
{
var readers = new List<EntryReader>();
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<ExifProfile>();
var list = new List<ExifProfile>(readers.Count);
foreach (EntryReader reader in readers)
{
reader.ReadBigValues();
var profile = new ExifProfile(reader.Values, reader.InvalidTags);
list.Add(profile);
}
return list;
}
/// <summary>
/// <see cref="DuplicateKeyComparer{TKey}"/> used for possibility add a duplicate offsets (but tags don't duplicate).
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
private class DuplicateKeyComparer<TKey> : IComparer<TKey>
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;
}
}
}
}

68
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<uint, Action> lazyLoaders;
public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList<uint, Action> 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<IExifValue> 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();
}
}
}

80
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
/// </summary>
private ByteOrder byteOrder;
/// <summary>
/// Indicating whether is BigTiff format.
/// </summary>
private bool isBigTiff;
/// <summary>
/// Initializes a new instance of the <see cref="TiffDecoderCore" /> class.
/// </summary>
@ -141,10 +147,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
where TPixel : unmanaged, IPixel<TPixel>
{
this.inputStream = stream;
var reader = new DirectoryReader(stream);
var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator);
IEnumerable<ExifProfile> directories = reader.Read();
this.byteOrder = reader.ByteOrder;
this.isBigTiff = reader.IsBigTiff;
var frames = new List<ImageFrame<TPixel>>();
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<TPixel> 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<ExifProfile> 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<TPixel>(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<ulong> stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span<ulong> stripOffsets);
IMemoryOwner<ulong> stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span<ulong> 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<ulong> ConvertNumbers(Array array, out Span<ulong> span)
{
if (array is Number[] numbers)
{
IMemoryOwner<ulong> memory = this.memoryAllocator.Allocate<ulong>(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;
}
}
/// <summary>
/// Calculates the size (in bytes) for a pixel buffer using the determined color format.
/// </summary>
@ -276,7 +320,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param>
/// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken)
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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
/// <param name="stripOffsets">The strip offsets.</param>
/// <param name="stripByteCounts">The strip byte counts.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken)
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<byte> stripBuffer = this.memoryAllocator.Allocate<byte>(uncompressedStripSize, AllocationOptions.Clean);
System.Span<byte> stripBufferSpan = stripBuffer.GetSpan();
Span<byte> stripBufferSpan = stripBuffer.GetSpan();
Buffer2D<TPixel> 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;
}

13
src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs

@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
internal static class TiffDecoderMetadataCreator
{
public static ImageMetadata Create<TPixel>(List<ImageFrame<TPixel>> frames, bool ignoreMetadata, ByteOrder byteOrder)
public static ImageMetadata Create<TPixel>(List<ImageFrame<TPixel>> frames, bool ignoreMetadata, ByteOrder byteOrder, bool isBigTiff)
where TPixel : unmanaged, IPixel<TPixel>
{
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;
}

13
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -24,12 +24,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// <param name="frameMetadata">The IFD entries container to read the image format information for current frame.</param>
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;
}
}

21
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
{
/// <summary>
/// The TIFF format type enum.
/// </summary>
public enum TiffFormatType
{
/// <summary>
/// The TIFF file format type.
/// </summary>
Default,
/// <summary>
/// The BigTIFF format type.
/// </summary>
BigTIFF
}
}

41
src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
public sealed class TiffImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize => 4;
public int HeaderSize => 8;
/// <inheritdoc/>
public IImageFormat DetectFormat(ReadOnlySpan<byte> header)
@ -26,9 +26,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff
private bool IsSupportedFileFormat(ReadOnlySpan<byte> 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;
}
}
}

5
src/ImageSharp/Formats/Tiff/TiffMetadata.cs

@ -26,6 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
public ByteOrder ByteOrder { get; set; }
/// <summary>
/// Gets or sets the format type.
/// </summary>
public TiffFormatType FormatType { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffMetadata(this);
}

2
src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
/// <param name="errorMessage">The error message for the exception.</param>
[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}");

17
src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs

@ -80,6 +80,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// <summary>
/// Reference to an IFD (32-bit (4-byte) unsigned integer).
/// </summary>
Ifd = 13
Ifd = 13,
/// <summary>
/// A 64-bit (8-byte) unsigned integer.
/// </summary>
Long8 = 16,
/// <summary>
/// A 64-bit (8-byte) signed integer (2's complement notation).
/// </summary>
SignedLong8 = 17,
/// <summary>
/// Reference to an IFD (64-bit (8-byte) unsigned integer).
/// </summary>
Ifd8 = 18,
}
}

4
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());

287
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<Action> loaders = new List<Action>();
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
/// </summary>
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<ExifTag> invalidTags;
private uint exifOffset;
private uint gpsOffset;
private List<ulong> 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<TDataType>(ReadOnlySpan<byte> 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<IExifValue> 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<ulong>(size, int.MaxValue, nameof(size));
if ((int)size > maxSize)
{
maxSize = (int)size;
}
}
if (this.allocator != null)
{
// tiff, bigTiff
using IMemoryOwner<byte> memory = this.allocator.Allocate<byte>(maxSize);
Span<byte> 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<byte> 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();
}
/// <summary>
/// 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<byte> offsetBuffer = stackalloc byte[4];
for (int i = 0; i < count; i++)
{
this.ReadValue(values);
this.ReadValue(values, offsetBuffer);
}
}
protected void ReadSubIfd(List<IExifValue> 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<IExifValue> 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<byte> offsetBuffer = stackalloc byte[8];
for (ulong i = 0; i < count; i++)
{
this.ReadValue64(values, offsetBuffer);
}
}
protected void ReadBigValue(IList<IExifValue> values, (ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag, Span<byte> 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<byte> buffer, uint numberOfComponents)
private object ConvertValue(ExifDataType dataType, ReadOnlySpan<byte> 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<IExifValue> values)
private void ReadValue(List<IExifValue> values, Span<byte> 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<IExifValue> values, Span<byte> 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<ExifTag>()).Add(tag);
private void Seek(long pos)
=> this.data.Seek(pos, SeekOrigin.Begin);
private void AddSubIfd(object val)
=> (this.subIfds ??= new List<ulong>()).Add(Convert.ToUInt64(val));
private void Seek(ulong pos)
=> this.data.Seek((long)pos, SeekOrigin.Begin);
private bool TryReadSpan(Span<byte> 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<byte> buffer)
{
if (buffer.Length < 8)
{
return default;
}
return this.IsBigEndian
? BinaryPrimitives.ReadInt64BigEndian(buffer)
: BinaryPrimitives.ReadInt64LittleEndian(buffer);
}
private ulong ConvertToUInt64(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < 8)
{
return default;
}
return this.IsBigEndian
? BinaryPrimitives.ReadUInt64BigEndian(buffer)
: BinaryPrimitives.ReadUInt64LittleEndian(buffer);
}
private double ConvertToDouble(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < 8)

18
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<byte> destination, int offset)
{
BinaryPrimitives.WriteInt64LittleEndian(destination.Slice(offset, 8), value);
return offset + 8;
}
private static int WriteUInt64(ulong value, Span<byte> destination, int offset)
{
BinaryPrimitives.WriteUInt64LittleEndian(destination.Slice(offset, 8), value);
return offset + 8;
}
private static int WriteInt32(int value, Span<byte> 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;

5
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs

@ -65,5 +65,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// Gets the TimeZoneOffset exif tag.
/// </summary>
public static ExifTag<uint[]> TimeZoneOffset { get; } = new ExifTag<uint[]>(ExifTagValue.TimeZoneOffset);
/// <summary>
/// Gets the offset to child IFDs exif tag.
/// </summary>
public static ExifTag<uint[]> SubIFDs { get; } = new ExifTag<uint[]>(ExifTagValue.SubIFDs);
}
}

66
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<ulong>
{
public ExifLong8(ExifTag<ulong> 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);
}
}

171
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<ulong>
{
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;
}
}
}

34
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;

26
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<long>
{
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);
}
}

22
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<long>
{
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);
}
}

5
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);

130
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(BigTIFFLong8Tiles, PixelTypes.Rgba32)]
public void ThrowsNotSupported<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => Assert.Throws<NotSupportedException>(() => provider.GetImage(TiffDecoder));
[Theory]
[WithFile(Damaged_MinIsWhite_RLE, PixelTypes.Rgba32)]
[WithFile(Damaged_MinIsBlack_RLE, PixelTypes.Rgba32)]
public void DamagedFiles<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
Assert.Throws<ImageDifferenceIsOverThresholdException>(() => TestTiffDecoder(provider));
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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));
}
}
}

260
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<Rgba32>(10, 10);
var testTags = new Dictionary<ExifTag, (ExifDataType DataType, object Value)>
{
{ new ExifTag<float[]>((ExifTagValue)0xdd01), (ExifDataType.SingleFloat, new float[] { 1.2f, 2.3f, 4.5f }) },
{ new ExifTag<float>((ExifTagValue)0xdd02), (ExifDataType.SingleFloat, 2.345f) },
{ new ExifTag<double[]>((ExifTagValue)0xdd03), (ExifDataType.DoubleFloat, new double[] { 4.5, 6.7 }) },
{ new ExifTag<double>((ExifTagValue)0xdd04), (ExifDataType.DoubleFloat, 8.903) },
{ new ExifTag<sbyte>((ExifTagValue)0xdd05), (ExifDataType.SignedByte, (sbyte)-3) },
{ new ExifTag<sbyte[]>((ExifTagValue)0xdd06), (ExifDataType.SignedByte, new sbyte[] { -3, 0, 5 }) },
{ new ExifTag<int[]>((ExifTagValue)0xdd07), (ExifDataType.SignedLong, new int[] { int.MinValue, 1, int.MaxValue }) },
{ new ExifTag<uint[]>((ExifTagValue)0xdd08), (ExifDataType.Long, new uint[] { 0, 1, uint.MaxValue }) },
{ new ExifTag<short>((ExifTagValue)0xdd09), (ExifDataType.SignedShort, (short)-1234) },
{ new ExifTag<ushort>((ExifTagValue)0xdd10), (ExifDataType.Short, (ushort)1234) },
};
// arrange
var values = new List<IExifValue>();
foreach (KeyValuePair<ExifTag, (ExifDataType DataType, object Value)> 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<ExifTag>());
// act
var encoder = new TiffEncoder();
using var memStream = new MemoryStream();
input.Save(memStream, encoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
ImageFrameMetadata loadedFrameMetadata = output.Frames.RootFrame.Metadata;
foreach (KeyValuePair<ExifTag, (ExifDataType DataType, object Value)> 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<ExifTag, (ExifDataType DataType, object Value)>
{
{ new ExifTag<ulong>((ExifTagValue)0xdd11), (ExifDataType.Long8, ulong.MaxValue) },
{ new ExifTag<long>((ExifTagValue)0xdd12), (ExifDataType.SignedLong8, long.MaxValue) },
//// WriteIfdTags64Bit: arrays aren't support (by our code)
////{ new ExifTag<ulong[]>((ExifTagValue)0xdd13), (ExifDataType.Long8, new ulong[] { 0, 1234, 56789UL, ulong.MaxValue }) },
////{ new ExifTag<long[]>((ExifTagValue)0xdd14), (ExifDataType.SignedLong8, new long[] { -1234, 56789L, long.MaxValue }) },
};
var values = new List<IExifValue>();
foreach (KeyValuePair<ExifTag, (ExifDataType DataType, object Value)> 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<IExifValue> outputTags = reader.Values;
// assert
foreach (KeyValuePair<ExifTag, (ExifDataType DataType, object Value)> 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<IExifValue> 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);
}
}
}

30
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<TPixel>(TestImageProvider<TPixel> provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(TiffDecoder);
image.DebugSave(provider);
image.CompareToOriginal(
provider,
useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance),
referenceDecoder ?? ReferenceDecoder);
}
}
}

22
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => Assert.Throws<NotSupportedException>(() => provider.GetImage(TiffDecoder));
where TPixel : unmanaged, IPixel<TPixel> => Assert.Throws<NotSupportedException>(() => 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<TPixel>(TestImageProvider<TPixel> provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(TiffDecoder);
image.DebugSave(provider);
image.CompareToOriginal(
provider,
useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance),
referenceDecoder ?? ReferenceDecoder);
}
}
}

8
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<Rgba32> 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)
{

40
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]

23
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";

3
tests/Images/Input/Tiff/BigTiff/BigTIFF.tif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ddb202145a9bce7670cc372ee578de5a53cd52cc8d5ae8a9ebdc9f9c4f4a7e81
size 12480

3
tests/Images/Input/Tiff/BigTiff/BigTIFFLong.tif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90178643a159ec50335e9314836df924233debeb100763af0f77cd1be3cf58ab
size 12480

3
tests/Images/Input/Tiff/BigTiff/BigTIFFLong8.tif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b38e61ccb01e10e26fb10c335fc6fca9ad087b0fb0df833e54bb02d1246e20d5
size 12480

3
tests/Images/Input/Tiff/BigTiff/BigTIFFLong8Tiles.tif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a2d92b0c430cefc390f13961e00950ee7246b013335594dd249ba823eb3c3fdb
size 12564

3
tests/Images/Input/Tiff/BigTiff/BigTIFFMotorola.tif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ace8a27dbed9f918993615e545a12310b84ad94bc6af8e256258e69731f1c7ce
size 12480

3
tests/Images/Input/Tiff/BigTiff/BigTIFFMotorolaLongStrips.tif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c5ebdc3774955d3b47644f57bd023e764a93ca2271118152ae920b34c1784bb
size 12480

220
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

3
tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD4.tif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:85c8da46abc2284f0ddac10bd03a7c98ee51024840b7e43f41f1c6a09bc02e7e
size 31520

3
tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD8.tif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:93e5ac30f507bec7936746ad6e109631c09f9b2332081e986063219ad2452a4a
size 31520

3
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

3
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

3
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

3
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

3
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

3
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

3
tests/Images/Input/Tiff/BigTiff/Classic.tif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cfa7fcf6927be5de644beb238067479e8d834d0cbe2257b00302f5dde84a1c1a
size 12404

5
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
Loading…
Cancel
Save