|
|
|
@ -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; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|