Browse Source

Merge pull request #1494 from IldarKhayrutdinov/tiff-format

#12 Use exif reader for tiff tags reading
pull/1570/head
James Jackson-South 5 years ago
committed by GitHub
parent
commit
9c62ab1885
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs
  2. 108
      src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
  3. 316
      src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs
  4. 88
      src/ImageSharp/Formats/Tiff/Streams/TiffBigEndianStream.cs
  5. 88
      src/ImageSharp/Formats/Tiff/Streams/TiffLittleEndianStream.cs
  6. 94
      src/ImageSharp/Formats/Tiff/Streams/TiffStream.cs
  7. 80
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  8. 27
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  9. 5
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  10. 16
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  11. 93
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  12. 200
      src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs
  13. 26
      src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs
  14. 13
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  15. 354
      src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
  16. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs
  17. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
  18. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Number.cs
  19. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs
  20. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Short.cs
  21. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ShortArray.cs
  22. 50
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs
  23. 4
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs
  24. 17
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  25. 9
      tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs
  26. 15
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs
  27. 1
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs
  28. 1
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs
  29. 1
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs
  30. 4
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs
  31. 4
      tests/ImageSharp.Tests/TestImages.cs
  32. 3
      tests/Images/Input/Tiff/moy.tiff

2
src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants
/// <summary>
/// Enumeration representing the sub-file types defined by the Tiff file-format.
/// </summary>
public enum TiffSubfileType : uint
public enum TiffSubfileType : ushort
{
/// <summary>
/// Full-resolution image data.

108
src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs

@ -1,10 +1,10 @@
// 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.Experimental.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
@ -14,92 +14,82 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
internal class DirectoryReader
{
private readonly TiffStream stream;
private readonly EntryReader tagReader;
private readonly Stream stream;
private uint nextIfdOffset;
public DirectoryReader(TiffStream stream)
// 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 SortedList<uint, Action>(new DuplicateKeyComparer<uint>());
public DirectoryReader(Stream stream) => this.stream = stream;
public ByteOrder ByteOrder { get; private set; }
public IEnumerable<ExifProfile> Read()
{
this.stream = stream;
this.tagReader = new EntryReader(stream);
this.ByteOrder = ReadByteOrder(this.stream);
this.nextIfdOffset = new HeaderReader(this.stream, this.ByteOrder).ReadFileHeader();
return this.ReadIfds();
}
public IEnumerable<IExifValue[]> Read()
private static ByteOrder ReadByteOrder(Stream stream)
{
if (this.ReadHeader())
var headerBytes = new byte[2];
stream.Read(headerBytes, 0, 2);
if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian)
{
return this.ReadIfds();
return ByteOrder.LittleEndian;
}
else if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian)
{
return ByteOrder.BigEndian;
}
return null;
throw TiffThrowHelper.InvalidHeader();
}
private bool ReadHeader()
private IEnumerable<ExifProfile> ReadIfds()
{
ushort magic = this.stream.ReadUInt16();
if (magic != TiffConstants.HeaderMagicNumber)
var readers = new List<EntryReader>();
while (this.nextIfdOffset != 0 && this.nextIfdOffset < this.stream.Length)
{
TiffThrowHelper.ThrowInvalidHeader();
var reader = new EntryReader(this.stream, this.ByteOrder, this.nextIfdOffset, this.lazyLoaders);
reader.ReadTags();
this.nextIfdOffset = reader.NextIfdOffset;
readers.Add(reader);
}
uint firstIfdOffset = this.stream.ReadUInt32();
if (firstIfdOffset == 0)
// sequential reading big values
foreach (Action loader in this.lazyLoaders.Values)
{
TiffThrowHelper.ThrowInvalidHeader();
loader();
}
this.nextIfdOffset = firstIfdOffset;
return true;
}
private IEnumerable<IExifValue[]> ReadIfds()
{
var list = new List<IExifValue[]>();
while (this.nextIfdOffset != 0)
var list = new List<ExifProfile>();
foreach (EntryReader reader in readers)
{
this.stream.Seek(this.nextIfdOffset);
IExifValue[] ifd = this.ReadIfd();
list.Add(ifd);
var profile = new ExifProfile(reader.Values, reader.InvalidTags);
list.Add(profile);
}
this.tagReader.LoadExtendedData();
return list;
}
private IExifValue[] ReadIfd()
/// <summary><see cref="DuplicateKeyComparer{TKey}"/> used for possiblity 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
{
long pos = this.stream.Position;
ushort entryCount = this.stream.ReadUInt16();
var entries = new List<IExifValue>(entryCount);
for (int i = 0; i < entryCount; i++)
public int Compare(TKey x, TKey y)
{
IExifValue tag = this.tagReader.ReadNext();
if (tag != null)
{
entries.Add(tag);
}
}
int result = x.CompareTo(y);
this.nextIfdOffset = this.stream.ReadUInt32();
int ifdSize = 2 + (entryCount * TiffConstants.SizeOfIfdEntry) + 4;
int readedBytes = (int)(this.stream.Position - pos);
int leftBytes = ifdSize - readedBytes;
if (leftBytes > 0)
{
this.stream.Skip(leftBytes);
// Handle equality as beeing greater
return (result == 0) ? 1 : result;
}
else if (leftBytes < 0)
{
TiffThrowHelper.ThrowOutOfRange("IFD");
}
return entries.ToArray();
}
}
}

316
src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs

