diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs index 3c70ee591e..1e298028d6 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs +++ b/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(); } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 0cac351978..eea42af28e 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/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 /// 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 invalidTags; - private uint exifOffset; - - private uint gpsOffset; + private List 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 offsetBuffer = new byte[4]; for (int i = 0; i < count; i++) { - this.ReadValue(values); + this.ReadValue(values, offsetBuffer); } } protected void ReadSubIfd(List 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 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 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 buffer, ulong numberOfComponents) + private object ConvertValue(ExifDataType dataType, ReadOnlySpan 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 values) + private void ReadValue(List values, Span 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 values) + private void ReadValue64(List values, Span 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)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()).Add(tag); + private void AddSubIfd(object val) + => (this.subIfds ??= new List()).Add(Convert.ToUInt64(val)); + private void Seek(ulong pos) => this.data.Seek((long)pos, SeekOrigin.Begin); diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs index ac4b0a1bf8..390599b730 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs @@ -65,5 +65,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// Gets the TimeZoneOffset exif tag. /// public static ExifTag TimeZoneOffset { get; } = new ExifTag(ExifTagValue.TimeZoneOffset); + + /// + /// Gets the offset to child IFDs exif tag. + /// + public static ExifTag SubIFDs { get; } = new ExifTag(ExifTagValue.SubIFDs); } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs index 2d3a93aed3..9e9e0e7378 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs +++ b/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; diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index 7e5b35f49b..33fb90cc04 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/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); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs index 7843ba4b9c..d59e276cf3 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs +++ b/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(TestImageProvider provider) where TPixel : unmanaged, IPixel => 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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image 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)); + } } } diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 1f23838ab6..4b8551042b 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/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 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) {