Browse Source

Added support for loading exif data from pre-2017 pngs from the "raw profile type exif" text chunk.

pull/1877/head
WINDEV2110EVAL\User 5 years ago
parent
commit
2c12c78e83
  1. 104
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  2. 19
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  3. 3
      tests/ImageSharp.Tests/TestImages.cs
  4. 3
      tests/Images/Input/Png/raw-profile-type-exif.png

104
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -112,6 +112,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
private PngChunk? nextChunk;
/// <summary>
/// "Exif" and two zero bytes. Used for the legacy exif parsing.
/// </summary>
private static readonly byte[] ExifHeader = new byte[] { 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 };
/// <summary>
/// Initializes a new instance of the <see cref="PngDecoderCore"/> class.
/// </summary>
@ -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
/// <summary>
/// Reads the compressed text chunk. Contains a uncompressed keyword and a compressed text string.
/// </summary>
/// <param name="baseMetadata">The <see cref="ImageMetadata"/> object.</param>
/// <param name="metadata">The metadata to decode to.</param>
/// <param name="data">The <see cref="T:Span"/> containing the data.</param>
private void ReadCompressedTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data)
private void ReadCompressedTextChunk(ImageMetadata baseMetadata, PngMetadata metadata, ReadOnlySpan<byte> 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);
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="metadata">The <see cref="ImageMetadata"/> to store the decoded exif tags into.</param>
/// <param name="data">The contents of the "raw profile type exif" text chunk.</param>
private void ReadLegacyExifTextChunk(ImageMetadata metadata, string data)
{
ReadOnlySpan<char> 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);
}
/// <summary>
/// Sets the <see cref="ExifProfile"/> in <paramref name="metadata"/> to <paramref name="newProfile"/>,
/// or copies exif tags if <paramref name="metadata"/> already contains an <see cref="ExifProfile"/>.
/// </summary>
/// <param name="metadata">The <see cref="ImageMetadata"/> to store the exif data in.</param>
/// <param name="newProfile">The <see cref="ExifProfile"/> to copy exif tags from.</param>
/// <param name="replaceExistingKeys">If <paramref name="metadata"/> already contains an <see cref="ExifProfile"/>,
/// controls whether existing exif tags in <paramref name="metadata"/> will be overwritten with any conflicting
/// tags from <paramref name="newProfile"/>.</param>
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());
}
}
}
}
/// <summary>

19
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<Rgba32> provider)
{
using Image<Rgba32> 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);
}
}
}

3
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";

3
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
Loading…
Cancel
Save