@ -3,317 +3,63 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
internal class EntryReader
internal class EntryReader : BaseExifReader
{
private readonly TiffStream stream;
private readonly uint startOffset;
private readonly SortedDictionary<uint, Action> extValueLoaders = new SortedDictionary<uint, Action>();
private readonly SortedList<uint, Action> lazyLoaders;
/// <summary>
/// Initializes a new instance of the <see cref="EntryReader" /> class.
/// </summary>
/// <param name="stream">The stream.</param>
public EntryReader(TiffStream stream)
public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList<uint, Action> lazyLoaders)
: base(stream)
{
this.stream = stream;
this.IsBigEndian = byteOrder == ByteOrder.BigEndian;
this.startOffset = ifdOffset;
this.lazyLoaders = lazyLoaders;
}
public IExifValue ReadNext()
{
var tagId = (ExifTagValue)this.stream.ReadUInt16();
ExifDataType dataType = EnumUtils.Parse(this.stream.ReadUInt16(), ExifDataType.Unknown);
uint count = this.stream.ReadUInt32();
ExifDataType rawDataType = dataType;
dataType = LongOrShortFiltering(tagId, dataType);
bool isArray = GetIsArray(tagId, count);
ExifValue entry = ExifValues.Create(tagId, dataType, isArray);
if (rawDataType == ExifDataType.Undefined && count == 0)
{
// todo: investgate
count = 4;
}
if (this.ReadValueOrOffset(entry, rawDataType, count))
{
return entry;
}
return null; // new UnkownExifTag(tagId);
}
public List<IExifValue> Values { get; } = new List<IExifValue>();
public void LoadExtendedData()
{
foreach (Action action in this.extValueLoaders.Values)
{
action();
}
}
public uint NextIfdOffset { get; private set; }
private static bool HasExtData(ExifValue tag, uint count) => ExifDataTypes.GetSize(tag.DataType) * count > 4;
private static bool SetValue(ExifValue entry, object value)
public void ReadTags()
{
if (!entry.IsArray && entry.DataType != ExifDataType.Ascii)
{
DebugGuard.IsTrue(((Array)value).Length == 1, "Expected a length is 1");
var single = ((Array)value).GetValue(0);
return entry.TrySetValue(single);
}
this.ReadValues(this.Values, this.startOffset);
this.NextIfdOffset = this.ReadUInt32();
return entry.TrySetValue(value);
this.ReadSubIfd(this.Values);
}
private static ExifDataType LongOrShortFiltering(ExifTagValue tagId, ExifDataType dataType)
{
switch (tagId)
{
case ExifTagValue.ImageWidth:
case ExifTagValue.ImageLength:
case ExifTagValue.StripOffsets:
case ExifTagValue.RowsPerStrip:
case ExifTagValue.StripByteCounts:
case ExifTagValue.TileWidth:
case ExifTagValue.TileLength:
case ExifTagValue.TileOffsets:
case ExifTagValue.TileByteCounts:
case ExifTagValue.OldSubfileType: // by spec SHORT, but can be LONG
return ExifDataType.Long;
default:
return dataType;
}
}
protected override void RegisterExtLoader(uint offset, Action reader) =>
this.lazyLoaders.Add(offset, reader);
}
private static bool GetIsArray(ExifTagValue tagId, uint count)
{
switch (tagId)
{
case ExifTagValue.BitsPerSample:
case ExifTagValue.StripOffsets:
case ExifTagValue.StripByteCounts:
case ExifTagValue.TileOffsets:
case ExifTagValue.TileByteCounts:
case ExifTagValue.ColorMap:
case ExifTagValue.ExtraSamples:
case ExifTagValue.SampleFormat:
return true;
internal class HeaderReader : BaseExifReader
{
public HeaderReader(Stream stream, ByteOrder byteOrder)
: base(stream) =>
this.IsBigEndian = byteOrder == ByteOrder.BigEndian;
default:
return count > 1;
}
}
public uint FirstIfdOffset { get; private set; }
private bool ReadValueOrOffset(ExifValue entry, ExifDataType rawDataType, uint count)
public uint ReadFileHeader()
{
if (entry.Tag == ExifTag.SubIFDOffset || entry.Tag == ExifTag.GPSIFDOffset /*|| entry.Tag == ExifTagValue.SubIFDs*/)
ushort magic = this.ReadUInt16();
if (magic != TiffConstants.HeaderMagicNumber)
{
// todo: ignore subIfds (exif, gps)
this.stream.Skip(4);
return false;
TiffThrowHelper.ThrowInvalidHeader();
}
if (HasExtData(entry, count))
{
uint offset = this.stream.ReadUInt32();
this.extValueLoaders.Add(offset, () =>
{
this.ReadExtValue(entry, rawDataType, offset, count);
});
return true;
}
long pos = this.stream.Position;
object value = this.ReadData(entry.DataType, rawDataType, count);
if (value == null)
{
// read unknown type value
value = this.stream.ReadBytes(4);
}
else
{
int leftBytes = 4 - (int)(this.stream.Position - pos);
if (leftBytes > 0)
{
this.stream.Skip(leftBytes);
}
else if (leftBytes < 0)
{
TiffThrowHelper.ThrowOutOfRange("IFD entry");
}
}
return SetValue(entry, value);
this.FirstIfdOffset = this.ReadUInt32();
return this.FirstIfdOffset;
}
private void ReadExtValue(ExifValue entry, ExifDataType rawDataType, uint offset, uint count)
{
DebugGuard.IsTrue(HasExtData(entry, count), "Excepted extended data");
DebugGuard.MustBeGreaterThanOrEqualTo(offset, (uint)TiffConstants.SizeOfTiffHeader, nameof(offset));
this.stream.Seek(offset);
var value = this.ReadData(entry.DataType, rawDataType, count);
SetValue(entry, value);
DebugGuard.IsTrue(entry.DataType == ExifDataType.Ascii || count > 1 ^ !entry.IsArray, "Invalid tag");
DebugGuard.IsTrue(entry.GetValue() != null, "Invalid tag");
}
private object ReadData(ExifDataType entryDataType, ExifDataType rawDataType, uint count)
{
switch (rawDataType)
{
case ExifDataType.Byte:
case ExifDataType.Undefined:
{
return this.stream.ReadBytes(count);
}
case ExifDataType.SignedByte:
{
sbyte[] res = new sbyte[count];
byte[] buf = this.stream.ReadBytes(count);
Array.Copy(buf, res, buf.Length);
return res;
}
case ExifDataType.Short:
{
if (entryDataType == ExifDataType.Long)
{
uint[] buf = new uint[count];
for (int i = 0; i < buf.Length; i++)
{
buf[i] = this.stream.ReadUInt16();
}
return buf;
}
else
{
ushort[] buf = new ushort[count];
for (int i = 0; i < buf.Length; i++)
{
buf[i] = this.stream.ReadUInt16();
}
return buf;
}
}
case ExifDataType.SignedShort:
{
short[] buf = new short[count];
for (int i = 0; i < buf.Length; i++)
{
buf[i] = this.stream.ReadInt16();
}
return buf;
}
case ExifDataType.Long:
{
uint[] buf = new uint[count];
for (int i = 0; i < buf.Length; i++)
{
buf[i] = this.stream.ReadUInt32();
}
return buf;
}
case ExifDataType.SignedLong:
{
int[] buf = new int[count];
for (int i = 0; i < buf.Length; i++)
{
buf[i] = this.stream.ReadInt32();
}
return buf;
}
case ExifDataType.Ascii:
{
byte[] buf = this.stream.ReadBytes(count);
if (buf[buf.Length - 1] != 0)
{
TiffThrowHelper.ThrowBadStringEntry();
}
return Encoding.UTF8.GetString(buf, 0, buf.Length - 1);
}
case ExifDataType.SingleFloat:
{
float[] buf = new float[count];
for (int i = 0; i < buf.Length; i++)
{
buf[i] = this.stream.ReadSingle();
}
return buf;
}
case ExifDataType.DoubleFloat:
{
double[] buf = new double[count];
for (int i = 0; i < buf.Length; i++)
{
buf[i] = this.stream.ReadDouble();
}
return buf;
}
case ExifDataType.Rational:
{
var buf = new Rational[count];
for (int i = 0; i < buf.Length; i++)
{
uint numerator = this.stream.ReadUInt32();
uint denominator = this.stream.ReadUInt32();
buf[i] = new Rational(numerator, denominator);
}
return buf;
}
case ExifDataType.SignedRational:
{
var buf = new SignedRational[count];
for (int i = 0; i < buf.Length; i++)
{
int numerator = this.stream.ReadInt32();
int denominator = this.stream.ReadInt32();
buf[i] = new SignedRational(numerator, denominator);
}
return buf;
}
case ExifDataType.Ifd:
{
return this.stream.ReadUInt32();
}
default:
return null;
}
}
protected override void RegisterExtLoader(uint offset, Action reader) => throw new NotSupportedException();
}
}

88
src/ImageSharp/Formats/Tiff/Streams/TiffBigEndianStream.cs

@ -1,88 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams
{
internal class TiffBigEndianStream : TiffStream
{
public TiffBigEndianStream(Stream stream)
: base(stream)
{
}
public override ByteOrder ByteOrder => ByteOrder.BigEndian;
/// <summary>
/// Converts buffer data into an <see cref="short"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override short ReadInt16()
{
byte[] bytes = this.ReadBytes(2);
return (short)((bytes[0] << 8) | bytes[1]);
}
/// <summary>
/// Converts buffer data into an <see cref="int"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override int ReadInt32()
{
byte[] bytes = this.ReadBytes(4);
return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
}
/// <summary>
/// Converts buffer data into a <see cref="uint"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override uint ReadUInt32()
{
return (uint)this.ReadInt32();
}
/// <summary>
/// Converts buffer data into a <see cref="ushort"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override ushort ReadUInt16()
{
return (ushort)this.ReadInt16();
}
/// <summary>
/// Converts buffer data into a <see cref="float"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override float ReadSingle()
{
byte[] bytes = this.ReadBytes(4);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
return BitConverter.ToSingle(bytes, 0);
}
/// <summary>
/// Converts buffer data into a <see cref="double"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override double ReadDouble()
{
byte[] bytes = this.ReadBytes(8);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
return BitConverter.ToDouble(bytes, 0);
}
}
}

88
src/ImageSharp/Formats/Tiff/Streams/TiffLittleEndianStream.cs

