Browse Source

Exif/Tiff readers improvements

pull/1570/head
Ildar Khayrutdinov 5 years ago
parent
commit
3600b3d255
  1. 85
      src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
  2. 38
      src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs
  3. 31
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  4. 2
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  5. 26
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  6. 71
      src/ImageSharp/Formats/Tiff/TiffFrameMetadataExtensions.cs
  7. 4
      src/ImageSharp/Formats/Tiff/TiffFrameMetadataResolutionExtensions.cs
  8. 15
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  9. 223
      src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
  10. 10
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  11. 19
      tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs
  12. 4
      tests/ImageSharp.Tests/TestImages.cs
  13. 3
      tests/Images/Input/Tiff/moy.tiff

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

@ -1,9 +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.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
@ -13,60 +14,82 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary>
internal class DirectoryReader
{
private readonly ByteOrder byteOrder;
private readonly Stream stream;
private uint nextIfdOffset;
public DirectoryReader(ByteOrder byteOrder, Stream stream)
{
this.byteOrder = byteOrder;
this.stream = 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.nextIfdOffset = new HeaderReader(this.byteOrder, this.stream).ReadFileHeader();
IEnumerable<List<IExifValue>> ifdList = this.ReadIfds();
this.ByteOrder = ReadByteOrder(this.stream);
this.nextIfdOffset = new HeaderReader(this.stream, this.ByteOrder).ReadFileHeader();
return this.ReadIfds();
}
var list = new List<ExifProfile>();
foreach (List<IExifValue> ifd in ifdList)
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)
{
var profile = new ExifProfile();
profile.InitializeInternal(ifd);
list.Add(profile);
return ByteOrder.LittleEndian;
}
else if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian)
{
return ByteOrder.BigEndian;
}
return list;
throw TiffThrowHelper.InvalidHeader();
}
private IEnumerable<List<IExifValue>> ReadIfds()
private IEnumerable<ExifProfile> ReadIfds()
{
var valuesList = new List<List<IExifValue>>();
var readersList = new SortedList<uint, EntryReader>();
while (this.nextIfdOffset != 0)
var readers = new List<EntryReader>();
while (this.nextIfdOffset != 0 && this.nextIfdOffset < this.stream.Length)
{
var reader = new EntryReader(this.byteOrder, this.stream, this.nextIfdOffset);
List<IExifValue> values = reader.ReadValues();
valuesList.Add(values);
var reader = new EntryReader(this.stream, this.ByteOrder, this.nextIfdOffset, this.lazyLoaders);
reader.ReadTags();
this.nextIfdOffset = reader.NextIfdOffset;
if (reader.BigValuesOffset.HasValue)
{
readersList.Add(reader.BigValuesOffset.Value, reader);
}
readers.Add(reader);
}
// sequential reading big values
foreach (EntryReader reader in readersList.Values)
foreach (Action loader in this.lazyLoaders.Values)
{
reader.LoadBigValues();
loader();
}
return valuesList;
var list = new List<ExifProfile>();
foreach (EntryReader reader in readers)
{
var profile = new ExifProfile(reader.Values, reader.InvalidTags);
list.Add(profile);
}
return list;
}
/// <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
{
public int Compare(TKey x, TKey y)
{
int result = x.CompareTo(y);
// Handle equality as beeing greater
return (result == 0) ? 1 : result;
}
}
}
}

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
@ -9,38 +10,41 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
internal class EntryReader : ExifReader
internal class EntryReader : BaseExifReader
{
private readonly uint startOffset;
public EntryReader(ByteOrder byteOrder, Stream stream, uint ifdOffset)
: base(byteOrder == ByteOrder.BigEndian, stream) =>
private readonly SortedList<uint, Action> lazyLoaders;
public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList<uint, Action> lazyLoaders)
: base(stream)
{
this.IsBigEndian = byteOrder == ByteOrder.BigEndian;
this.startOffset = ifdOffset;
this.lazyLoaders = lazyLoaders;
}
public uint? BigValuesOffset => this.LazyStartOffset;
public List<IExifValue> Values { get; } = new List<IExifValue>();
public uint NextIfdOffset { get; private set; }
public override List<IExifValue> ReadValues()
public void ReadTags()
{
var values = new List<IExifValue>();
this.AddValues(values, this.startOffset);
this.ReadValues(this.Values, this.startOffset);
this.NextIfdOffset = this.ReadUInt32();
this.AddSubIfdValues(values);
return values;
this.ReadSubIfd(this.Values);
}
public void LoadBigValues() => this.LazyLoad();
protected override void RegisterExtLoader(uint offset, Action reader) =>
this.lazyLoaders.Add(offset, reader);
}
internal class HeaderReader : ExifReader
internal class HeaderReader : BaseExifReader
{
public HeaderReader(ByteOrder byteOrder, Stream stream)
: base(byteOrder == ByteOrder.BigEndian, stream)
{
}
public HeaderReader(Stream stream, ByteOrder byteOrder)
: base(stream) =>
this.IsBigEndian = byteOrder == ByteOrder.BigEndian;
public uint FirstIfdOffset { get; private set; }
@ -55,5 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
this.FirstIfdOffset = this.ReadUInt32();
return this.FirstIfdOffset;
}
protected override void RegisterExtLoader(uint offset, Action reader) => throw new NotImplementedException();
}
}

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

