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