Browse Source

Implement reading SubIfds (Code 330 (hex 0x014A)), ExifReader

improvements
pull/1760/head
Ildar Khayrutdinov 5 years ago
parent
commit
7fe80cfd9c
  1. 2
      src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
  2. 110
      src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
  3. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
  4. 10
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs
  5. 1
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs
  6. 28
      tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs
  7. 6
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs

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

@ -71,9 +71,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
reader.ExtTags.Sort((t1, t2) => t1.offset.CompareTo(t2.offset));
// this means that most likely all elements are placed before next IFD
if (reader.ExtTags[0].offset < reader.NextIfdOffset)
{
// this means that most likely all elements are placed before next IFD
reader.ReadExtValues();
}
}

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

@ -39,13 +39,14 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
uint ifdOffset = this.ReadUInt32();
this.ReadValues(values, ifdOffset);
this.ReadExtValues(values);
uint thumbnailOffset = this.ReadUInt32();
this.GetThumbnail(thumbnailOffset);
this.ReadSubIfd(values);
this.ReadExtValues(values);
return values;
}
@ -78,8 +79,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary>
internal abstract class BaseExifReader
{
private readonly byte[] offsetBuffer = new byte[4];
private readonly byte[] offsetBuffer8 = new byte[8];
private readonly byte[] buf8 = new byte[8];
private readonly byte[] buf4 = new byte[4];
private readonly byte[] buf2 = new byte[2];
@ -87,9 +86,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
private readonly Stream data;
private List<ExifTag> invalidTags;
private uint exifOffset;
private uint gpsOffset;
private List<ulong> subIfds;
protected BaseExifReader(Stream stream) =>
this.data = stream ?? throw new ArgumentNullException(nameof(stream));
@ -140,22 +137,23 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
this.Seek(offset);
int count = this.ReadUInt16();
Span<byte> offsetBuffer = new byte[4];
for (int i = 0; i < count; i++)
{
this.ReadValue(values);
this.ReadValue(values, offsetBuffer);
}
}
protected void ReadSubIfd(List<IExifValue> values)
{
if (this.exifOffset != 0)
if (this.subIfds is null)
{
this.ReadValues(values, this.exifOffset);
return;
}
if (this.gpsOffset != 0)
foreach (ulong subIfdOffset in this.subIfds)
{
this.ReadValues(values, this.gpsOffset);
this.ReadValues(values, (uint)subIfdOffset);
}
}
@ -169,9 +167,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
this.Seek(offset);
ulong count = this.ReadUInt64();
Span<byte> offsetBuffer = new byte[8];
for (ulong i = 0; i < count; i++)
{
this.ReadValue64(values);
this.ReadValue64(values, offsetBuffer);
}
}
@ -182,21 +181,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
this.Seek(tag.offset);
if (this.TryReadSpan(dataBuffer))
{
object value = this.ConvertValue(tag.dataType, dataBuffer, tag.numberOfComponents);
object value = this.ConvertValue(tag.dataType, dataBuffer, tag.numberOfComponents > 1 || tag.exif.IsArray);
this.Add(values, tag.exif, value);
}
}
protected void ReadSubIfd64(List<IExifValue> values)
{
if (this.exifOffset != 0)
if (this.subIfds is null)
{
this.ReadValues64(values, this.exifOffset);
return;
}
if (this.gpsOffset != 0)
foreach (ulong subIfdOffset in this.subIfds)
{
this.ReadValues64(values, this.gpsOffset);
this.ReadValues64(values, subIfdOffset);
}
}
@ -231,7 +230,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
return Encoding.UTF8.GetString(buffer);
}
private object ConvertValue(ExifDataType dataType, ReadOnlySpan<byte> buffer, ulong numberOfComponents)
private object ConvertValue(ExifDataType dataType, ReadOnlySpan<byte> buffer, bool isArray)
{
if (buffer.Length == 0)
{
@ -245,102 +244,104 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
case ExifDataType.Ascii:
return this.ConvertToString(buffer);
case ExifDataType.Byte:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ConvertToByte(buffer);
}
return buffer.ToArray();
case ExifDataType.DoubleFloat:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ConvertToDouble(buffer);
}
return ToArray(dataType, buffer, this.ConvertToDouble);
case ExifDataType.Long:
if (numberOfComponents == 1)
case ExifDataType.Ifd:
if (!isArray)
{
return this.ConvertToUInt32(buffer);
}
return ToArray(dataType, buffer, this.ConvertToUInt32);
case ExifDataType.Rational:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ToRational(buffer);
}
return ToArray(dataType, buffer, this.ToRational);
case ExifDataType.Short:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ConvertToShort(buffer);
}
return ToArray(dataType, buffer, this.ConvertToShort);
case ExifDataType.SignedByte:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ConvertToSignedByte(buffer);
}
return ToArray(dataType, buffer, this.ConvertToSignedByte);
case ExifDataType.SignedLong:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ConvertToInt32(buffer);
}
return ToArray(dataType, buffer, this.ConvertToInt32);
case ExifDataType.SignedRational:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ToSignedRational(buffer);
}
return ToArray(dataType, buffer, this.ToSignedRational);
case ExifDataType.SignedShort:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ConvertToSignedShort(buffer);
}
return ToArray(dataType, buffer, this.ConvertToSignedShort);
case ExifDataType.SingleFloat:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ConvertToSingle(buffer);
}
return ToArray(dataType, buffer, this.ConvertToSingle);
case ExifDataType.Long8:
if (numberOfComponents == 1)
case ExifDataType.Ifd8:
if (!isArray)
{
return this.ConvertToUInt64(buffer);
}
return ToArray(dataType, buffer, this.ConvertToUInt64);
case ExifDataType.SignedLong8:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ConvertToInt64(buffer);
}
return ToArray(dataType, buffer, this.ConvertToUInt64);
case ExifDataType.Undefined:
if (numberOfComponents == 1)
if (!isArray)
{
return this.ConvertToByte(buffer);
}
return buffer.ToArray();
default:
throw new NotSupportedException();
throw new NotSupportedException($"Data type {dataType} is not supported.");
}
}
private void ReadValue(List<IExifValue> values)
private void ReadValue(List<IExifValue> values, Span<byte> offsetBuffer)
{
// 2 | 2 | 4 | 4
// tag | type | count | value offset
@ -354,7 +355,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
uint numberOfComponents = this.ReadUInt32();
this.TryReadSpan(this.offsetBuffer);
this.TryReadSpan(offsetBuffer);
// Ensure that the data type is valid
if (dataType == ExifDataType.Unknown)
@ -380,7 +381,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
uint size = numberOfComponents * ExifDataTypes.GetSize(dataType);
if (size > 4)
{
uint newOffset = this.ConvertToUInt32(this.offsetBuffer);
uint newOffset = this.ConvertToUInt32(offsetBuffer);
// Ensure that the new index does not overrun the data.
if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length)
@ -393,12 +394,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
}
else
{
object value = this.ConvertValue(dataType, this.offsetBuffer, numberOfComponents);
object value = this.ConvertValue(dataType, offsetBuffer.Slice(0, (int)size), numberOfComponents > 1 || exifValue.IsArray);
this.Add(values, exifValue, value);
}
}
private void ReadValue64(List<IExifValue> values)
private void ReadValue64(List<IExifValue> values, Span<byte> offsetBuffer)
{
if ((this.data.Length - this.data.Position) < 20)
{
@ -410,7 +411,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
ulong numberOfComponents = this.ReadUInt64();
this.TryReadSpan(this.offsetBuffer8);
this.TryReadSpan(offsetBuffer);
if (dataType == ExifDataType.Unknown)
{
@ -442,12 +443,15 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
case ExifTagValue.TileByteCounts:
exifValue = new ExifLong8Array(ExifTagValue.TileByteCounts);
break;
////case ExifTagValue.SubIFDOffset:
//// exifValue = new ExifLong8Array(ExifTagValue.SubIFDOffset);
//// break;
////case ExifTagValue.GPSIFDOffset:
//// exifValue = new ExifLong8Array(ExifTagValue.GPSIFDOffset);
//// break;
case ExifTagValue.SubIFDOffset:
exifValue = new ExifLong8(ExifTagValue.SubIFDOffset);
break;
case ExifTagValue.GPSIFDOffset:
exifValue = new ExifLong8(ExifTagValue.GPSIFDOffset);
break;
case ExifTagValue.SubIFDs:
exifValue = new ExifLong8Array(ExifTagValue.SubIFDs);
break;
default:
exifValue = exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents);
break;
@ -462,7 +466,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
ulong size = numberOfComponents * ExifDataTypes.GetSize(dataType);
if (size > 8)
{
ulong newOffset = this.ConvertToUInt64(this.offsetBuffer8);
ulong newOffset = this.ConvertToUInt64(offsetBuffer);
if (newOffset > ulong.MaxValue || newOffset > ((ulong)this.data.Length - size))
{
this.AddInvalidTag(new UnkownExifTag(tag));
@ -473,7 +477,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
}
else
{
object value = this.ConvertValue(dataType, ((Span<byte>)this.offsetBuffer8).Slice(0, (int)size), numberOfComponents);
object value = this.ConvertValue(dataType, offsetBuffer.Slice(0, (int)size), numberOfComponents > 1 || exifValue.IsArray);
this.Add(values, exifValue, value);
}
}
@ -497,11 +501,18 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
if (exif.Tag == ExifTag.SubIFDOffset)
{
this.exifOffset = (uint)value;
this.AddSubIfd(value);
}
else if (exif.Tag == ExifTag.GPSIFDOffset)
{
this.gpsOffset = (uint)value;
this.AddSubIfd(value);
}
else if (exif.Tag == ExifTag.SubIFDs)
{
foreach (object val in (Array)value)
{
this.AddSubIfd(val);
}
}
else
{
@ -512,6 +523,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
private void AddInvalidTag(ExifTag tag)
=> (this.invalidTags ??= new List<ExifTag>()).Add(tag);
private void AddSubIfd(object val)
=> (this.subIfds ??= new List<ulong>()).Add(Convert.ToUInt64(val));
private void Seek(ulong pos)
=> this.data.Seek((long)pos, SeekOrigin.Begin);

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

@ -65,5 +65,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// Gets the TimeZoneOffset exif tag.
/// </summary>
public static ExifTag<uint[]> TimeZoneOffset { get; } = new ExifTag<uint[]>(ExifTagValue.TimeZoneOffset);
/// <summary>
/// Gets the offset to child IFDs exif tag.
/// </summary>
public static ExifTag<uint[]> SubIFDs { get; } = new ExifTag<uint[]>(ExifTagValue.SubIFDs);
}
}

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

@ -49,18 +49,18 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
return this.SetSingle(val);
case uint val:
return this.SetSingle(val);
case short val:
return this.SetSingle(val);
case ushort val:
return this.SetSingle(val);
case int[] array:
return this.SetArray(array);
case short val:
return this.SetSingle(val);
case uint[] array:
return this.SetArray(array);
case short[] array:
case int[] array:
return this.SetArray(array);
case ushort[] array:
return this.SetArray(array);
case short[] array:
return this.SetArray(array);
}
return false;

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