@ -1,88 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams
{
internal class TiffLittleEndianStream : TiffStream
{
public TiffLittleEndianStream(Stream stream)
: base(stream)
{
}
public override ByteOrder ByteOrder => ByteOrder.LittleEndian;
/// <summary>
/// Converts buffer data into an <see cref="short"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override short ReadInt16()
{
byte[] bytes = this.ReadBytes(2);
return (short)(bytes[0] | (bytes[1] << 8));
}
/// <summary>
/// Converts buffer data into an <see cref="int"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override int ReadInt32()
{
byte[] bytes = this.ReadBytes(4);
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
}
/// <summary>
/// Converts buffer data into a <see cref="uint"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override uint ReadUInt32()
{
return (uint)this.ReadInt32();
}
/// <summary>
/// Converts buffer data into a <see cref="ushort"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override ushort ReadUInt16()
{
return (ushort)this.ReadInt16();
}
/// <summary>
/// Converts buffer data into a <see cref="float"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override float ReadSingle()
{
byte[] bytes = this.ReadBytes(4);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
return BitConverter.ToSingle(bytes, 0);
}
/// <summary>
/// Converts buffer data into a <see cref="double"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public override double ReadDouble()
{
byte[] bytes = this.ReadBytes(8);
if (!BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
return BitConverter.ToDouble(bytes, 0);
}
}
}

94
src/ImageSharp/Formats/Tiff/Streams/TiffStream.cs

@ -1,94 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams
{
/// <summary>
/// The tiff data stream base class.
/// </summary>
internal abstract class TiffStream
{
/// <summary>
/// The input stream.
/// </summary>
private readonly Stream stream;
/// <summary>
/// Initializes a new instance of the <see cref="TiffStream"/> class.
/// </summary>
/// <param name="stream">The stream.</param>
protected TiffStream(Stream stream)
{
this.stream = stream;
}
/// <summary>
/// Gets a value indicating whether the file is encoded in little-endian or big-endian format.
/// </summary>
public abstract ByteOrder ByteOrder { get; }
/// <summary>
/// Gets the input stream.
/// </summary>
public Stream InputStream => this.stream;
/// <summary>
/// Gets the stream position.
/// </summary>
public long Position => this.stream.Position;
public void Seek(uint offset)
{
this.stream.Seek(offset, SeekOrigin.Begin);
}
public void Skip(uint offset)
{
this.stream.Seek(offset, SeekOrigin.Current);
}
public void Skip(int offset)
{
this.stream.Seek(offset, SeekOrigin.Current);
}
/// <summary>
/// Converts buffer data into a <see cref="byte"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public byte ReadByte()
{
return (byte)this.stream.ReadByte();
}
/// <summary>
/// Converts buffer data into an <see cref="sbyte"/> using the correct endianness.
/// </summary>
/// <returns>The converted value.</returns>
public sbyte ReadSByte()
{
return (sbyte)this.stream.ReadByte();
}
public byte[] ReadBytes(uint count)
{
byte[] buf = new byte[count];
this.stream.Read(buf, 0, buf.Length);
return buf;
}
public abstract short ReadInt16();
public abstract int ReadInt32();
public abstract uint ReadUInt32();
public abstract ushort ReadUInt16();
public abstract float ReadSingle();
public abstract double ReadDouble();
}
}

80
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -2,12 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using System.IO;
using System.Threading;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Streams;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
@ -50,9 +48,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
options ??= new TiffDecoder();
this.configuration = configuration ?? Configuration.Default;
this.Configuration = configuration ?? Configuration.Default;
this.ignoreMetadata = options.IgnoreMetadata;
this.memoryAllocator = this.configuration.MemoryAllocator;
this.memoryAllocator = this.Configuration.MemoryAllocator;
}
/// <summary>
@ -101,7 +99,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public TiffPredictor Predictor { get; set; }
/// <inheritdoc/>
public Configuration Configuration => this.configuration;
public Configuration Configuration { get; }
/// <inheritdoc/>
public Size Dimensions { get; private set; }
@ -111,21 +109,20 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
where TPixel : unmanaged, IPixel<TPixel>
{
this.inputStream = stream;
TiffStream tiffStream = CreateStream(stream);
var reader = new DirectoryReader(tiffStream);
var reader = new DirectoryReader(stream);
IEnumerable<IExifValue[]> directories = reader.Read();
IEnumerable<ExifProfile> directories = reader.Read();
var frames = new List<ImageFrame<TPixel>>();
var framesMetadata = new List<TiffFrameMetadata>();
foreach (IExifValue[] ifd in directories)
foreach (ExifProfile ifd in directories)
{
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, out TiffFrameMetadata frameMetadata);
frames.Add(frame);
framesMetadata.Add(frameMetadata);
}
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, tiffStream.ByteOrder);
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, reader.ByteOrder);
// todo: tiff frames can have different sizes
{
@ -140,7 +137,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
}
var image = new Image<TPixel>(this.configuration, metadata, frames);
var image = new Image<TPixel>(this.Configuration, metadata, frames);
return image;
}
@ -149,56 +146,23 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.inputStream = stream;
TiffStream tiffStream = CreateStream(stream);
var reader = new DirectoryReader(tiffStream);
var reader = new DirectoryReader(stream);
IEnumerable<IExifValue[]> directories = reader.Read();
IEnumerable<ExifProfile> directories = reader.Read();
var framesMetadata = new List<TiffFrameMetadata>();
foreach (IExifValue[] ifd in directories)
foreach (ExifProfile ifd in directories)
{
var meta = new TiffFrameMetadata();
meta.FrameTags.AddRange(ifd);
var meta = new TiffFrameMetadata() { ExifProfile = ifd };
framesMetadata.Add(meta);
}
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, tiffStream.ByteOrder);
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, reader.ByteOrder);
TiffFrameMetadata root = framesMetadata[0];
return new ImageInfo(new PixelTypeInfo(root.BitsPerPixel), (int)root.Width, (int)root.Height, metadata);
}
private static TiffStream CreateStream(Stream stream)
{
ByteOrder byteOrder = ReadByteOrder(stream);
if (byteOrder == ByteOrder.BigEndian)
{
return new TiffBigEndianStream(stream);
}
else if (byteOrder == ByteOrder.LittleEndian)
{
return new TiffLittleEndianStream(stream);
}
throw TiffThrowHelper.InvalidHeader();
}
private static ByteOrder ReadByteOrder(Stream stream)
{
var headerBytes = new byte[2];
stream.Read(headerBytes, 0, 2);
if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian)
{
return ByteOrder.LittleEndian;
}
else if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian)
{
return ByteOrder.BigEndian;
}
throw TiffThrowHelper.InvalidHeader();
}
/// <summary>
/// Decodes the image data from a specified IFD.
/// </summary>
@ -208,22 +172,22 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <returns>
/// The tiff frame.
/// </returns>
private ImageFrame<TPixel> DecodeFrame<TPixel>(IExifValue[] tags, out TiffFrameMetadata frameMetaData)
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, out TiffFrameMetadata frameMetaData)
where TPixel : unmanaged, IPixel<TPixel>
{
var coreMetadata = new ImageFrameMetadata();
frameMetaData = coreMetadata.GetTiffMetadata();
frameMetaData.FrameTags.AddRange(tags);
frameMetaData.ExifProfile = tags;
this.VerifyAndParse(frameMetaData);
int width = (int)frameMetaData.Width;
int height = (int)frameMetaData.Height;
var frame = new ImageFrame<TPixel>(this.configuration, width, height, coreMetadata);
var frame = new ImageFrame<TPixel>(this.Configuration, width, height, coreMetadata);
int rowsPerStrip = (int)frameMetaData.RowsPerStrip;
uint[] stripOffsets = frameMetaData.StripOffsets;
uint[] stripByteCounts = frameMetaData.StripByteCounts;
Number[] stripOffsets = frameMetaData.StripOffsets;
Number[] stripByteCounts = frameMetaData.StripByteCounts;
if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar)
{
@ -273,7 +237,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.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="width">The image width.</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width)
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, int width)
where TPixel : unmanaged, IPixel<TPixel>
{
int stripsPerPixel = this.BitsPerSample.Length;
@ -304,7 +268,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
int stripIndex = (i * stripsPerPixel) + planeIndex;
decompressor.Decompress(this.inputStream, stripOffsets[stripIndex], stripByteCounts[stripIndex], stripBuffers[planeIndex].GetSpan());
decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffers[planeIndex].GetSpan());
}
colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight);
@ -319,7 +283,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
}
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts, int width)
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, int width)
where TPixel : unmanaged, IPixel<TPixel>
{
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
@ -337,7 +301,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip;
decompressor.Decompress(this.inputStream, stripOffsets[stripIndex], stripByteCounts[stripIndex], stripBuffer.GetSpan());
decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffer.GetSpan());
colorDecoder.Decode(stripBuffer.GetSpan(), pixels, 0, rowsPerStrip * stripIndex, frame.Width, stripHeight);
}

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

