From 2c12c78e83bff820cb974302030ad5eff712fd29 Mon Sep 17 00:00:00 2001 From: "WINDEV2110EVAL\\User" Date: Sun, 5 Dec 2021 15:20:03 -0800 Subject: [PATCH] Added support for loading exif data from pre-2017 pngs from the "raw profile type exif" text chunk. --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 104 +++++++++++++++++- .../Formats/Png/PngDecoderTests.cs | 19 ++++ tests/ImageSharp.Tests/TestImages.cs | 3 + .../Input/Png/raw-profile-type-exif.png | 3 + 4 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 tests/Images/Input/Png/raw-profile-type-exif.png diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index cf3cd7eb14..82fc5e8159 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -112,6 +112,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// private PngChunk? nextChunk; + /// + /// "Exif" and two zero bytes. Used for the legacy exif parsing. + /// + private static readonly byte[] ExifHeader = new byte[] { 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 }; + /// /// Initializes a new instance of the class. /// @@ -182,7 +187,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.CompressedText: - this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadCompressedTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.InternationalText: this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); @@ -192,7 +197,7 @@ namespace SixLabors.ImageSharp.Formats.Png { var exifData = new byte[chunk.Length]; chunk.Data.GetSpan().CopyTo(exifData); - metadata.ExifProfile = new ExifProfile(exifData); + this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true); } break; @@ -255,7 +260,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.CompressedText: - this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadCompressedTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.InternationalText: this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); @@ -265,7 +270,7 @@ namespace SixLabors.ImageSharp.Formats.Png { var exifData = new byte[chunk.Length]; chunk.Data.GetSpan().CopyTo(exifData); - metadata.ExifProfile = new ExifProfile(exifData); + this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true); } break; @@ -937,9 +942,10 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Reads the compressed text chunk. Contains a uncompressed keyword and a compressed text string. /// + /// The object. /// The metadata to decode to. /// The containing the data. - private void ReadCompressedTextChunk(PngMetadata metadata, ReadOnlySpan data) + private void ReadCompressedTextChunk(ImageMetadata baseMetadata, PngMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { @@ -971,6 +977,94 @@ namespace SixLabors.ImageSharp.Formats.Png { metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty)); } + + if (name.Equals("Raw profile type exif", StringComparison.OrdinalIgnoreCase)) + { + this.ReadLegacyExifTextChunk(baseMetadata, uncompressed); + } + } + + /// + /// Reads exif data encoded into a text chunk with the name "raw profile type exif". + /// This method was used by ImageMagick, exiftool, exiv2, digiKam, etc, before the + /// 2017 update to png that allowed a true exif chunk. We load + /// + /// The to store the decoded exif tags into. + /// The contents of the "raw profile type exif" text chunk. + private void ReadLegacyExifTextChunk(ImageMetadata metadata, string data) + { + ReadOnlySpan dataSpan = data.AsSpan(); + dataSpan = dataSpan.TrimStart(); + + if (!dataSpan.Slice(0, 4).ToString().Equals("exif", StringComparison.OrdinalIgnoreCase)) + { + // "exif" identifier is missing from the beginning of the text chunk + return; + } + + // Skip to the data length + dataSpan = dataSpan.Slice(4).TrimStart(); + int dataLengthEnd = dataSpan.IndexOf('\n'); + int dataLength = int.Parse(dataSpan.Slice(0, dataSpan.IndexOf('\n')).ToString()); + + // Skip to the hex-encoded data + dataSpan = dataSpan.Slice(dataLengthEnd).Trim(); + string dataSpanString = dataSpan.ToString().Replace("\n", string.Empty); + if (dataSpanString.Length != (dataLength * 2)) + { + // Invalid length + return; + } + + // Parse the hex-encoded data into the byte array we are going to hand off to ExifProfile + byte[] dataBlob = new byte[dataLength - ExifHeader.Length]; + for (int i = 0; i < dataLength; i++) + { + byte parsed = Convert.ToByte(dataSpanString.Substring(i * 2, 2), 16); + if (i < ExifHeader.Length) + { + if (parsed != ExifHeader[i]) + { + // Invalid exif header in the actual data blob + return; + } + } + else + { + dataBlob[i - ExifHeader.Length] = parsed; + } + } + + this.MergeOrSetExifProfile(metadata, new ExifProfile(dataBlob), replaceExistingKeys: false); + } + + /// + /// Sets the in to , + /// or copies exif tags if already contains an . + /// + /// The to store the exif data in. + /// The to copy exif tags from. + /// If already contains an , + /// controls whether existing exif tags in will be overwritten with any conflicting + /// tags from . + private void MergeOrSetExifProfile(ImageMetadata metadata, ExifProfile newProfile, bool replaceExistingKeys) + { + if (metadata.ExifProfile is null) + { + // No exif metadata was loaded yet, so just assign it + metadata.ExifProfile = newProfile; + } + else + { + // Try to merge existing keys with the ones from the new profile + foreach (IExifValue newKey in newProfile.Values) + { + if (replaceExistingKeys || metadata.ExifProfile.GetValueInternal(newKey.Tag) is null) + { + metadata.ExifProfile.SetValueInternal(newKey.Tag, newKey.GetValue()); + } + } + } } /// diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 9fc4d03dda..215cd14a1a 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -444,5 +444,24 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png "Disco") .Dispose(); } + + [Theory] + [WithFile(TestImages.Png.Issue1875, PixelTypes.Rgba32)] + public void PngDecoder_CanDecode_LegacyTextExifChunk(TestImageProvider provider) + { + using Image image = provider.GetImage(PngDecoder); + + Assert.Equal(0, image.Metadata.ExifProfile.InvalidTags.Count); + Assert.Equal(3, image.Metadata.ExifProfile.Values.Count); + + Assert.Equal( + "A colorful tiling of blue, red, yellow, and green 4x4 pixel blocks.", + image.Metadata.ExifProfile.GetValue(ImageSharp.Metadata.Profiles.Exif.ExifTag.ImageDescription).Value); + Assert.Equal( + "Duplicated from basn3p02.png, then image metadata modified with exiv2", + image.Metadata.ExifProfile.GetValue(ImageSharp.Metadata.Profiles.Exif.ExifTag.ImageHistory).Value); + + Assert.Equal(42, (int)image.Metadata.ExifProfile.GetValue(ImageSharp.Metadata.Profiles.Exif.ExifTag.ImageNumber).Value); + } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e003649135..41e5e15d3d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -117,6 +117,9 @@ namespace SixLabors.ImageSharp.Tests // Issue 1765: https://github.com/SixLabors/ImageSharp/issues/1765 public const string Issue1765_Net6DeflateStreamRead = "Png/issues/Issue_1765_Net6DeflateStreamRead.png"; + // Discussion 1875: https://github.com/SixLabors/ImageSharp/discussions/1875 + public const string Issue1875 = "Png/raw-profile-type-exif.png"; + public static class Bad { public const string MissingDataChunk = "Png/xdtn0g01.png"; diff --git a/tests/Images/Input/Png/raw-profile-type-exif.png b/tests/Images/Input/Png/raw-profile-type-exif.png new file mode 100644 index 0000000000..efd9b35aaa --- /dev/null +++ b/tests/Images/Input/Png/raw-profile-type-exif.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2259b08fd0c4681ecd068244df358b486f5eca1fcd18edbc7d9207eeef3ca5ed +size 392