@ -92,6 +92,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
case ExifTagValue.StripRowCounts: return new ExifLongArray(ExifTag.StripRowCounts);
case ExifTagValue.IntergraphRegisters: return new ExifLongArray(ExifTag.IntergraphRegisters);
case ExifTagValue.TimeZoneOffset: return new ExifLongArray(ExifTag.TimeZoneOffset);
case ExifTagValue.SubIFDs: return new ExifLongArray(ExifTag.SubIFDs);
case ExifTagValue.ImageWidth: return new ExifNumber(ExifTag.ImageWidth);
case ExifTagValue.ImageLength: return new ExifNumber(ExifTag.ImageLength);

28
tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs

@ -3,8 +3,10 @@
// ReSharper disable InconsistentNaming
using System;
using System.Linq;
using System.IO;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
@ -26,6 +28,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[WithFile(BigTIFFMotorolaLongStrips, PixelTypes.Rgba32)]
[WithFile(BigTIFFSubIFD4, PixelTypes.Rgba32)]
[WithFile(BigTIFFSubIFD8, PixelTypes.Rgba32)]
[WithFile(Indexed4_Deflate, PixelTypes.Rgba32)]
[WithFile(Indexed8_LZW, PixelTypes.Rgba32)]
[WithFile(RLE, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
@ -42,6 +47,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[InlineData(BigTIFFMotorolaLongStrips, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(BigTIFFSubIFD4, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(BigTIFFSubIFD8, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(Indexed4_Deflate, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(Indexed8_LZW, 8, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(RLE, 1, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)]
public void Identify(string imagePath, int expectedPixelSize, int expectedWidth, int expectedHeight, double expectedHResolution, double expectedVResolution, PixelResolutionUnit expectedResolutionUnit)
{
var testFile = TestFile.Create(imagePath);
@ -82,5 +90,25 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(expectedByteOrder, img.Metadata.GetTiffMetadata().ByteOrder);
}
}
[Theory]
[WithFile(BigTIFFSubIFD8, PixelTypes.Rgba32)]
public void TiffDecoder_SubIfd8<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(TiffDecoder);
ExifProfile meta = image.Frames.RootFrame.Metadata.ExifProfile;
Assert.Equal(0, meta.InvalidTags.Count);
Assert.Equal(15, meta.Values.Count);
Assert.Equal(64, (int)meta.GetValue(ExifTag.ImageWidth).Value);
Assert.Equal(64, (int)meta.GetValue(ExifTag.ImageLength).Value);
Assert.Equal(64, (int)meta.GetValue(ExifTag.RowsPerStrip).Value);
Assert.Equal(2, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.ImageWidth));
Assert.Equal(2, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripOffsets));
Assert.Equal(2, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripByteCounts));
}
}
}

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

@ -168,8 +168,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif
2 x due to use of non-standard padding tag 0xEA1C listed in EXIF Tool. We can read those values but adhere
strictly to the 2.3.1 specification when writing. (TODO: Support 2.3.2)
https://exiftool.org/TagNames/EXIF.html */
[InlineData(TestImageWriteFormat.Jpeg, 16)]
[InlineData(TestImageWriteFormat.Png, 16)]
[InlineData(TestImageWriteFormat.Jpeg, 18)]
[InlineData(TestImageWriteFormat.Png, 18)]
public void SetValue(TestImageWriteFormat imageFormat, int expectedProfileValueCount)
{
Image<Rgba32> image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image();
@ -519,7 +519,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif
// todo: duplicate tags
Assert.Equal(2, profile.Values.Count(v => (ushort)v.Tag == 59932));
Assert.Equal(16, profile.Values.Count);
Assert.Equal(18, profile.Values.Count);
foreach (IExifValue value in profile.Values)
{

Loading…
Cancel
Save