@ -47,37 +47,28 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
if (tiffMetadata.XmpProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.XMP, true);
if (buf != null)
IExifValue<byte[]> val = frame.ExifProfile.GetValue<byte[]>(ExifTag.XMP);
if (val != null)
{
tiffMetadata.XmpProfile = buf;
}
}
if (coreMetadata.ExifProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.SubIFDOffset, true);
if (buf != null)
{
coreMetadata.ExifProfile = new ExifProfile(buf);
tiffMetadata.XmpProfile = val.Value;
}
}
if (coreMetadata.IptcProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.IPTC, true);
if (buf != null)
IExifValue<byte[]> val = frame.ExifProfile.GetValue<byte[]>(ExifTag.IPTC);
if (val != null)
{
coreMetadata.IptcProfile = new IptcProfile(buf);
coreMetadata.IptcProfile = new IptcProfile(val.Value);
}
}
if (coreMetadata.IccProfile == null)
{
byte[] buf = frame.GetArray<byte>(ExifTag.IccProfile, true);
if (buf != null)
IExifValue<byte[]> val = frame.ExifProfile.GetValue<byte[]>(ExifTag.IccProfile);
if (val != null)
{
coreMetadata.IccProfile = new IccProfile(buf);
coreMetadata.IccProfile = new IccProfile(val.Value);
}
}
}

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

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -30,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
TiffThrowHelper.ThrowNotSupported("The lower-order bits of the byte FillOrder is not supported.");
}
if (entries.GetArray<uint>(ExifTag.TileOffsets, true) != null)
if (entries.ExifProfile.GetValue<uint[]>(ExifTag.TileOffsets) != null)
{
TiffThrowHelper.ThrowNotSupported("The Tile images is not supported.");
}
@ -242,7 +241,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
case TiffCompression.CcittGroup3Fax:
{
options.CompressionType = TiffDecoderCompressionType.T4;
IExifValue t4options = entries.FrameTags.Find(tag => tag.Tag == ExifTag.T4Options);
IExifValue t4options = entries.ExifProfile.GetValue(ExifTag.T4Options);
if (t4options != null)
{
var t4OptionValue = (FaxCompressionOptions)t4options.GetValue();

16
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -114,17 +114,17 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
var xResolution = new ExifRational(ExifTagValue.XResolution)
{
Value = frameMetadata.GetSingle<Rational>(ExifTag.XResolution)
Value = frameMetadata.ExifProfile.GetValue<Rational>(ExifTag.XResolution).Value
};
var yResolution = new ExifRational(ExifTagValue.YResolution)
{
Value = frameMetadata.GetSingle<Rational>(ExifTag.YResolution)
Value = frameMetadata.ExifProfile.GetValue<Rational>(ExifTag.YResolution).Value
};
var resolutionUnit = new ExifShort(ExifTagValue.ResolutionUnit)
{
Value = frameMetadata.GetSingle<ushort>(ExifTag.ResolutionUnit)
Value = frameMetadata.ExifProfile.GetValue<ushort>(ExifTag.ResolutionUnit).Value
};
this.collector.AddInternal(xResolution);
@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
private void ProcessMetadata(TiffFrameMetadata frameMetadata)
{
foreach (IExifValue entry in frameMetadata.FrameTags)
foreach (IExifValue entry in frameMetadata.ExifProfile.Values)
{
// todo: skip subIfd
if (entry.DataType == ExifDataType.Ifd)
@ -183,7 +183,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
else
{
tiffFrameMetadata.Remove(ExifTag.SubIFDOffset);
tiffFrameMetadata.ExifProfile.RemoveValue(ExifTag.SubIFDOffset);
}
if (imageMetadata.IptcProfile != null)
@ -198,7 +198,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
else
{
tiffFrameMetadata.Remove(ExifTag.IPTC);
tiffFrameMetadata.ExifProfile.RemoveValue(ExifTag.IPTC);
}
if (imageMetadata.IccProfile != null)
@ -212,7 +212,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
else
{
tiffFrameMetadata.Remove(ExifTag.IccProfile);
tiffFrameMetadata.ExifProfile.RemoveValue(ExifTag.IccProfile);
}
TiffMetadata tiffMetadata = imageMetadata.GetTiffMetadata();
@ -227,7 +227,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
else
{
tiffFrameMetadata.Remove(ExifTag.XMP);
tiffFrameMetadata.ExifProfile.RemoveValue(ExifTag.XMP);
}
}
}

93
src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -21,6 +21,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
private const TiffPredictor DefaultPredictor = TiffPredictor.None;
private ExifProfile frameTags;
/// <summary>
/// Initializes a new instance of the <see cref="TiffFrameMetadata"/> class.
/// </summary>
@ -29,27 +31,31 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
/// <summary>
/// Gets the Tiff directory tags list.
/// Gets the Tiff directory tags.
/// </summary>
public List<IExifValue> FrameTags { get; internal set; } = new List<IExifValue>();
public ExifProfile ExifProfile
{
get => this.frameTags ??= new ExifProfile();
internal set => this.frameTags = value;
}
/// <summary>Gets a general indication of the kind of data contained in this subfile.</summary>
/// <value>A general indication of the kind of data contained in this subfile.</value>
public TiffNewSubfileType NewSubfileType => this.GetSingleEnum<TiffNewSubfileType, uint>(ExifTag.SubfileType, TiffNewSubfileType.FullImage);
public TiffNewSubfileType SubfileType => (TiffNewSubfileType?)this.ExifProfile.GetValue<uint>(ExifTag.SubfileType)?.Value ?? TiffNewSubfileType.FullImage;
/// <summary>Gets a general indication of the kind of data contained in this subfile.</summary>
/// <value>A general indication of the kind of data contained in this subfile.</value>
public TiffSubfileType? SubfileType => this.GetSingleEnumNullable<TiffSubfileType, uint>(ExifTag.OldSubfileType);
public TiffSubfileType? OldSubfileType => (TiffSubfileType?)this.ExifProfile.GetValue<ushort>(ExifTag.OldSubfileType)?.Value;
/// <summary>
/// Gets the number of columns in the image, i.e., the number of pixels per row.
/// </summary>
public uint Width => this.GetSingle<uint>(ExifTag.ImageWidth);
public Number Width => this.ExifProfile.GetValue<Number>(ExifTag.ImageWidth).Value;
/// <summary>
/// Gets the number of rows of pixels in the image.
/// </summary>
public uint Height => this.GetSingle<uint>(ExifTag.ImageLength);
public Number Height => this.ExifProfile.GetValue<Number>(ExifTag.ImageLength).Value;
/// <summary>
/// Gets the number of bits per component.
@ -58,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
get
{
var bits = this.GetArray<ushort>(ExifTag.BitsPerSample, true);
var bits = this.ExifProfile.GetValue<ushort[]>(ExifTag.BitsPerSample)?.Value;
if (bits == null)
{
if (this.PhotometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero
@ -92,25 +98,25 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <summary>Gets the compression scheme used on the image data.</summary>
/// <value>The compression scheme used on the image data.</value>
public TiffCompression Compression => this.GetSingleEnum<TiffCompression, ushort>(ExifTag.Compression);
public TiffCompression Compression => (TiffCompression)this.ExifProfile.GetValue<ushort>(ExifTag.Compression).Value;
/// <summary>
/// Gets the color space of the image data.
/// </summary>
public TiffPhotometricInterpretation PhotometricInterpretation => this.GetSingleEnum<TiffPhotometricInterpretation, ushort>(ExifTag.PhotometricInterpretation);
public TiffPhotometricInterpretation PhotometricInterpretation => (TiffPhotometricInterpretation)this.ExifProfile.GetValue<ushort>(ExifTag.PhotometricInterpretation).Value;
/// <summary>
/// Gets the logical order of bits within a byte.
/// </summary>
internal TiffFillOrder FillOrder => this.GetSingleEnum<TiffFillOrder, ushort>(ExifTag.FillOrder, TiffFillOrder.MostSignificantBitFirst);
internal TiffFillOrder FillOrder => (TiffFillOrder?)this.ExifProfile.GetValue<ushort>(ExifTag.FillOrder)?.Value ?? TiffFillOrder.MostSignificantBitFirst;
/// <summary>
/// Gets or sets the a string that describes the subject of the image.
/// </summary>
public string ImageDescription
{
get => this.GetString(ExifTag.ImageDescription);
set => this.SetString(ExifTag.ImageDescription, value);
get => this.ExifProfile.GetValue(ExifTag.ImageDescription)?.Value;
set => this.ExifProfile.SetValue(ExifTag.ImageDescription, value);
}
/// <summary>
@ -118,8 +124,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public string Make
{
get => this.GetString(ExifTag.Make);
set => this.SetString(ExifTag.Make, value);
get => this.ExifProfile.GetValue(ExifTag.Make)?.Value;
set => this.ExifProfile.SetValue(ExifTag.Make, value);
}
/// <summary>
@ -127,27 +133,27 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public string Model
{
get => this.GetString(ExifTag.Model);
set => this.SetString(ExifTag.Model, value);
get => this.ExifProfile.GetValue(ExifTag.Model)?.Value;
set => this.ExifProfile.SetValue(ExifTag.Model, value);
}
/// <summary>Gets for each strip, the byte offset of that strip..</summary>
public uint[] StripOffsets => this.GetArray<uint>(ExifTag.StripOffsets);
public Number[] StripOffsets => this.ExifProfile.GetValue<Number[]>(ExifTag.StripOffsets).Value;
/// <summary>
/// Gets the number of components per pixel.
/// </summary>
public ushort SamplesPerPixel => this.GetSingle<ushort>(ExifTag.SamplesPerPixel);
public ushort SamplesPerPixel => this.ExifProfile.GetValue<ushort>(ExifTag.SamplesPerPixel).Value;
/// <summary>
/// Gets the number of rows per strip.
/// </summary>
public uint RowsPerStrip => this.GetSingle<uint>(ExifTag.RowsPerStrip);
public Number RowsPerStrip => this.ExifProfile.GetValue<Number>(ExifTag.RowsPerStrip).Value;
/// <summary>
/// Gets for each strip, the number of bytes in the strip after compression.
/// </summary>
public uint[] StripByteCounts => this.GetArray<uint>(ExifTag.StripByteCounts);
public Number[] StripByteCounts => this.ExifProfile.GetValue<Number[]>(ExifTag.StripByteCounts).Value;
/// <summary>Gets the resolution of the image in x- direction.</summary>
/// <value>The density of the image in x- direction.</value>
@ -162,7 +168,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <summary>
/// Gets how the components of each pixel are stored.
/// </summary>
public TiffPlanarConfiguration PlanarConfiguration => this.GetSingleEnum<TiffPlanarConfiguration, ushort>(ExifTag.PlanarConfiguration, DefaultPlanarConfiguration);
public TiffPlanarConfiguration PlanarConfiguration => (TiffPlanarConfiguration?)this.ExifProfile.GetValue<ushort>(ExifTag.PlanarConfiguration)?.Value ?? DefaultPlanarConfiguration;
/// <summary>
/// Gets the unit of measurement for XResolution and YResolution.
@ -174,8 +180,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public string Software
{
get => this.GetString(ExifTag.Software);
set => this.SetString(ExifTag.Software, value);
get => this.ExifProfile.GetValue(ExifTag.Software)?.Value;
set => this.ExifProfile.SetValue(ExifTag.Software, value);
}
/// <summary>
@ -183,8 +189,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public string DateTime
{
get => this.GetString(ExifTag.DateTime);
set => this.SetString(ExifTag.DateTime, value);
get => this.ExifProfile.GetValue(ExifTag.DateTime)?.Value;
set => this.ExifProfile.SetValue(ExifTag.DateTime, value);
}
/// <summary>
@ -192,8 +198,8 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public string Artist
{
get => this.GetString(ExifTag.Artist);
set => this.SetString(ExifTag.Artist, value);
get => this.ExifProfile.GetValue(ExifTag.Artist)?.Value;
set => this.ExifProfile.SetValue(ExifTag.Artist, value);
}
/// <summary>
@ -201,39 +207,39 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
public string HostComputer
{
get => this.GetString(ExifTag.HostComputer);
set => this.SetString(ExifTag.HostComputer, value);
get => this.ExifProfile.GetValue(ExifTag.HostComputer)?.Value;
set => this.ExifProfile.SetValue(ExifTag.HostComputer, value);
}
/// <summary>
/// Gets a color map for palette color images.
/// </summary>
public ushort[] ColorMap => this.GetArray<ushort>(ExifTag.ColorMap, true);
public ushort[] ColorMap => this.ExifProfile.GetValue<ushort[]>(ExifTag.ColorMap)?.Value;
/// <summary>
/// Gets the description of extra components.
/// </summary>
public ushort[] ExtraSamples => this.GetArray<ushort>(ExifTag.ExtraSamples, true);
public ushort[] ExtraSamples => this.ExifProfile.GetValue<ushort[]>(ExifTag.ExtraSamples)?.Value;
/// <summary>
/// Gets or sets the copyright notice.
/// </summary>
public string Copyright
{
get => this.GetString(ExifTag.Copyright);
set => this.SetString(ExifTag.Copyright, value);
get => this.ExifProfile.GetValue(ExifTag.Copyright)?.Value;
set => this.ExifProfile.SetValue(ExifTag.Copyright, value);
}
/// <summary>
/// Gets a mathematical operator that is applied to the image data before an encoding scheme is applied.
/// </summary>
public TiffPredictor Predictor => this.GetSingleEnum<TiffPredictor, ushort>(ExifTag.Predictor, DefaultPredictor);
public TiffPredictor Predictor => (TiffPredictor?)this.ExifProfile.GetValue<ushort>(ExifTag.Predictor)?.Value ?? DefaultPredictor;
/// <summary>
/// Gets the specifies how to interpret each data sample in a pixel.
/// <see cref="SamplesPerPixel"/>
/// </summary>
public TiffSampleFormat[] SampleFormat => this.GetEnumArray<TiffSampleFormat, ushort>(ExifTag.SampleFormat, true);
public TiffSampleFormat[] SampleFormat => this.ExifProfile.GetValue<ushort[]>(ExifTag.SampleFormat)?.Value?.Select(a => (TiffSampleFormat)a).ToArray();
/// <summary>
/// Clears the metadata.
@ -241,7 +247,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public void ClearMetadata()
{
var tags = new List<IExifValue>();
foreach (IExifValue entry in this.FrameTags)
foreach (IExifValue entry in this.ExifProfile.Values)
{
switch ((ExifTagValue)(ushort)entry.Tag)
{
@ -261,19 +267,10 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
}
this.FrameTags = tags;
this.ExifProfile = new ExifProfile(tags, this.ExifProfile.InvalidTags);
}
/// <inheritdoc/>
public IDeepCloneable DeepClone()
{
var tags = new List<IExifValue>();
foreach (IExifValue entry in this.FrameTags)
{
tags.Add(entry.DeepClone());
}
return new TiffFrameMetadata() { FrameTags = tags };
}
public IDeepCloneable DeepClone() => new TiffFrameMetadata() { ExifProfile = this.ExifProfile.DeepClone() };
}
}

200
src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs

@ -1,200 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Linq;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
/// <summary>
/// The tiff metadata extensions
/// </summary>
internal static class TiffFrameMetadataExtensions
{
public static T[] GetArray<T>(this TiffFrameMetadata meta, ExifTag tag, bool optional = false)
where T : struct
{
if (meta.TryGetArray(tag, out T[] result))
{
return result;
}
if (!optional)
{
TiffThrowHelper.ThrowTagNotFound(nameof(tag));
}
return null;
}
public static bool TryGetArray<T>(this TiffFrameMetadata meta, ExifTag tag, out T[] result)
where T : struct
{
foreach (IExifValue entry in meta.FrameTags)
{
if (entry.Tag == tag)
{
DebugGuard.IsTrue(entry.IsArray, "Expected array entry");
result = (T[])entry.GetValue();
return true;
}
}
result = null;
return false;
}
public static TEnum[] GetEnumArray<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag, bool optional = false)
where TEnum : struct
where TTagValue : struct
{
if (meta.TryGetArray(tag, out TTagValue[] result))
{
return result.Select(a => (TEnum)(object)a).ToArray();
}
if (!optional)
{
TiffThrowHelper.ThrowTagNotFound(nameof(tag));
}
return null;
}
public static string GetString(this TiffFrameMetadata meta, ExifTag tag)
{
foreach (IExifValue entry in meta.FrameTags)
{
if (entry.Tag == tag)
{
DebugGuard.IsTrue(entry.DataType == ExifDataType.Ascii, "Expected string entry");
object value = entry.GetValue();
DebugGuard.IsTrue(value is string, "Expected string entry");
return (string)value;
}
}
return null;
}
public static bool SetString(this TiffFrameMetadata meta, ExifTag tag, string value)
{
IExifValue obj = FindOrCreate(meta, tag);
DebugGuard.IsTrue(obj.DataType == ExifDataType.Ascii, "Expected string entry");
return obj.TrySetValue(value);
}
public static TEnum? GetSingleEnumNullable<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag)
where TEnum : struct
where TTagValue : struct
{
if (!meta.TryGetSingle(tag, out TTagValue value))
{
return null;
}
return (TEnum)(object)value;
}
public static TEnum GetSingleEnum<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag, TEnum? defaultValue = null)
where TEnum : struct
where TTagValue : struct
=> meta.GetSingleEnumNullable<TEnum, TTagValue>(tag) ?? (defaultValue != null ? defaultValue.Value : throw TiffThrowHelper.TagNotFound(nameof(tag)));
public static bool SetSingleEnum<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag, TEnum value)
where TEnum : struct
where TTagValue : struct
{
IExifValue obj = FindOrCreate(meta, tag);
object val = (TTagValue)(object)value;
return obj.TrySetValue(val);
}
public static T GetSingle<T>(this TiffFrameMetadata meta, ExifTag tag)
where T : struct
{
if (meta.TryGetSingle(tag, out T result))
{
return result;
}
throw TiffThrowHelper.TagNotFound(nameof(tag));
}
public static bool TryGetSingle<T>(this TiffFrameMetadata meta, ExifTag tag, out T result)
where T : struct
{
foreach (IExifValue entry in meta.FrameTags)
{
if (entry.Tag == tag)
{
DebugGuard.IsTrue(!entry.IsArray, "Expected non array entry");
object value = entry.GetValue();
result = (T)value;
return true;
}
}
result = default;
return false;
}
public static bool SetSingle<T>(this TiffFrameMetadata meta, ExifTag tag, T value)
where T : struct
{
IExifValue obj = FindOrCreate(meta, tag);
DebugGuard.IsTrue(!obj.IsArray, "Expected non array entry");
object val = value;
return obj.TrySetValue(val);
}
public static bool Remove(this TiffFrameMetadata meta, ExifTag tag)
{
IExifValue obj = null;
foreach (IExifValue entry in meta.FrameTags)
{
if (entry.Tag == tag)
{
obj = entry;
break;
}
}
if (obj != null)
{
return meta.FrameTags.Remove(obj);
}
return false;
}
private static IExifValue FindOrCreate(TiffFrameMetadata meta, ExifTag tag)
{
IExifValue obj = null;
foreach (IExifValue entry in meta.FrameTags)
{
if (entry.Tag == tag)
{
obj = entry;
break;
}
}
if (obj == null)
{
obj = ExifValues.Create(tag);
meta.FrameTags.Add(obj);
}
return obj;
}
}
}

