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. // Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
@ -13,60 +14,82 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// </summary> /// </summary>
internal class DirectoryReader internal class DirectoryReader
{ {
private readonly ByteOrder byteOrder;
private readonly Stream stream; private readonly Stream stream;
private uint nextIfdOffset; private uint nextIfdOffset;
public DirectoryReader(ByteOrder byteOrder, 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
this.byteOrder = byteOrder; private readonly SortedList<uint, Action> lazyLoaders = new SortedList<uint, Action>(new DuplicateKeyComparer<uint>());
this.stream = stream;
} public DirectoryReader(Stream stream) => this.stream = stream;
public ByteOrder ByteOrder { get; private set; }
public IEnumerable<ExifProfile> Read() public IEnumerable<ExifProfile> Read()
{ {
this.nextIfdOffset = new HeaderReader(this.byteOrder, this.stream).ReadFileHeader(); this.ByteOrder = ReadByteOrder(this.stream);
this.nextIfdOffset = new HeaderReader(this.stream, this.ByteOrder).ReadFileHeader();
IEnumerable<List<IExifValue>> ifdList = this.ReadIfds(); return this.ReadIfds();
}
var list = new List<ExifProfile>(); private static ByteOrder ReadByteOrder(Stream stream)
foreach (List<IExifValue> ifd in ifdList) {
var headerBytes = new byte[2];
stream.Read(headerBytes, 0, 2);
if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian)
{ {
var profile = new ExifProfile(); return ByteOrder.LittleEndian;
profile.InitializeInternal(ifd); }
list.Add(profile); 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 readers = new List<EntryReader>();
var readersList = new SortedList<uint, EntryReader>(); while (this.nextIfdOffset != 0 && this.nextIfdOffset < this.stream.Length)
while (this.nextIfdOffset != 0)
{ {
var reader = new EntryReader(this.byteOrder, this.stream, this.nextIfdOffset); var reader = new EntryReader(this.stream, this.ByteOrder, this.nextIfdOffset, this.lazyLoaders);
List<IExifValue> values = reader.ReadValues(); reader.ReadTags();
valuesList.Add(values);
this.nextIfdOffset = reader.NextIfdOffset; this.nextIfdOffset = reader.NextIfdOffset;
if (reader.BigValuesOffset.HasValue) readers.Add(reader);
{
readersList.Add(reader.BigValuesOffset.Value, reader);
}
} }
// sequential reading big values // 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. // Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -9,38 +10,41 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Experimental.Tiff namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{ {
internal class EntryReader : ExifReader internal class EntryReader : BaseExifReader
{ {
private readonly uint startOffset; private readonly uint startOffset;
public EntryReader(ByteOrder byteOrder, Stream stream, uint ifdOffset) private readonly SortedList<uint, Action> lazyLoaders;
: base(byteOrder == ByteOrder.BigEndian, stream) =>
public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList<uint, Action> lazyLoaders)
: base(stream)
{
this.IsBigEndian = byteOrder == ByteOrder.BigEndian;
this.startOffset = ifdOffset; 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 uint NextIfdOffset { get; private set; }
public override List<IExifValue> ReadValues() public void ReadTags()
{ {
var values = new List<IExifValue>(); this.ReadValues(this.Values, this.startOffset);
this.AddValues(values, this.startOffset);
this.NextIfdOffset = this.ReadUInt32(); this.NextIfdOffset = this.ReadUInt32();
this.AddSubIfdValues(values); this.ReadSubIfd(this.Values);
return 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) public HeaderReader(Stream stream, ByteOrder byteOrder)
: base(byteOrder == ByteOrder.BigEndian, stream) : base(stream) =>
{ this.IsBigEndian = byteOrder == ByteOrder.BigEndian;
}
public uint FirstIfdOffset { get; private set; } public uint FirstIfdOffset { get; private set; }
@ -55,5 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
this.FirstIfdOffset = this.ReadUInt32(); this.FirstIfdOffset = this.ReadUInt32();
return this.FirstIfdOffset; 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. // Licensed under the Apache License, Version 2.0.
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading; using System.Threading;
using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression; using SixLabors.ImageSharp.Formats.Experimental.Tiff.Compression;
@ -102,8 +101,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.inputStream = stream; this.inputStream = stream;
ByteOrder byteOrder = ReadByteOrder(stream); var reader = new DirectoryReader(stream);
var reader = new DirectoryReader(byteOrder, stream);
IEnumerable<ExifProfile> directories = reader.Read(); IEnumerable<ExifProfile> directories = reader.Read();
@ -116,7 +114,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
framesMetadata.Add(frameMetadata); 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 // todo: tiff frames can have different sizes
{ {
@ -140,40 +138,23 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.inputStream = stream; this.inputStream = stream;
ByteOrder byteOrder = ReadByteOrder(stream); var reader = new DirectoryReader(stream);
var reader = new DirectoryReader(byteOrder, stream);
IEnumerable<ExifProfile> directories = reader.Read(); IEnumerable<ExifProfile> directories = reader.Read();
var framesMetadata = new List<TiffFrameMetadata>(); var framesMetadata = new List<TiffFrameMetadata>();
foreach (ExifProfile ifd in directories) foreach (ExifProfile ifd in directories)
{ {
var meta = new TiffFrameMetadata() { FrameTags = ifd }; var meta = new TiffFrameMetadata() { ExifProfile = ifd };
framesMetadata.Add(meta); framesMetadata.Add(meta);
} }
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, byteOrder); ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.ignoreMetadata, reader.ByteOrder);
TiffFrameMetadata root = framesMetadata[0]; TiffFrameMetadata root = framesMetadata[0];
return new ImageInfo(new PixelTypeInfo(root.BitsPerPixel), (int)root.Width, (int)root.Height, metadata); 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> /// <summary>
/// Decodes the image data from a specified IFD. /// Decodes the image data from a specified IFD.
/// </summary> /// </summary>
@ -188,7 +169,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
{ {
var coreMetadata = new ImageFrameMetadata(); var coreMetadata = new ImageFrameMetadata();
frameMetaData = coreMetadata.GetTiffMetadata(); frameMetaData = coreMetadata.GetTiffMetadata();
frameMetaData.FrameTags = tags; frameMetaData.ExifProfile = tags;
this.VerifyAndParse(frameMetaData); 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) private void ProcessMetadata(TiffFrameMetadata frameMetadata)
{ {
foreach (IExifValue entry in frameMetadata.FrameTags.Values) foreach (IExifValue entry in frameMetadata.ExifProfile.Values)
{ {
// todo: skip subIfd // todo: skip subIfd
if (entry.DataType == ExifDataType.Ifd) if (entry.DataType == ExifDataType.Ifd)

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

@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
/// <summary> /// <summary>
/// Gets the Tiff directory tags. /// Gets the Tiff directory tags.
/// </summary> /// </summary>
public ExifProfile FrameTags public ExifProfile ExifProfile
{ {
get => this.frameTags ??= new ExifProfile(); get => this.frameTags ??= new ExifProfile();
internal set => this.frameTags = value; internal set => this.frameTags = value;
@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string ImageDescription public string ImageDescription
{ {
get => this.GetString(ExifTag.ImageDescription); get => this.GetString(ExifTag.ImageDescription);
set => this.SetString(ExifTag.ImageDescription, value); set => this.Set(ExifTag.ImageDescription, value);
} }
/// <summary> /// <summary>
@ -125,7 +125,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string Make public string Make
{ {
get => this.GetString(ExifTag.Make); get => this.GetString(ExifTag.Make);
set => this.SetString(ExifTag.Make, value); set => this.Set(ExifTag.Make, value);
} }
/// <summary> /// <summary>
@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string Model public string Model
{ {
get => this.GetString(ExifTag.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> /// <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 public string Software
{ {
get => this.GetString(ExifTag.Software); get => this.GetString(ExifTag.Software);
set => this.SetString(ExifTag.Software, value); set => this.Set(ExifTag.Software, value);
} }
/// <summary> /// <summary>
@ -190,7 +190,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string DateTime public string DateTime
{ {
get => this.GetString(ExifTag.DateTime); get => this.GetString(ExifTag.DateTime);
set => this.SetString(ExifTag.DateTime, value); set => this.Set(ExifTag.DateTime, value);
} }
/// <summary> /// <summary>
@ -199,7 +199,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string Artist public string Artist
{ {
get => this.GetString(ExifTag.Artist); get => this.GetString(ExifTag.Artist);
set => this.SetString(ExifTag.Artist, value); set => this.Set(ExifTag.Artist, value);
} }
/// <summary> /// <summary>
@ -208,7 +208,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string HostComputer public string HostComputer
{ {
get => this.GetString(ExifTag.HostComputer); get => this.GetString(ExifTag.HostComputer);
set => this.SetString(ExifTag.HostComputer, value); set => this.Set(ExifTag.HostComputer, value);
} }
/// <summary> /// <summary>
@ -227,7 +227,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public string Copyright public string Copyright
{ {
get => this.GetString(ExifTag.Copyright); get => this.GetString(ExifTag.Copyright);
set => this.SetString(ExifTag.Copyright, value); set => this.Set(ExifTag.Copyright, value);
} }
/// <summary> /// <summary>
@ -247,7 +247,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public void ClearMetadata() public void ClearMetadata()
{ {
var tags = new List<IExifValue>(); var tags = new List<IExifValue>();
foreach (IExifValue entry in this.FrameTags.Values) foreach (IExifValue entry in this.ExifProfile.Values)
{ {
switch ((ExifTagValue)(ushort)entry.Tag) switch ((ExifTagValue)(ushort)entry.Tag)
{ {
@ -267,12 +267,10 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
} }
} }
var profile = new ExifProfile(); this.ExifProfile = new ExifProfile(tags, this.ExifProfile.InvalidTags);
profile.InitializeInternal(tags);
this.FrameTags = profile;
} }
/// <inheritdoc/> /// <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) public static bool TryGetArray<T>(this TiffFrameMetadata meta, ExifTag tag, out T[] result)
where T : struct 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(obj.IsArray, "Expected array entry");
{ object value = obj.GetValue();
DebugGuard.IsTrue(entry.IsArray, "Expected array entry"); result = (T[])value;
return true;
result = (T[])entry.GetValue();
return true;
}
} }
result = null; result = null;
@ -65,23 +63,20 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
public static string GetString(this TiffFrameMetadata meta, ExifTag tag) 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(obj.DataType == ExifDataType.Ascii, "Expected string entry");
{ object value = obj.GetValue();
DebugGuard.IsTrue(entry.DataType == ExifDataType.Ascii, "Expected string entry"); DebugGuard.IsTrue(value is string, "Expected string entry");
object value = entry.GetValue(); return (string)value;
DebugGuard.IsTrue(value is string, "Expected string entry");
return (string)value;
}
} }
return null; return null;
} }
public static void SetString(this TiffFrameMetadata meta, ExifTag tag, string value) => public static void Set(this TiffFrameMetadata meta, ExifTag tag, object value) =>
meta.FrameTags.SetValueInternal(tag, value); meta.ExifProfile.SetValueInternal(tag, value);
public static TEnum? GetSingleEnumNullable<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag) public static TEnum? GetSingleEnumNullable<TEnum, TTagValue>(this TiffFrameMetadata meta, ExifTag tag)
where TEnum : struct where TEnum : struct
@ -100,11 +95,6 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
where TTagValue : struct where TTagValue : struct
=> meta.GetSingleEnumNullable<TEnum, TTagValue>(tag) ?? (defaultValue != null ? defaultValue.Value : throw TiffThrowHelper.TagNotFound(nameof(tag))); => 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) public static T GetSingle<T>(this TiffFrameMetadata meta, ExifTag tag)
where T : struct 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) public static bool TryGetSingle<T>(this TiffFrameMetadata meta, ExifTag tag, out T result)
where T : struct 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(!obj.IsArray, "Expected non array entry");
{ object value = obj.GetValue();
DebugGuard.IsTrue(!entry.IsArray, "Expected non array entry"); result = (T)value;
return true;
object value = entry.GetValue();
result = (T)value;
return true;
}
} }
result = default; result = default;
return false; 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) public static bool Remove(this TiffFrameMetadata meta, ExifTag tag)
{ {
IExifValue obj = null; IExifValue obj = meta.ExifProfile.GetValueInternal(tag);
foreach (IExifValue entry in meta.FrameTags.Values)
{
if (entry.Tag == tag)
{
obj = entry;
break;
}
}
if (obj != null) if (obj != null)
{ {
return meta.FrameTags.RemoveValue(obj.Tag); return meta.ExifProfile.RemoveValue(obj.Tag);
} }
return false; return false;

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

@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
break; break;
} }
meta.SetSingle(ExifTag.ResolutionUnit, (ushort)unit + 1); meta.Set(ExifTag.ResolutionUnit, (ushort)unit + 1);
meta.SetResolution(ExifTag.XResolution, horizontal); meta.SetResolution(ExifTag.XResolution, horizontal);
meta.SetResolution(ExifTag.YResolution, vertical); meta.SetResolution(ExifTag.YResolution, vertical);
} }
@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.Tiff
break; 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>(); 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> /// <summary>
/// Initializes a new instance of the <see cref="ExifProfile"/> class /// Initializes a new instance of the <see cref="ExifProfile"/> class
/// by making a copy from another EXIF profile. /// by making a copy from another EXIF profile.
@ -259,9 +271,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution); this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution);
} }
internal void InitializeInternal(List<IExifValue> values) =>
this.values = values;
private void SyncResolution(ExifTag<Rational> tag, double resolution) private void SyncResolution(ExifTag<Rational> tag, double resolution)
{ {
IExifValue<Rational> value = this.GetValue(tag); 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 namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{ {
/// <summary> internal class ExifReader : BaseExifReader
/// Reads and parses EXIF data from a stream.
/// </summary>
internal class ExifReader
{ {
private readonly Stream data; private readonly List<Action> loaders = new List<Action>();
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 bool isBigEndian; public ExifReader(byte[] exifData)
: base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData))))
private List<ExifTag> invalidTags;
private uint exifOffset = 0;
private uint gpsOffset = 0;
public ExifReader(bool isBigEndian, Stream stream)
{ {
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> /// <summary>
@ -84,12 +27,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// <returns> /// <returns>
/// The <see cref="Collection{ExifValue}"/>. /// The <see cref="Collection{ExifValue}"/>.
/// </returns> /// </returns>
public virtual List<IExifValue> ReadValues() public List<IExifValue> ReadValues()
{ {
var values = new List<IExifValue>(); var values = new List<IExifValue>();
// Exif header: II == 0x4949 // II == 0x4949
this.isBigEndian = this.ReadUInt16() != 0x4949; this.IsBigEndian = this.ReadUInt16() != 0x4949;
if (this.ReadUInt16() != 0x002A) if (this.ReadUInt16() != 0x002A)
{ {
@ -97,38 +40,107 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
} }
uint ifdOffset = this.ReadUInt32(); uint ifdOffset = this.ReadUInt32();
this.AddValues(values, ifdOffset); this.ReadValues(values, ifdOffset);
uint thumbnailOffset = this.ReadUInt32(); uint thumbnailOffset = this.ReadUInt32();
this.GetThumbnail(thumbnailOffset); this.GetThumbnail(thumbnailOffset);
this.AddSubIfdValues(values); this.ReadSubIfd(values);
this.LazyLoad();
foreach (Action loader in this.loaders)
{
loader();
}
return values; 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> /// <summary>
/// Adds the collection of EXIF values to the reader. /// Reads the values to the values collection.
/// </summary> /// </summary>
/// <param name="values">The values.</param> /// <param name="values">The values.</param>
/// <param name="index">The index.</param> /// <param name="offset">The IFD offset.</param>
protected void AddValues(List<IExifValue> values, uint index) protected void ReadValues(List<IExifValue> values, uint offset)
{ {
if (index > this.Length) if (offset > this.data.Length)
{ {
return; return;
} }
this.Seek(index); this.Seek(offset);
int count = this.ReadUInt16(); int count = this.ReadUInt16();
for (int i = 0; i < count; i++) 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) if (this.exifOffset != 0)
{ {
this.AddValues(values, this.exifOffset); this.ReadValues(values, this.exifOffset);
} }
if (this.gpsOffset != 0) 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 // 2 | 2 | 4 | 4
// tag | type | count | value offset // tag | type | count | value offset
if (this.RemainingLength < 12) if ((this.data.Length - this.data.Position) < 12)
{ {
return; return;
} }
@ -316,28 +328,22 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
uint size = numberOfComponents * ExifDataTypes.GetSize(dataType); uint size = numberOfComponents * ExifDataTypes.GetSize(dataType);
if (size > 4) 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 // 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)); this.AddInvalidTag(new UnkownExifTag(tag));
return; return;
} }
if (this.lazyLoaders.ContainsKey(newIndex)) this.RegisterExtLoader(newOffset, () =>
{
Debug.WriteLine($"Duplicate offset: tag={tag}, size={size}, offset={newIndex}");
}
this.lazyLoaders.Add(newIndex, () =>
{ {
var dataBuffer = new byte[size]; var dataBuffer = new byte[size];
this.Seek(newIndex); this.Seek(newOffset);
if (this.TryReadSpan(dataBuffer)) if (this.TryReadSpan(dataBuffer))
{ {
object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents); object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents);
this.Add(values, exifValue, value); this.Add(values, exifValue, value);
} }
}); });
@ -391,7 +397,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
private bool TryReadSpan(Span<byte> span) private bool TryReadSpan(Span<byte> span)
{ {
int length = span.Length; int length = span.Length;
if (this.RemainingLength < length) if ((this.data.Length - this.data.Position) < length)
{ {
span = default; span = default;
return false; return false;
@ -411,29 +417,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
? this.ConvertToShort(this.buf2) ? this.ConvertToShort(this.buf2)
: default; : 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) private double ConvertToDouble(ReadOnlySpan<byte> buffer)
{ {
if (buffer.Length < 8) if (buffer.Length < 8)
@ -538,19 +521,5 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
? BinaryPrimitives.ReadInt16BigEndian(buffer) ? BinaryPrimitives.ReadInt16BigEndian(buffer)
: BinaryPrimitives.ReadInt16LittleEndian(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); Assert.Equal(10, image.Metadata.VerticalResolution);
TiffFrameMetadata frame = image.Frames.RootFrame.Metadata.GetTiffMetadata(); 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.Width);
Assert.Equal(32u, frame.Height); 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.ImageDescription);
Assert.Equal("This is Изготовитель камеры", frame.Make); Assert.Equal("This is Изготовитель камеры", frame.Make);
Assert.Equal("This is Модель камеры", frame.Model); 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(1, frame.SamplesPerPixel);
Assert.Equal(32u, frame.RowsPerStrip); 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.HorizontalResolution);
Assert.Equal(10, frame.VerticalResolution); Assert.Equal(10, frame.VerticalResolution);
Assert.Equal(TiffPlanarConfiguration.Chunky, frame.PlanarConfiguration); Assert.Equal(TiffPlanarConfiguration.Chunky, frame.PlanarConfiguration);
@ -178,8 +178,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(TiffPredictor.None, frame.Predictor); Assert.Equal(TiffPredictor.None, frame.Predictor);
Assert.Null(frame.SampleFormat); Assert.Null(frame.SampleFormat);
Assert.Equal("This is Авторские права", frame.Copyright); Assert.Equal("This is Авторские права", frame.Copyright);
Assert.Equal(4, frame.FrameTags.GetValue<ushort>(ExifTag.Rating).Value); Assert.Equal(4, frame.ExifProfile.GetValue<ushort>(ExifTag.Rating).Value);
Assert.Equal(75, frame.FrameTags.GetValue<ushort>(ExifTag.RatingPercent).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. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using ImageMagick; using ImageMagick;
@ -54,20 +55,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
return result; return result;
} }
}
public static void Compare(Number[] a1, Number[] a2) internal class NumberComparer : IEqualityComparer<Number>
{ {
Assert.True(a1 == null ^ a2 != null); public bool Equals(Number x, Number y) => x.Equals(y);
if (a1 == null /*&& a2 == null*/)
{
return;
}
Assert.Equal(a1.Length, a2.Length); public int GetHashCode(Number obj) => obj.GetHashCode();
for (int i = 0; i < a1.Length; i++)
{
Assert.Equal((int)a1[i], (int)a2[i]);
}
}
} }
} }

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 FillOrder2 = "Tiff/b0350_fillorder2.tiff";
public const string LittleEndianByteOrder = "Tiff/little_endian.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 const string SampleMetadata = "Tiff/metadata_sample.tiff";
public static readonly string[] Multiframes = { MultiframeDeflateWithPreview, MultiframeLzwPredictor /*, MultiFrameDifferentSize, MultiframeDifferentSizeTiled, MultiFrameDifferentVariants,*/ }; public static readonly string[] Multiframes = { MultiframeDeflateWithPreview, MultiframeLzwPredictor /*, MultiFrameDifferentSize, MultiframeDifferentSizeTiled, MultiFrameDifferentVariants,*/ };
public static readonly string[] Metadata = { SampleMetadata }; 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