Browse Source

Eliminate intermediate buffer allocations when reading Exif tags

af/merge-core
Jason Nelson 8 years ago
parent
commit
5405215c1a
  1. 313
      src/ImageSharp/MetaData/Profiles/Exif/ExifReader.cs

313
src/ImageSharp/MetaData/Profiles/Exif/ExifReader.cs

@ -2,10 +2,12 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Primitives;
namespace SixLabors.ImageSharp.MetaData.Profiles.Exif namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
@ -15,20 +17,20 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
/// </summary> /// </summary>
internal sealed class ExifReader internal sealed class ExifReader
{ {
private readonly Collection<ExifTag> invalidTags = new Collection<ExifTag>(); private readonly List<ExifTag> invalidTags = new List<ExifTag>();
private byte[] exifData; private byte[] exifData;
private uint currentIndex; private int position;
private bool isLittleEndian; private Endianness endianness = Endianness.BigEndian;
private uint exifOffset; private uint exifOffset;
private uint gpsOffset; private uint gpsOffset;
private uint startIndex; private int startIndex;
private delegate TDataType ConverterMethod<TDataType>(byte[] data); private delegate TDataType ConverterMethod<TDataType>(ReadOnlySpan<byte> data);
/// <summary> /// <summary>
/// Gets the invalid tags. /// Gets the invalid tags.
/// </summary> /// </summary>
public IEnumerable<ExifTag> InvalidTags => this.invalidTags; public IList<ExifTag> InvalidTags => this.invalidTags;
/// <summary> /// <summary>
/// Gets the thumbnail length in the byte stream /// Gets the thumbnail length in the byte stream
@ -47,12 +49,12 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
{ {
get get
{ {
if (this.currentIndex >= this.exifData.Length) if (this.position >= this.exifData.Length)
{ {
return 0; return 0;
} }
return this.exifData.Length - (int)this.currentIndex; return this.exifData.Length - (int)this.position;
} }
} }
@ -63,55 +65,58 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
/// <returns> /// <returns>
/// The <see cref="Collection{ExifValue}"/>. /// The <see cref="Collection{ExifValue}"/>.
/// </returns> /// </returns>
public Collection<ExifValue> Read(byte[] data) public List<ExifValue> Read(byte[] data)
{ {
DebugGuard.NotNull(data, nameof(data)); DebugGuard.NotNull(data, nameof(data));
var result = new Collection<ExifValue>(); var values = new List<ExifValue>();
this.exifData = data; this.exifData = data;
if (this.GetString(4) == "Exif") if (this.GetString(4) == "Exif")
{ {
if (this.GetShort() != 0) if (this.ReadShort() != 0)
{ {
return result; return values;
} }
this.startIndex = 6; this.startIndex = 6;
} }
else else
{ {
this.currentIndex = 0; this.position = 0;
} }
this.isLittleEndian = this.GetString(2) == "II"; if (this.GetString(2) == "II")
{
this.endianness = Endianness.LittleEndian;
}
if (this.GetShort() != 0x002A) if (this.ReadShort() != 0x002A)
{ {
return result; return values;
} }
uint ifdOffset = this.GetLong(); uint ifdOffset = this.ReadUInt32();
this.AddValues(result, ifdOffset); this.AddValues(values, (int)ifdOffset);
uint thumbnailOffset = this.GetLong(); uint thumbnailOffset = this.ReadUInt32();
this.GetThumbnail(thumbnailOffset); this.GetThumbnail((int)thumbnailOffset);
if (this.exifOffset != 0) if (this.exifOffset != 0)
{ {
this.AddValues(result, this.exifOffset); this.AddValues(values, (int)this.exifOffset);
} }
if (this.gpsOffset != 0) if (this.gpsOffset != 0)
{ {
this.AddValues(result, this.gpsOffset); this.AddValues(values, (int)this.gpsOffset);
} }
return result; return values;
} }
private static TDataType[] ToArray<TDataType>(ExifDataType dataType, byte[] data, ConverterMethod<TDataType> converter) private static TDataType[] ToArray<TDataType>(ExifDataType dataType, ReadOnlySpan<byte> data, ConverterMethod<TDataType> converter)
{ {
int dataTypeSize = (int)ExifValue.GetSize(dataType); int dataTypeSize = (int)ExifValue.GetSize(dataType);
int length = data.Length / dataTypeSize; int length = data.Length / dataTypeSize;
@ -121,7 +126,7 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
Array.Copy(data, i * dataTypeSize, buffer, 0, dataTypeSize); data.Slice(i * dataTypeSize, dataTypeSize).CopyTo(buffer);
result.SetValue(converter(buffer), i); result.SetValue(converter(buffer), i);
} }
@ -129,14 +134,14 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
return result; return result;
} }
private static byte ToByte(byte[] data) private byte ConvertToByte(ReadOnlySpan<byte> buffer) => buffer[0];
{
return data[0];
}
private static string ToString(byte[] data) private unsafe string ConvertToString(ReadOnlySpan<byte> buffer)
{ {
string result = Encoding.UTF8.GetString(data, 0, data.Length); byte[] bytes = buffer.ToArray();
string result = Encoding.UTF8.GetString(bytes, 0, buffer.Length);
int nullCharIndex = result.IndexOf('\0'); int nullCharIndex = result.IndexOf('\0');
if (nullCharIndex != -1) if (nullCharIndex != -1)
{ {
@ -151,15 +156,14 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
/// </summary> /// </summary>
/// <param name="values">The values.</param> /// <param name="values">The values.</param>
/// <param name="index">The index.</param> /// <param name="index">The index.</param>
private void AddValues(Collection<ExifValue> values, uint index) private void AddValues(IList<ExifValue> values, int index)
{ {
this.currentIndex = this.startIndex + index; this.position = this.startIndex + index;
ushort count = this.GetShort(); ushort count = this.ReadShort();
for (ushort i = 0; i < count; i++) for (ushort i = 0; i < count; i++)
{ {
ExifValue value = this.CreateValue(); if (!this.TryReadValue(out ExifValue value))
if (value == null)
{ {
continue; continue;
} }
@ -200,9 +204,9 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
} }
} }
private object ConvertValue(ExifDataType dataType, byte[] data, uint numberOfComponents) private object ConvertValue(ExifDataType dataType, ReadOnlySpan<byte> buffer, uint numberOfComponents)
{ {
if (data == null || data.Length == 0) if (buffer == null || buffer.Length == 0)
{ {
return null; return null;
} }
@ -212,106 +216,110 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
case ExifDataType.Unknown: case ExifDataType.Unknown:
return null; return null;
case ExifDataType.Ascii: case ExifDataType.Ascii:
return ToString(data); return this.ConvertToString(buffer);
case ExifDataType.Byte: case ExifDataType.Byte:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return ToByte(data); return this.ConvertToByte(buffer);
} }
return data; return buffer.ToArray();
case ExifDataType.DoubleFloat: case ExifDataType.DoubleFloat:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return this.ToDouble(data); return this.ConvertToDouble(buffer);
} }
return ToArray(dataType, data, this.ToDouble); return ToArray(dataType, buffer, this.ConvertToDouble);
case ExifDataType.Long: case ExifDataType.Long:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return this.ToLong(data); return this.ConvertToUInt64(buffer);
} }
return ToArray(dataType, data, this.ToLong); return ToArray(dataType, buffer, this.ConvertToUInt64);
case ExifDataType.Rational: case ExifDataType.Rational:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return this.ToRational(data); return this.ToRational(buffer);
} }
return ToArray(dataType, data, this.ToRational); return ToArray(dataType, buffer, this.ToRational);
case ExifDataType.Short: case ExifDataType.Short:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return this.ToShort(data); return this.ConvertToShort(buffer);
} }
return ToArray(dataType, data, this.ToShort); return ToArray(dataType, buffer, this.ConvertToShort);
case ExifDataType.SignedByte: case ExifDataType.SignedByte:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return this.ToSignedByte(data); return this.ConvertToSignedByte(buffer);
} }
return ToArray(dataType, data, this.ToSignedByte); return ToArray(dataType, buffer, this.ConvertToSignedByte);
case ExifDataType.SignedLong: case ExifDataType.SignedLong:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return this.ToSignedLong(data); return this.ToInt32(buffer);
} }
return ToArray(dataType, data, this.ToSignedLong); return ToArray(dataType, buffer, this.ToInt32);
case ExifDataType.SignedRational: case ExifDataType.SignedRational:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return this.ToSignedRational(data); return this.ToSignedRational(buffer);
} }
return ToArray(dataType, data, this.ToSignedRational); return ToArray(dataType, buffer, this.ToSignedRational);
case ExifDataType.SignedShort: case ExifDataType.SignedShort:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return this.ToSignedShort(data); return this.ConvertToSignedShort(buffer);
} }
return ToArray(dataType, data, this.ToSignedShort); return ToArray(dataType, buffer, this.ConvertToSignedShort);
case ExifDataType.SingleFloat: case ExifDataType.SingleFloat:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return this.ToSingle(data); return this.ConvertToSingle(buffer);
} }
return ToArray(dataType, data, this.ToSingle); return ToArray(dataType, buffer, this.ConvertToSingle);
case ExifDataType.Undefined: case ExifDataType.Undefined:
if (numberOfComponents == 1) if (numberOfComponents == 1)
{ {
return ToByte(data); return ConvertToByte(buffer);
} }
return data; return buffer.ToArray();
default: default:
throw new NotSupportedException(); throw new NotSupportedException();
} }
} }
private ExifValue CreateValue() private bool TryReadValue(out ExifValue exifValue)
{ {
if (this.RemainingLength < 12) if (this.RemainingLength < 12)
{ {
return null; exifValue = default;
return false;
} }
ExifTag tag = this.ToEnum(this.GetShort(), ExifTag.Unknown); ExifTag tag = this.ToEnum(this.ReadShort(), ExifTag.Unknown);
ExifDataType dataType = this.ToEnum(this.GetShort(), ExifDataType.Unknown); ExifDataType dataType = this.ToEnum(this.ReadShort(), ExifDataType.Unknown);
object value; object value;
if (dataType == ExifDataType.Unknown) if (dataType == ExifDataType.Unknown)
{ {
return new ExifValue(tag, dataType, null, false); exifValue = new ExifValue(tag, dataType, null, false);
return true;
} }
uint numberOfComponents = this.GetLong(); uint numberOfComponents = this.ReadUInt32();
// Issue #132: ExifDataType == Undefined is treated like a byte array. // 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 numberOfComponents == 0 this value can only be handled as an inline value and must fallback to 4 (bytes)
@ -321,29 +329,36 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
} }
uint size = numberOfComponents * ExifValue.GetSize(dataType); uint size = numberOfComponents * ExifValue.GetSize(dataType);
byte[] data = this.GetBytes(4);
this.TryReadSpan(4, out ReadOnlySpan<byte> data);
if (size > 4) if (size > 4)
{ {
uint oldIndex = this.currentIndex; int oldIndex = this.position;
this.currentIndex = this.ToLong(data) + this.startIndex; this.position = (int)this.ConvertToUInt64(data) + this.startIndex;
if (this.RemainingLength < size) if (this.RemainingLength < size)
{ {
this.invalidTags.Add(tag); this.invalidTags.Add(tag);
this.currentIndex = oldIndex; this.position = oldIndex;
return null;
exifValue = default;
return false;
} }
value = this.ConvertValue(dataType, this.GetBytes(size), numberOfComponents); this.TryReadSpan((int)size, out ReadOnlySpan<byte> innerData);
this.currentIndex = oldIndex;
value = this.ConvertValue(dataType, innerData, numberOfComponents);
this.position = oldIndex;
} }
else else
{ {
value = this.ConvertValue(dataType, data, numberOfComponents); value = this.ConvertValue(dataType, data, numberOfComponents);
} }
bool isArray = value != null && numberOfComponents > 1; exifValue = new ExifValue(tag, dataType, value, isArray: value != null && numberOfComponents > 1);
return new ExifValue(tag, dataType, value, isArray);
return true;
} }
private TEnum ToEnum<TEnum>(int value, TEnum defaultValue) private TEnum ToEnum<TEnum>(int value, TEnum defaultValue)
@ -358,51 +373,57 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
return defaultValue; return defaultValue;
} }
private byte[] GetBytes(uint length) private bool TryReadSpan(int length, out ReadOnlySpan<byte> span)
{ {
if (this.currentIndex + length > (uint)this.exifData.Length) if (this.position + length > this.exifData.Length || this.position < 0)
{ {
return null; span = default;
return false;
} }
byte[] data = new byte[length]; span = new ReadOnlySpan<byte>(this.exifData, this.position, length);
Array.Copy(this.exifData, (int)this.currentIndex, data, 0, (int)length);
this.currentIndex += length; this.position += length;
return data; return true;
} }
private uint GetLong() private uint ReadUInt32()
{ {
return this.ToLong(this.GetBytes(4)); // Known as Long in Exif Specification
return this.TryReadSpan(4, out ReadOnlySpan<byte> span)
? this.ConvertToUInt64(span)
: default;
} }
private ushort GetShort() private ushort ReadShort()
{ {
return this.ToShort(this.GetBytes(2)); return this.TryReadSpan(2, out ReadOnlySpan<byte> span)
? this.ConvertToShort(span)
: default;
} }
private string GetString(uint length) private string GetString(int length)
{ {
byte[] data = this.GetBytes(length); if (this.TryReadSpan(length, out ReadOnlySpan<byte> span) && span.Length != 0)
if (data == null || data.Length == 0)
{ {
return null; return this.ConvertToString(span);
} }
return ToString(data); return null;
} }
private void GetThumbnail(uint offset) private void GetThumbnail(int offset)
{ {
var values = new Collection<ExifValue>(); var values = new List<ExifValue>();
this.AddValues(values, offset); this.AddValues(values, offset);
foreach (ExifValue value in values) foreach (ExifValue value in values)
{ {
if (value.Tag == ExifTag.JPEGInterchangeFormat && (value.DataType == ExifDataType.Long)) if (value.Tag == ExifTag.JPEGInterchangeFormat && (value.DataType == ExifDataType.Long))
{ {
this.ThumbnailOffset = (uint)value.Value + this.startIndex; this.ThumbnailOffset = (uint)value.Value + (uint)this.startIndex;
} }
else if (value.Tag == ExifTag.JPEGInterchangeFormatLength && value.DataType == ExifDataType.Long) else if (value.Tag == ExifTag.JPEGInterchangeFormatLength && value.DataType == ExifDataType.Long)
{ {
@ -411,120 +432,112 @@ namespace SixLabors.ImageSharp.MetaData.Profiles.Exif
} }
} }
private double ToDouble(byte[] data) private unsafe double ConvertToDouble(ReadOnlySpan<byte> buffer)
{ {
if (!this.ValidateArray(data, 8)) if (buffer.Length < 8)
{ {
return default; return default;
} }
return BitConverter.ToDouble(data, 0); long intValue = this.endianness == Endianness.BigEndian
? BinaryPrimitives.ReadInt64BigEndian(buffer)
: BinaryPrimitives.ReadInt64LittleEndian(buffer);
return *((double*)&intValue);
} }
private uint ToLong(byte[] data) private uint ConvertToUInt64(ReadOnlySpan<byte> buffer)
{ {
if (!this.ValidateArray(data, 4)) // Known as Long in Exif Specification
if (buffer.Length < 4)
{ {
return default; return default;
} }
return BitConverter.ToUInt32(data, 0); return this.endianness == Endianness.BigEndian
? BinaryPrimitives.ReadUInt32BigEndian(buffer)
: BinaryPrimitives.ReadUInt32LittleEndian(buffer);
} }
private ushort ToShort(byte[] data) private ushort ConvertToShort(ReadOnlySpan<byte> buffer)
{ {
if (!this.ValidateArray(data, 2)) if (buffer.Length < 2)
{ {
return default; return default;
} }
return BitConverter.ToUInt16(data, 0); return this.endianness == Endianness.BigEndian
? BinaryPrimitives.ReadUInt16BigEndian(buffer)
: BinaryPrimitives.ReadUInt16LittleEndian(buffer);
} }
private float ToSingle(byte[] data) private unsafe float ConvertToSingle(ReadOnlySpan<byte> buffer)
{ {
if (!this.ValidateArray(data, 4)) if (buffer.Length < 4)
{ {
return default; return default;
} }
return BitConverter.ToSingle(data, 0); int intValue = this.endianness == Endianness.BigEndian
? BinaryPrimitives.ReadInt32BigEndian(buffer)
: BinaryPrimitives.ReadInt32LittleEndian(buffer);
return *((float*)&intValue);
} }
private Rational ToRational(byte[] data) private Rational ToRational(ReadOnlySpan<byte> buffer)
{ {
if (!this.ValidateArray(data, 8, 4)) if (buffer.Length < 8)
{ {
return default(Rational); return default;
} }
uint numerator = BitConverter.ToUInt32(data, 0); uint numerator = ConvertToUInt64(buffer.Slice(0, 4));
uint denominator = BitConverter.ToUInt32(data, 4); uint denominator = ConvertToUInt64(buffer.Slice(4, 4));
return new Rational(numerator, denominator, false); return new Rational(numerator, denominator, false);
} }
private sbyte ToSignedByte(byte[] data) private sbyte ConvertToSignedByte(ReadOnlySpan<byte> buffer)
{ {
return unchecked((sbyte)data[0]); return unchecked((sbyte)buffer[0]);
} }
private int ToSignedLong(byte[] data) private int ToInt32(ReadOnlySpan<byte> buffer) // SignedLong in Exif Specification
{ {
if (!this.ValidateArray(data, 4)) if (buffer.Length < 4)
{ {
return default(int); return default;
} }
return BitConverter.ToInt32(data, 0); return this.endianness == Endianness.BigEndian
? BinaryPrimitives.ReadInt32BigEndian(buffer)
: BinaryPrimitives.ReadInt32LittleEndian(buffer);
} }
private SignedRational ToSignedRational(byte[] data) private SignedRational ToSignedRational(ReadOnlySpan<byte> buffer)
{ {
if (!this.ValidateArray(data, 8, 4)) if (buffer.Length < 8)
{ {
return default; return default;
} }
int numerator = BitConverter.ToInt32(data, 0); int numerator = this.ToInt32(buffer.Slice(0, 4));
int denominator = BitConverter.ToInt32(data, 4); int denominator = this.ToInt32(buffer.Slice(4, 4));
return new SignedRational(numerator, denominator, false); return new SignedRational(numerator, denominator, false);
} }
private short ToSignedShort(byte[] data) private short ConvertToSignedShort(ReadOnlySpan<byte> buffer)
{ {
if (!this.ValidateArray(data, 2)) if (buffer.Length < 2)
{ {
return default; return default;
} }
return BitConverter.ToInt16(data, 0); return this.endianness == Endianness.BigEndian
} ? BinaryPrimitives.ReadInt16BigEndian(buffer)
: BinaryPrimitives.ReadInt16LittleEndian(buffer);
private bool ValidateArray(byte[] data, int size)
{
return this.ValidateArray(data, size, size);
}
private bool ValidateArray(byte[] data, int size, int stepSize)
{
if (data == null || data.Length < size)
{
return false;
}
if (this.isLittleEndian == BitConverter.IsLittleEndian)
{
return true;
}
for (int i = 0; i < data.Length; i += stepSize)
{
Array.Reverse(data, i, stepSize);
}
return true;
} }
} }
} }
Loading…
Cancel
Save