26
src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs

@ -19,9 +19,9 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
break;
case PixelResolutionUnit.PixelsPerMeter:
{
unit = PixelResolutionUnit.PixelsPerCentimeter;
horizontal = UnitConverter.MeterToCm(horizontal);
vertical = UnitConverter.MeterToCm(vertical);
unit = PixelResolutionUnit.PixelsPerCentimeter;
horizontal = UnitConverter.MeterToCm(horizontal);
vertical = UnitConverter.MeterToCm(vertical);
}
break;
@ -30,29 +30,27 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
break;
}
meta.SetSingle(ExifTag.ResolutionUnit, (ushort)unit + 1);
meta.ExifProfile.SetValue(ExifTag.ResolutionUnit, (ushort)(unit + 1));
meta.SetResolution(ExifTag.XResolution, horizontal);
meta.SetResolution(ExifTag.YResolution, vertical);
}
public static PixelResolutionUnit GetResolutionUnit(this TiffFrameMetadata meta)
{
if (!meta.TryGetSingle(ExifTag.ResolutionUnit, out ushort res))
{
res = TiffFrameMetadata.DefaultResolutionUnit;
}
ushort res = meta.ExifProfile.GetValue<ushort>(ExifTag.ResolutionUnit)?.Value ?? TiffFrameMetadata.DefaultResolutionUnit;
return (PixelResolutionUnit)(res - 1);
}
public static double? GetResolution(this TiffFrameMetadata meta, ExifTag tag)
public static double? GetResolution(this TiffFrameMetadata meta, ExifTag<Rational> tag)
{
if (!meta.TryGetSingle(tag, out Rational resolution))
IExifValue<Rational> resolution = meta.ExifProfile.GetValue<Rational>(tag);
if (resolution == null)
{
return null;
}
double res = resolution.ToDouble();
double res = resolution.Value.ToDouble();
switch (meta.ResolutionUnit)
{
case PixelResolutionUnit.AspectRatio:
@ -68,11 +66,11 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
}
private static void SetResolution(this TiffFrameMetadata meta, ExifTag tag, double? value)
private static void SetResolution(this TiffFrameMetadata meta, ExifTag<Rational> tag, double? value)
{
if (value == null)
{
meta.Remove(tag);
meta.ExifProfile.RemoveValue(tag);
return;
}
else
@ -94,7 +92,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
break;
}
meta.SetSingle(tag, new Rational(res));
meta.ExifProfile.SetValue(tag, new Rational(res));
}
}
}