@ -2,7 +2,6 @@
// 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;
@ -102,8 +101,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
where TPixel : unmanaged, IPixel<TPixel>
{
this.inputStream = stream;
ByteOrder byteOrder = ReadByteOrder(stream);
var reader = new DirectoryReader(byteOrder, stream);
var reader = new DirectoryReader(stream);
IEnumerable<ExifProfile> directories = reader.Read();
@ -116,7 +114,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
framesMetadata.Add(frameMetadata);
}
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, byteOrder);
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, reader.ByteOrder);
// todo: tiff frames can have different sizes
{
@ -140,40 +138,23 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.inputStream = stream;
ByteOrder byteOrder = ReadByteOrder(stream);
var reader = new DirectoryReader(byteOrder, stream);
var reader = new DirectoryReader(stream);
IEnumerable<ExifProfile> directories = reader.Read();
var framesMetadata = new List<TiffFrameMetadata>();
foreach (ExifProfile ifd in directories)
{
var meta = new TiffFrameMetadata() { FrameTags = ifd };
var meta = new TiffFrameMetadata() { ExifProfile = ifd };
framesMetadata.Add(meta);
}
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, 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 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>
@ -188,7 +169,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{
var coreMetadata = new ImageFrameMetadata();
frameMetaData = coreMetadata.GetTiffMetadata();
frameMetaData.FrameTags = tags;
frameMetaData.ExifProfile = tags;
this.VerifyAndParse(frameMetaData);

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

@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
private void ProcessMetadata(TiffFrameMetadata frameMetadata)
{
foreach (IExifValue entry in frameMetadata.FrameTags.Values)
foreach (IExifValue entry in frameMetadata.ExifProfile.Values)
{
// todo: skip subIfd
if (entry.DataType == ExifDataType.Ifd)

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

@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <summary>
/// Gets the Tiff directory tags.
/// </summary>
public ExifProfile FrameTags
public ExifProfile ExifProfile
{
get => this.frameTags ??= new ExifProfile();
internal set => this.frameTags = value;
@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string ImageDescription
{
get => this.GetString(ExifTag.ImageDescription);
set => this.SetString(ExifTag.ImageDescription, value);
set => this.Set(ExifTag.ImageDescription, value);
}
/// <summary>
@ -125,7 +125,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string Make
{
get => this.GetString(ExifTag.Make);
set => this.SetString(ExifTag.Make, value);
set => this.Set(ExifTag.Make, value);
}
/// <summary>
@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string Model
{
get => this.GetString(ExifTag.Model);
set => this.SetString(ExifTag.Model, value);
set => this.Set(ExifTag.Model, value);
}
/// <summary>Gets for each strip, the byte offset of that strip..</summary>
@ -181,7 +181,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string Software
{
get => this.GetString(ExifTag.Software);
set => this.SetString(ExifTag.Software, value);
set => this.Set(ExifTag.Software, value);
}
/// <summary>
@ -190,7 +190,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string DateTime
{
get => this.GetString(ExifTag.DateTime);
set => this.SetString(ExifTag.DateTime, value);
set => this.Set(ExifTag.DateTime, value);
}
/// <summary>
@ -199,7 +199,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string Artist
{
get => this.GetString(ExifTag.Artist);
set => this.SetString(ExifTag.Artist, value);
set => this.Set(ExifTag.Artist, value);
}
/// <summary>
@ -208,7 +208,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string HostComputer
{
get => this.GetString(ExifTag.HostComputer);
set => this.SetString(ExifTag.HostComputer, value);
set => this.Set(ExifTag.HostComputer, value);
}
/// <summary>
@ -227,7 +227,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string Copyright
{
get => this.GetString(ExifTag.Copyright);
set => this.SetString(ExifTag.Copyright, value);
set => this.Set(ExifTag.Copyright, value);
}
/// <summary>
@ -247,7 +247,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public void ClearMetadata()
{
var tags = new List<IExifValue>();
foreach (IExifValue entry in this.FrameTags.Values)
foreach (IExifValue entry in this.ExifProfile.Values)
{
switch ((ExifTagValue)(ushort)entry.Tag)
{
@ -267,12 +267,10 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
}
}
var profile = new ExifProfile();
profile.InitializeInternal(tags);
this.FrameTags = profile;
this.ExifProfile = new ExifProfile(tags, this.ExifProfile.InvalidTags);
}
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffFrameMetadata() { FrameTags = this.FrameTags.DeepClone() };
public IDeepCloneable DeepClone() => new TiffFrameMetadata() { ExifProfile = this.ExifProfile.DeepClone() };
}
}

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

@ -31,15 +31,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public static bool TryGetArray<T>(this TiffFrameMetadata meta, ExifTag tag, out T[] result)
where T : struct
{
foreach (IExifValue entry in meta.FrameTags.Values)
IExifValue obj = meta.ExifProfile.GetValueInternal(tag);
if (obj != null)
{
if (entry.Tag == tag)
{
DebugGuard.IsTrue(entry.IsArray, "Expected array entry");
result = (T[])entry.GetValue();
return true;
}
DebugGuard.IsTrue(obj.IsArray, "Expected array entry");
object value = obj.GetValue();
result = (T[])value;
return true;
}
result = null;
@ -65,23 +63,20 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public static string GetString(this TiffFrameMetadata meta, ExifTag tag)
{
foreach (IExifValue entry in meta.FrameTags.Values)
IExifValue obj = meta.ExifProfile.GetValueInternal(tag);
if (obj != null)
{
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;
}
DebugGuard.IsTrue(obj.DataType == ExifDataType.Ascii, "Expected string entry");
object value = obj.GetValue();
DebugGuard.IsTrue(value is string, "Expected string entry");
return (string)value;
}
return null;
}
public static void SetString(this TiffFrameMetadata meta, ExifTag tag, string value) =>
meta.FrameTags.SetValueInternal(tag, value);
public static void Set(this TiffFrameMetadata meta, ExifTag tag, object value) =>
meta.ExifProfile.SetValueInternal(tag, value);
public static TEnum? GetSingleEnumNullable<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag)
where TEnum : struct
@ -100,11 +95,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
where TTagValue : struct
=> meta.GetSingleEnumNullable<TEnum, TTagValue>(tag) ?? (defaultValue != null ? defaultValue.Value : throw TiffThrowHelper.TagNotFound(nameof(tag)));
public static void SetSingleEnum<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag, TEnum value)
where TEnum : struct
where TTagValue : struct
=> meta.FrameTags.SetValueInternal(tag, value);
public static T GetSingle<T>(this TiffFrameMetadata meta, ExifTag tag)
where T : struct
{
@ -119,42 +109,25 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public static bool TryGetSingle<T>(this TiffFrameMetadata meta, ExifTag tag, out T result)
where T : struct
{
foreach (IExifValue entry in meta.FrameTags.Values)
IExifValue obj = meta.ExifProfile.GetValueInternal(tag);
if (obj != null)
{
if (entry.Tag == tag)
{
DebugGuard.IsTrue(!entry.IsArray, "Expected non array entry");
object value = entry.GetValue();
result = (T)value;
return true;
}
DebugGuard.IsTrue(!obj.IsArray, "Expected non array entry");
object value = obj.GetValue();
result = (T)value;
return true;
}
result = default;
return false;
}
public static void SetSingle<T>(this TiffFrameMetadata meta, ExifTag tag, T value)
where T : struct
=> meta.FrameTags.SetValueInternal(tag, value);
public static bool Remove(this TiffFrameMetadata meta, ExifTag tag)
{
IExifValue obj = null;
foreach (IExifValue entry in meta.FrameTags.Values)
{
if (entry.Tag == tag)
{
obj = entry;
break;
}
}
IExifValue obj = meta.ExifProfile.GetValueInternal(tag);
if (obj != null)
{
return meta.FrameTags.RemoveValue(obj.Tag);
return meta.ExifProfile.RemoveValue(obj.Tag);
}
return false;

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

@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
break;
}
meta.SetSingle(ExifTag.ResolutionUnit, (ushort)unit + 1);
meta.Set(ExifTag.ResolutionUnit, (ushort)unit + 1);
meta.SetResolution(ExifTag.XResolution, horizontal);
meta.SetResolution(ExifTag.YResolution, vertical);
}
@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
break;
}
meta.SetSingle(tag, new Rational(res));
meta.Set(tag, new Rational(res));
}
}
}

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