13
src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
@ -52,6 +53,18 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
this.InvalidTags = Array.Empty<ExifTag>();
}
/// <summary>
/// Initializes a new instance of the <see cref="ExifProfile" /> class.
/// </summary>
/// <param name="values">The values.</param>
/// <param name="invalidTags">The invalid tags.</param>
internal ExifProfile(List<IExifValue> values, IReadOnlyList<ExifTag> invalidTags)
{
this.Parts = ExifParts.All;
this.values = values;
this.InvalidTags = invalidTags;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExifProfile"/> class
/// by making a copy from another EXIF profile.

354
src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs

@ -5,27 +5,103 @@ using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{
internal class ExifReader : BaseExifReader
{
private readonly List<Action> loaders = new List<Action>();
public ExifReader(byte[] exifData)
: base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData))))
{
}
/// <summary>
/// Reads and returns the collection of EXIF values.
/// </summary>
/// <returns>
/// The <see cref="Collection{ExifValue}"/>.
/// </returns>
public List<IExifValue> ReadValues()
{
var values = new List<IExifValue>();
// II == 0x4949
this.IsBigEndian = this.ReadUInt16() != 0x4949;
if (this.ReadUInt16() != 0x002A)
{
return values;
}
uint ifdOffset = this.ReadUInt32();
this.ReadValues(values, ifdOffset);
uint thumbnailOffset = this.ReadUInt32();
this.GetThumbnail(thumbnailOffset);
this.ReadSubIfd(values);
foreach (Action loader in this.loaders)
{
loader();
}
return values;
}
protected override void RegisterExtLoader(uint offset, Action loader) => this.loaders.Add(loader);
private void GetThumbnail(uint offset)
{
if (offset == 0)
{
return;
}
var values = new List<IExifValue>();
this.ReadValues(values, offset);
foreach (ExifValue value in values)
{
if (value == ExifTag.JPEGInterchangeFormat)
{
this.ThumbnailOffset = ((ExifLong)value).Value;
}
else if (value == ExifTag.JPEGInterchangeFormatLength)
{
this.ThumbnailLength = ((ExifLong)value).Value;
}
}
}
}
/// <summary>
/// Reads and parses EXIF data from a byte array.
/// Reads and parses EXIF data from a stream.
/// </summary>
internal sealed class ExifReader
internal abstract class BaseExifReader
{
private List<ExifTag> invalidTags;
private readonly byte[] exifData;
private int position;
private readonly byte[] offsetBuffer = new byte[4];
private readonly byte[] buf4 = new byte[4];
private readonly byte[] buf2 = new byte[2];
private readonly Stream data;
private bool isBigEndian;
private List<ExifTag> invalidTags;
private uint exifOffset;
private uint gpsOffset;
public ExifReader(byte[] exifData)
{
this.exifData = exifData ?? throw new ArgumentNullException(nameof(exifData));
}
protected BaseExifReader(Stream stream) =>
this.data = stream ?? throw new ArgumentNullException(nameof(stream));
private delegate TDataType ConverterMethod<TDataType>(ReadOnlySpan<byte> data);
@ -35,66 +111,55 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
public IReadOnlyList<ExifTag> InvalidTags => this.invalidTags ?? (IReadOnlyList<ExifTag>)Array.Empty<ExifTag>();
/// <summary>
/// Gets the thumbnail length in the byte stream.
/// Gets or sets the thumbnail length in the byte stream.
/// </summary>
public uint ThumbnailLength { get; private set; }
public uint ThumbnailLength { get; protected set; }
/// <summary>
/// Gets the thumbnail offset position in the byte stream.
/// Gets or sets the thumbnail offset position in the byte stream.
/// </summary>
public uint ThumbnailOffset { get; private set; }
public uint ThumbnailOffset { get; protected set; }
/// <summary>
/// Gets the remaining length.
/// </summary>
private int RemainingLength
public bool IsBigEndian
{
get
{
if (this.position >= this.exifData.Length)
{
return 0;
}
return this.exifData.Length - this.position;
}
get => this.isBigEndian;
protected set => this.isBigEndian = value;
}
protected abstract void RegisterExtLoader(uint offset, Action loader);
/// <summary>
/// Reads and returns the collection of EXIF values.
/// Reads the values to the values collection.
/// </summary>
/// <returns>
/// The <see cref="Collection{ExifValue}"/>.
/// </returns>
public List<IExifValue> ReadValues()
/// <param name="values">The values.</param>
/// <param name="offset">The IFD offset.</param>
protected void ReadValues(List<IExifValue> values, uint offset)
{
var values = new List<IExifValue>();
// II == 0x4949
this.isBigEndian = this.ReadUInt16() != 0x4949;
if (this.ReadUInt16() != 0x002A)
if (offset > this.data.Length)
{
return values;
return;
}
uint ifdOffset = this.ReadUInt32();
this.AddValues(values, ifdOffset);
this.Seek(offset);
int count = this.ReadUInt16();
uint thumbnailOffset = this.ReadUInt32();
this.GetThumbnail(thumbnailOffset);
for (int i = 0; i < count; i++)
{
this.ReadValue(values);
}
}
protected void ReadSubIfd(List<IExifValue> values)
{
if (this.exifOffset != 0)
{
this.AddValues(values, this.exifOffset);
this.ReadValues(values, this.exifOffset);
}
if (this.gpsOffset != 0)
{
this.AddValues(values, this.gpsOffset);
this.ReadValues(values, this.gpsOffset);
}
return values;
}
private static TDataType[] ToArray<TDataType>(ExifDataType dataType, ReadOnlySpan<byte> data, ConverterMethod<TDataType> converter)
@ -128,58 +193,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
return Encoding.UTF8.GetString(buffer);
}
/// <summary>
/// Adds the collection of EXIF values to the reader.
/// </summary>
/// <param name="values">The values.</param>
/// <param name="index">The index.</param>
private void AddValues(List<IExifValue> values, uint index)
{
if (index > (uint)this.exifData.Length)
{
return;
}
this.position = (int)index;
int count = this.ReadUInt16();
for (int i = 0; i < count; i++)
{
if (!this.TryReadValue(out ExifValue value))
{
continue;
}
bool duplicate = false;
foreach (IExifValue val in values)
{
if (val == value)
{
duplicate = true;
break;
}
}
if (duplicate)
{
continue;
}
if (value == ExifTag.SubIFDOffset)
{
this.exifOffset = ((ExifLong)value).Value;
}
else if (value == ExifTag.GPSIFDOffset)
{
this.gpsOffset = ((ExifLong)value).Value;
}
else
{
values.Add(value);
}
}
}
private object ConvertValue(ExifDataType dataType, ReadOnlySpan<byte> buffer, uint numberOfComponents)
{
if (buffer.Length == 0)
@ -275,28 +288,28 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
}
}
private bool TryReadValue(out ExifValue exifValue)
private void ReadValue(List<IExifValue> values)
{
exifValue = default;
// 2 | 2 | 4 | 4
// tag | type | count | value offset
if (this.RemainingLength < 12)
if ((this.data.Length - this.data.Position) < 12)
{
return false;
return;
}
var tag = (ExifTagValue)this.ReadUInt16();
ExifDataType dataType = EnumUtils.Parse(this.ReadUInt16(), ExifDataType.Unknown);
uint numberOfComponents = this.ReadUInt32();
this.TryReadSpan(this.offsetBuffer);
// Ensure that the data type is valid
if (dataType == ExifDataType.Unknown)
{
return false;
return;
}
uint numberOfComponents = this.ReadUInt32();
// Issue #132: ExifDataType == Undefined is treated like a byte array.
// If numberOfComponents == 0 this value can only be handled as an inline value and must fallback to 4 (bytes)
if (dataType == ExifDataType.Undefined && numberOfComponents == 0)
@ -304,110 +317,105 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
numberOfComponents = 4;
}
uint size = numberOfComponents * ExifDataTypes.GetSize(dataType);
ExifValue exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents);
this.TryReadSpan(4, out ReadOnlySpan<byte> offsetBuffer);
if (exifValue is null)
{
this.AddInvalidTag(new UnkownExifTag(tag));
return;
}
object value;
uint size = numberOfComponents * ExifDataTypes.GetSize(dataType);
if (size > 4)
{
int oldIndex = this.position;
uint newIndex = this.ConvertToUInt32(offsetBuffer);
uint newOffset = this.ConvertToUInt32(this.offsetBuffer);
// Ensure that the new index does not overrun the data
if (newIndex > int.MaxValue)
if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length)
{
this.AddInvalidTag(new UnkownExifTag(tag));
return false;
return;
}
this.position = (int)newIndex;
if (this.RemainingLength < size)
this.RegisterExtLoader(newOffset, () =>
{
this.AddInvalidTag(new UnkownExifTag(tag));
this.position = oldIndex;
return false;
}
this.TryReadSpan((int)size, out ReadOnlySpan<byte> dataBuffer);
value = this.ConvertValue(dataType, dataBuffer, numberOfComponents);
this.position = oldIndex;
var dataBuffer = new byte[size];
this.Seek(newOffset);
if (this.TryReadSpan(dataBuffer))
{
object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents);
this.Add(values, exifValue, value);
}
});
}
else
{
value = this.ConvertValue(dataType, offsetBuffer, numberOfComponents);
}
object value = this.ConvertValue(dataType, this.offsetBuffer, numberOfComponents);
exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents);
this.Add(values, exifValue, value);
}
}
if (exifValue is null)
private void Add(IList<IExifValue> values, IExifValue exif, object value)
{
if (!exif.TrySetValue(value))
{
this.AddInvalidTag(new UnkownExifTag(tag));
return false;
return;
}
if (!exifValue.TrySetValue(value))
foreach (IExifValue val in values)
{
return false;
// sometimes duplicates appear,
// can compare val.Tag == exif.Tag
if (val == exif)
{
Debug.WriteLine($"Duplicate Exif tag: tag={exif.Tag}, dataType={exif.DataType}");
return;
}
}
return true;
if (exif.Tag == ExifTag.SubIFDOffset)
{
this.exifOffset = (uint)value;
}
else if (exif.Tag == ExifTag.GPSIFDOffset)
{
this.gpsOffset = (uint)value;
}
else
{
values.Add(exif);
}
}
private void AddInvalidTag(ExifTag tag)
=> (this.invalidTags ?? (this.invalidTags = new List<ExifTag>())).Add(tag);
=> (this.invalidTags ??= new List<ExifTag>()).Add(tag);
private void Seek(long pos)
=> this.data.Seek(pos, SeekOrigin.Begin);
private bool TryReadSpan(int length, out ReadOnlySpan<byte> span)
private bool TryReadSpan(Span<byte> span)
{
if (this.RemainingLength < length)
int length = span.Length;
if ((this.data.Length - this.data.Position) < length)
{
span = default;
return false;
}
span = new ReadOnlySpan<byte>(this.exifData, this.position, length);
this.position += length;
return true;
int readed = this.data.Read(span);
return readed == length;
}
private uint ReadUInt32()
{
// Known as Long in Exif Specification
return this.TryReadSpan(4, out ReadOnlySpan<byte> span)
? this.ConvertToUInt32(span)
// Known as Long in Exif Specification
protected uint ReadUInt32() =>
this.TryReadSpan(this.buf4)
? this.ConvertToUInt32(this.buf4)
: default;
}
private ushort ReadUInt16()
{
return this.TryReadSpan(2, out ReadOnlySpan<byte> span)
? this.ConvertToShort(span)
protected ushort ReadUInt16() => this.TryReadSpan(this.buf2)
? this.ConvertToShort(this.buf2)
: default;
}
private void GetThumbnail(uint offset)
{
var values = new List<IExifValue>();
this.AddValues(values, offset);
foreach (ExifValue value in values)
{
if (value == ExifTag.JPEGInterchangeFormat)
{
this.ThumbnailOffset = ((ExifLong)value).Value;
}
else if (value == ExifTag.JPEGInterchangeFormatLength)
{
this.ThumbnailLength = ((ExifLong)value).Value;
}
}
}
private double ConvertToDouble(ReadOnlySpan<byte> buffer)
{

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

@ -11,11 +11,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary>
public static ExifTag<uint> SubfileType { get; } = new ExifTag<uint>(ExifTagValue.SubfileType);
/// <summary>
/// Gets the RowsPerStrip exif tag.
/// </summary>
public static ExifTag<uint> RowsPerStrip { get; } = new ExifTag<uint>(ExifTagValue.RowsPerStrip);
/// <summary>
/// Gets the SubIFDOffset exif tag.
/// </summary>

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

@ -56,11 +56,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary>
public static ExifTag<uint[]> StripRowCounts { get; } = new ExifTag<uint[]>(ExifTagValue.StripRowCounts);
/// <summary>
/// Gets the StripByteCounts exif tag.
/// </summary>
public static ExifTag<uint[]> StripByteCounts { get; } = new ExifTag<uint[]>(ExifTagValue.StripByteCounts);
/// <summary>
/// Gets the IntergraphRegisters exif tag.
/// </summary>

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

@ -16,6 +16,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary>
public static ExifTag<Number> ImageLength { get; } = new ExifTag<Number>(ExifTagValue.ImageLength);
/// <summary>
/// Gets the RowsPerStrip exif tag.
/// </summary>
public static ExifTag<Number> RowsPerStrip { get; } = new ExifTag<Number>(ExifTagValue.RowsPerStrip);
/// <summary>
/// Gets the TileWidth exif tag.
/// </summary>

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

@ -11,6 +11,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary>
public static ExifTag<Number[]> StripOffsets { get; } = new ExifTag<Number[]>(ExifTagValue.StripOffsets);
/// <summary>
/// Gets the StripByteCounts exif tag.
/// </summary>
public static ExifTag<Number[]> StripByteCounts { get; } = new ExifTag<Number[]>(ExifTagValue.StripByteCounts);
/// <summary>
/// Gets the TileByteCounts exif tag.
/// </summary>

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

@ -56,6 +56,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary>
public static ExifTag<ushort> PlanarConfiguration { get; } = new ExifTag<ushort>(ExifTagValue.PlanarConfiguration);
/// <summary>
/// Gets the Predictor exif tag.
/// </summary>
public static ExifTag<ushort> Predictor { get; } = new ExifTag<ushort>(ExifTagValue.Predictor);
/// <summary>
/// Gets the GrayResponseUnit exif tag.
/// </summary>

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

@ -46,11 +46,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary>
public static ExifTag<ushort[]> TransferFunction { get; } = new ExifTag<ushort[]>(ExifTagValue.TransferFunction);
/// <summary>
/// Gets the Predictor exif tag.
/// </summary>
public static ExifTag<ushort[]> Predictor { get; } = new ExifTag<ushort[]>(ExifTagValue.Predictor);
/// <summary>
/// Gets the HalftoneHints exif tag.
/// </summary>

50
src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{
internal sealed class ExifNumberArray : ExifArrayValue<Number>
@ -36,6 +38,54 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
}
}
public override bool TrySetValue(object value)
{
if (base.TrySetValue(value))
{
return true;
}
switch (value)
{
case int val:
return this.SetSingle(val, v => (Number)v);
case uint val:
return this.SetSingle(val, v => (Number)v);
case short val:
return this.SetSingle(val, v => (Number)v);
case ushort val:
return this.SetSingle(val, v => (Number)v);
case int[] array:
return this.SetArray(array, v => (Number)v);
case uint[] array:
return this.SetArray(array, v => (Number)v);
case short[] array:
return this.SetArray(array, v => (Number)v);
case ushort[] array:
return this.SetArray(array, v => (Number)v);
}
return false;
}
public override IExifValue DeepClone() => new ExifNumberArray(this);
private bool SetSingle<T>(T value, Func<T, Number> converter)
{
this.Value = new Number[] { converter(value) };
return true;
}
private bool SetArray<T>(T[] values, Func<T, Number> converter)
{
var numbers = new Number[values.Length];
for (int i = 0; i < values.Length; i++)
{
numbers[i] = converter(values[i]);
}
this.Value = numbers;
return true;
}
}
}

4
src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs

@ -93,6 +93,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
case ExifTagValue.ImageWidth: return new ExifNumber(ExifTag.ImageWidth);
case ExifTagValue.ImageLength: return new ExifNumber(ExifTag.ImageLength);
case ExifTagValue.RowsPerStrip: return new ExifNumber(ExifTag.RowsPerStrip);
case ExifTagValue.TileWidth: return new ExifNumber(ExifTag.TileWidth);
case ExifTagValue.TileLength: return new ExifNumber(ExifTag.TileLength);
case ExifTagValue.BadFaxLines: return new ExifNumber(ExifTag.BadFaxLines);
@ -100,6 +101,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
case ExifTagValue.PixelXDimension: return new ExifNumber(ExifTag.PixelXDimension);
case ExifTagValue.PixelYDimension: return new ExifNumber(ExifTag.PixelYDimension);
case ExifTagValue.StripByteCounts: return new ExifNumberArray(ExifTag.StripByteCounts);
case ExifTagValue.StripOffsets: return new ExifNumberArray(ExifTag.StripOffsets);
case ExifTagValue.TileByteCounts: return new ExifNumberArray(ExifTag.TileByteCounts);
case ExifTagValue.ImageLayer: return new ExifNumberArray(ExifTag.ImageLayer);
@ -158,6 +160,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
case ExifTagValue.Orientation: return new ExifShort(ExifTag.Orientation);
case ExifTagValue.SamplesPerPixel: return new ExifShort(ExifTag.SamplesPerPixel);
case ExifTagValue.PlanarConfiguration: return new ExifShort(ExifTag.PlanarConfiguration);
case ExifTagValue.Predictor: return new ExifShort(ExifTag.Predictor);
case ExifTagValue.GrayResponseUnit: return new ExifShort(ExifTag.GrayResponseUnit);
case ExifTagValue.ResolutionUnit: return new ExifShort(ExifTag.ResolutionUnit);
case ExifTagValue.CleanFaxData: return new ExifShort(ExifTag.CleanFaxData);
@ -203,7 +206,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
case ExifTagValue.ExtraSamples: return new ExifShortArray(ExifTag.ExtraSamples);
case ExifTagValue.PageNumber: return new ExifShortArray(ExifTag.PageNumber);
case ExifTagValue.TransferFunction: return new ExifShortArray(ExifTag.TransferFunction);
case ExifTagValue.Predictor: return new ExifShortArray(ExifTag.Predictor);
case ExifTagValue.HalftoneHints: return new ExifShortArray(ExifTag.HalftoneHints);
case ExifTagValue.SampleFormat: return new ExifShortArray(ExifTag.SampleFormat);
case ExifTagValue.TransferRange: return new ExifShortArray(ExifTag.TransferRange);