@ -53,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.
@ -259,9 +271,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution);
}
internal void InitializeInternal(List<IExifValue> values) =>
this.values = values;
private void SyncResolution(ExifTag<Rational> tag, double resolution)
{
IExifValue<Rational> value = this.GetValue(tag);

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

@ -12,70 +12,13 @@ using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{
/// <summary>
/// Reads and parses EXIF data from a stream.
/// </summary>
internal class ExifReader
internal class ExifReader : BaseExifReader
{
private readonly Stream data;
private readonly byte[] offsetBuffer = new byte[4];
private readonly byte[] buf4 = new byte[4];
private readonly byte[] buf2 = new byte[2];
// 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>());
private readonly List<Action> loaders = new List<Action>();
private bool isBigEndian;
private List<ExifTag> invalidTags;
private uint exifOffset = 0;
private uint gpsOffset = 0;
public ExifReader(bool isBigEndian, Stream stream)
public ExifReader(byte[] exifData)
: base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData))))
{
this.isBigEndian = isBigEndian;
this.data = stream ?? throw new ArgumentNullException(nameof(stream));
}
public ExifReader(byte[] exifData) =>
this.data = new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData)));
private delegate TDataType ConverterMethod<TDataType>(ReadOnlySpan<byte> data);
/// <summary>
/// Gets the invalid tags.
/// </summary>
public IReadOnlyList<ExifTag> InvalidTags => this.invalidTags ?? (IReadOnlyList<ExifTag>)Array.Empty<ExifTag>();
/// <summary>
/// Gets the thumbnail length in the byte stream.
/// </summary>
public uint ThumbnailLength { get; private set; }
/// <summary>
/// Gets the thumbnail offset position in the byte stream.
/// </summary>
public uint ThumbnailOffset { get; private set; }
protected uint? LazyStartOffset => this.lazyLoaders.Count > 0 ? this.lazyLoaders.Keys[0] : (uint?)null;
private uint Length => (uint)this.data.Length;
private int RemainingLength
{
get
{
if (this.data.Position >= this.data.Length)
{
return 0;
}
return (int)(this.data.Length - this.data.Position);
}
}
/// <summary>
@ -84,12 +27,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// <returns>
/// The <see cref="Collection{ExifValue}"/>.
/// </returns>
public virtual List<IExifValue> ReadValues()
public List<IExifValue> ReadValues()
{
var values = new List<IExifValue>();
// Exif header: II == 0x4949
this.isBigEndian = this.ReadUInt16() != 0x4949;
// II == 0x4949
this.IsBigEndian = this.ReadUInt16() != 0x4949;
if (this.ReadUInt16() != 0x002A)
{
@ -97,38 +40,107 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
}
uint ifdOffset = this.ReadUInt32();
this.AddValues(values, ifdOffset);
this.ReadValues(values, ifdOffset);
uint thumbnailOffset = this.ReadUInt32();
this.GetThumbnail(thumbnailOffset);
this.AddSubIfdValues(values);
this.LazyLoad();
this.ReadSubIfd(values);
foreach (Action loader in this.loaders)
{
loader();
}
return values;
}
protected void LazyLoad()
protected override void RegisterExtLoader(uint offset, Action loader) => this.loaders.Add(loader);
private void GetThumbnail(uint offset)
{
foreach (Action act in this.lazyLoaders.Values)
if (offset == 0)
{
return;
}
var values = new List<IExifValue>();
this.ReadValues(values, offset);
foreach (ExifValue value in values)
{
act();
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 stream.
/// </summary>
internal abstract class BaseExifReader
{
private readonly byte[] offsetBuffer = new byte[4];
private readonly byte[] buf4 = new byte[4];
private readonly byte[] buf2 = new byte[2];
private readonly Stream data;
private bool isBigEndian;
private List<ExifTag> invalidTags;
private uint exifOffset;
private uint gpsOffset;
protected BaseExifReader(Stream stream) =>
this.data = stream ?? throw new ArgumentNullException(nameof(stream));
private delegate TDataType ConverterMethod<TDataType>(ReadOnlySpan<byte> data);
/// <summary>
/// Gets the invalid tags.
/// </summary>
public IReadOnlyList<ExifTag> InvalidTags => this.invalidTags ?? (IReadOnlyList<ExifTag>)Array.Empty<ExifTag>();
/// <summary>
/// Gets or sets the thumbnail length in the byte stream.
/// </summary>
public uint ThumbnailLength { get; protected set; }
/// <summary>
/// Gets or sets the thumbnail offset position in the byte stream.
/// </summary>
public uint ThumbnailOffset { get; protected set; }
public bool IsBigEndian
{
get => this.isBigEndian;
protected set => this.isBigEndian = value;
}
protected abstract void RegisterExtLoader(uint offset, Action loader);
/// <summary>
/// Adds the collection of EXIF values to the reader.
/// Reads the values to the values collection.
/// </summary>
/// <param name="values">The values.</param>
/// <param name="index">The index.</param>
protected void AddValues(List<IExifValue> values, uint index)
/// <param name="offset">The IFD offset.</param>
protected void ReadValues(List<IExifValue> values, uint offset)
{
if (index > this.Length)
if (offset > this.data.Length)
{
return;
}
this.Seek(index);
this.Seek(offset);
int count = this.ReadUInt16();
for (int i = 0; i < count; i++)
@ -137,16 +149,16 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
}
}
protected void AddSubIfdValues(List<IExifValue> 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);
}
}
@ -280,7 +292,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{
// 2 | 2 | 4 | 4
// tag | type | count | value offset
if (this.RemainingLength < 12)
if ((this.data.Length - this.data.Position) < 12)
{
return;
}
@ -316,28 +328,22 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
uint size = numberOfComponents * ExifDataTypes.GetSize(dataType);
if (size > 4)
{
uint newIndex = this.ConvertToUInt32(this.offsetBuffer);
uint newOffset = this.ConvertToUInt32(this.offsetBuffer);
// Ensure that the new index does not overrun the data
if (newIndex > int.MaxValue || newIndex + size > this.Length)
if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length)
{
this.AddInvalidTag(new UnkownExifTag(tag));
return;
}
if (this.lazyLoaders.ContainsKey(newIndex))
{
Debug.WriteLine($"Duplicate offset: tag={tag}, size={size}, offset={newIndex}");
}
this.lazyLoaders.Add(newIndex, () =>
this.RegisterExtLoader(newOffset, () =>
{
var dataBuffer = new byte[size];
this.Seek(newIndex);
this.Seek(newOffset);
if (this.TryReadSpan(dataBuffer))
{
object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents);
this.Add(values, exifValue, value);
}
});
@ -391,7 +397,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
private bool TryReadSpan(Span<byte> span)
{
int length = span.Length;
if (this.RemainingLength < length)
if ((this.data.Length - this.data.Position) < length)
{
span = default;
return false;
@ -411,29 +417,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
? this.ConvertToShort(this.buf2)
: default;
private void GetThumbnail(uint offset)
{
if (offset == 0)
{
return;
}
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)
{
if (buffer.Length < 8)
@ -538,19 +521,5 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
? BinaryPrimitives.ReadInt16BigEndian(buffer)
: BinaryPrimitives.ReadInt16LittleEndian(buffer);
}
/// <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>
public class DuplicateKeyComparer<TKey> : IComparer<TKey>
where TKey : IComparable
{
public int Compare(TKey x, TKey y)
{
int result = x.CompareTo(y);
// Handle equality as beeing greater
return (result == 0) ? 1 : result;
}
}
}
}

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

@ -146,7 +146,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(10, image.Metadata.VerticalResolution);
TiffFrameMetadata frame = image.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(30, frame.FrameTags.Values.Count);
Assert.Equal(30, frame.ExifProfile.Values.Count);
Assert.Equal(32u, frame.Width);
Assert.Equal(32u, frame.Height);
@ -156,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);
TiffTestUtils.Compare(new Number[] { 8 }, frame.StripOffsets);
Assert.Equal(new Number[] { 8u }, frame.StripOffsets, new NumberComparer());
Assert.Equal(1, frame.SamplesPerPixel);
Assert.Equal(32u, frame.RowsPerStrip);
TiffTestUtils.Compare(new Number[] { 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);
@ -178,8 +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.FrameTags.GetValue<ushort>(ExifTag.Rating).Value);
Assert.Equal(75, frame.FrameTags.GetValue<ushort>(ExifTag.RatingPercent).Value);
Assert.Equal(4, frame.ExifProfile.GetValue<ushort>(ExifTag.Rating).Value);
Assert.Equal(75, frame.ExifProfile.GetValue<ushort>(ExifTag.RatingPercent).Value);
}
}

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

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
using ImageMagick;
@ -54,20 +55,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
return result;
}
}
public static void Compare(Number[] a1, Number[] a2)
{
Assert.True(a1 == null ^ a2 != null);
if (a1 == null /*&& a2 == null*/)
{
return;
}
internal class NumberComparer : IEqualityComparer<Number>
{
public bool Equals(Number x, Number y) => x.Equals(y);
Assert.Equal(a1.Length, a2.Length);
for (int i = 0; i < a1.Length; i++)
{
Assert.Equal((int)a1[i], (int)a2[i]);
}
}
public int GetHashCode(Number obj) => obj.GetHashCode();
}
}

4
tests/ImageSharp.Tests/TestImages.cs

@ -567,13 +567,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