17
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -8,6 +8,7 @@ using System.Linq;
using SixLabors.ImageSharp.Formats.Experimental.Tiff;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
@ -145,6 +146,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(10, image.Metadata.VerticalResolution);
TiffFrameMetadata frame = image.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(30, frame.ExifProfile.Values.Count);
Assert.Equal(32u, frame.Width);
Assert.Equal(32u, frame.Height);
Assert.Equal(new ushort[] { 4 }, frame.BitsPerSample);
@ -153,10 +156,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal("This is Название", frame.ImageDescription);
Assert.Equal("This is Изготовитель камеры", frame.Make);
Assert.Equal("This is Модель камеры", frame.Model);
Assert.Equal(new uint[] { 8 }, frame.StripOffsets);
Assert.Equal(new Number[] { 8u }, frame.StripOffsets, new NumberComparer());
Assert.Equal(1, frame.SamplesPerPixel);
Assert.Equal(32u, frame.RowsPerStrip);
Assert.Equal(new uint[] { 297 }, frame.StripByteCounts);
Assert.Equal(new Number[] { 297u }, frame.StripByteCounts, new NumberComparer());
Assert.Equal(10, frame.HorizontalResolution);
Assert.Equal(10, frame.VerticalResolution);
Assert.Equal(TiffPlanarConfiguration.Chunky, frame.PlanarConfiguration);
@ -175,6 +178,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(TiffPredictor.None, frame.Predictor);
Assert.Null(frame.SampleFormat);
Assert.Equal("This is Авторские права", frame.Copyright);
Assert.Equal(4, frame.ExifProfile.GetValue<ushort>(ExifTag.Rating).Value);
Assert.Equal(75, frame.ExifProfile.GetValue<ushort>(ExifTag.RatingPercent).Value);
}
}
@ -191,14 +196,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(2, image.Frames.Count);
TiffFrameMetadata frame0 = image.Frames[0].Metadata.GetTiffMetadata();
Assert.Equal(TiffNewSubfileType.FullImage, frame0.NewSubfileType);
Assert.Null(frame0.SubfileType);
Assert.Equal(TiffNewSubfileType.FullImage, frame0.SubfileType);
Assert.Null(frame0.OldSubfileType);
Assert.Equal(255u, frame0.Width);
Assert.Equal(255u, frame0.Height);
TiffFrameMetadata frame1 = image.Frames[1].Metadata.GetTiffMetadata();
Assert.Equal(TiffNewSubfileType.Preview, frame1.NewSubfileType);
Assert.Equal(TiffSubfileType.Preview, frame1.SubfileType);
Assert.Equal(TiffNewSubfileType.Preview, frame1.SubfileType);
Assert.Null(frame1.OldSubfileType);
Assert.Equal(255u, frame1.Width);
Assert.Equal(255u, frame1.Height);
}

9
tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs

@ -2,9 +2,11 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
using ImageMagick;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@ -54,4 +56,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
return result;
}
}
internal class NumberComparer : IEqualityComparer<Number>
{
public bool Equals(Number x, Number y) => x.Equals(y);
public int GetHashCode(Number obj) => obj.GetHashCode();
}
}

15
tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
@ -14,6 +15,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests
{
[Trait("Profile", "Exif")]
public class ExifProfileTests
{
public enum TestImageWriteFormat
@ -201,10 +203,14 @@ namespace SixLabors.ImageSharp.Tests
IExifValue<Rational[]> latitude = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude);
Assert.Equal(expectedLatitude, latitude.Value);
// todo: duplicate tags
Assert.Equal(2, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932));
int profileCount = image.Metadata.ExifProfile.Values.Count;
image = WriteAndRead(image, imageFormat);
Assert.NotNull(image.Metadata.ExifProfile);
Assert.Equal(0, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932));
// Should be 3 less.
// 1 x due to setting of null "ReferenceBlackWhite" value.
@ -363,8 +369,14 @@ namespace SixLabors.ImageSharp.Tests
// Force parsing of the profile.
Assert.Equal(25, profile.Values.Count);
// todo: duplicate tags (from root container and subIfd)
Assert.Equal(2, profile.Values.Count(v => (ExifTagValue)(ushort)v.Tag == ExifTagValue.DateTime));
byte[] bytes = profile.ToByteArray();
Assert.Equal(525, bytes.Length);
var profile2 = new ExifProfile(bytes);
Assert.Equal(25, profile2.Values.Count);
}
[Theory]
@ -477,6 +489,9 @@ namespace SixLabors.ImageSharp.Tests
{
Assert.NotNull(profile);
// todo: duplicate tags
Assert.Equal(2, profile.Values.Count(v => (ushort)v.Tag == 59932));
Assert.Equal(16, profile.Values.Count);
foreach (IExifValue value in profile.Values)

1
tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifReaderTests.cs

@ -8,6 +8,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests
{
[Trait("Profile", "Exif")]
public class ExifReaderTests
{
[Fact]

1
tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifTagDescriptionAttributeTests.cs

@ -6,6 +6,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests
{
[Trait("Profile", "Exif")]
public class ExifTagDescriptionAttributeTests
{
[Fact]

1
tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifValueTests.cs

@ -7,6 +7,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests
{
[Trait("Profile", "Exif")]
public class ExifValueTests
{
private ExifProfile profile;

4
tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs

@ -6,6 +6,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values
{
[Trait("Profile", "Exif")]
public class ExifValuesTests
{
public static TheoryData<ExifTag> ByteTags => new TheoryData<ExifTag>
@ -94,6 +95,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values
public static TheoryData<ExifTag> NumberArrayTags => new TheoryData<ExifTag>
{
{ ExifTag.StripOffsets },
{ ExifTag.StripByteCounts },
{ ExifTag.TileByteCounts },
{ ExifTag.ImageLayer }
};
@ -160,6 +162,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values
{ ExifTag.Orientation },
{ ExifTag.SamplesPerPixel },
{ ExifTag.PlanarConfiguration },
{ ExifTag.Predictor },
{ ExifTag.GrayResponseUnit },
{ ExifTag.ResolutionUnit },
{ ExifTag.CleanFaxData },
@ -208,7 +211,6 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values
{ ExifTag.ExtraSamples },
{ ExifTag.PageNumber },
{ ExifTag.TransferFunction },
{ ExifTag.Predictor },
{ ExifTag.HalftoneHints },
{ ExifTag.SampleFormat },
{ ExifTag.TransferRange },

4
tests/ImageSharp.Tests/TestImages.cs

@ -569,13 +569,15 @@ namespace SixLabors.ImageSharp.Tests
public const string FillOrder2 = "Tiff/b0350_fillorder2.tiff";
public const string LittleEndianByteOrder = "Tiff/little_endian.tiff";
public const string Fax4_Motorola = "Tiff/moy.tiff";
public const string SampleMetadata = "Tiff/metadata_sample.tiff";
public static readonly string[] Multiframes = { MultiframeDeflateWithPreview, MultiframeLzwPredictor /*, MultiFrameDifferentSize, MultiframeDifferentSizeTiled, MultiFrameDifferentVariants,*/ };
public static readonly string[] Metadata = { SampleMetadata };
public static readonly string[] NotSupported = { Calliphora_RgbJpeg, RgbJpeg, RgbUncompressedTiled, MultiframeDifferentSize, MultiframeDifferentVariants, FillOrder2 };
public static readonly string[] NotSupported = { Calliphora_RgbJpeg, RgbJpeg, RgbUncompressedTiled, MultiframeDifferentSize, MultiframeDifferentVariants, FillOrder2, Calliphora_Fax4Compressed, Fax4_Motorola };
}
}
}

3
tests/Images/Input/Tiff/moy.tiff

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:026bb9372882d8fc15540b4f94e23138e75aacb0ebf2f5940b056fc66819ec46
size 1968862
Loading…
Cancel
Save