Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for loading exif data from PNG "Raw profile type exif" text chunk #1877

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2c12c78
Added support for loading exif data from pre-2017 pngs from the "raw …
jubilant-enigma Dec 5, 2021
d5ddc46
Fixed an incomplete comment
jubilant-enigma Dec 5, 2021
c8a191d
Update src/ImageSharp/Formats/Png/PngDecoderCore.cs
jubilant-enigma Dec 6, 2021
7f4c9cd
Update src/ImageSharp/Formats/Png/PngDecoderCore.cs
jubilant-enigma Dec 6, 2021
8b9c334
Moved the ExifHeader property to after the constructor to satisfy Sty…
jubilant-enigma Dec 6, 2021
c2b906e
Removed unnecessary temporary allocations.
jubilant-enigma Dec 6, 2021
c8e2902
Moved legacy exif data loading test from PngDecoderTests to PngMetada…
jubilant-enigma Dec 6, 2021
0c65e13
Don't save the exif text chunk if it is successfully parsed
jubilant-enigma Dec 7, 2021
bf3035f
Don't include unnecessary parameters for helper functions that are on…
jubilant-enigma Dec 7, 2021
2a7ec5d
Moved ExifHeader to a local variable since it's only used in one func…
jubilant-enigma Dec 28, 2021
82e664a
New, faster HexStringToBytes implementation based off the reference s…
jubilant-enigma Dec 28, 2021
0409d96
Merge branch 'is_master/master' into je/nonstandard-png-exif
jubilant-enigma Dec 28, 2021
4c0df9f
Merge branch 'master' into je/nonstandard-png-exif
JimBobSquarePants Jan 3, 2022
76261ff
Update shared-infrastructure
JimBobSquarePants Jan 3, 2022
6dba6cf
Moved HexStringToBytes into a SixLabors.ImageSharp.Common.Helpers.Hex…
jubilant-enigma Jan 3, 2022
bdb69d1
Allow reading legacy exif data from uncompressed text chunks as well.
jubilant-enigma Jan 3, 2022
47cd2a4
Merge branch 'je/nonstandard-png-exif' of https://github.com/jubilant…
jubilant-enigma Jan 3, 2022
6cdc595
Merge with remote
jubilant-enigma Jan 3, 2022
7e7ea93
Fixed comments.
jubilant-enigma Jan 3, 2022
95318b1
Update shared-infrastructure
JimBobSquarePants Jan 4, 2022
3c421bb
Merge remote-tracking branch 'upstream/master' into je/nonstandard-pn…
JimBobSquarePants Jan 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 99 additions & 5 deletions src/ImageSharp/Formats/Png/PngDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
/// </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 };
jubilant-enigma marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Initializes a new instance of the <see cref="PngDecoderCore"/> class.
/// </summary>
Expand Down Expand Up @@ -182,7 +187,7 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
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());
Expand All @@ -192,7 +197,7 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
{
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;
Expand Down Expand Up @@ -255,7 +260,7 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
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());
Expand All @@ -265,7 +270,7 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
{
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;
Expand Down Expand Up @@ -937,9 +942,10 @@ private void ReadTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data)
/// <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)
{
Expand Down Expand Up @@ -971,6 +977,94 @@ private void ReadCompressedTextChunk(PngMetadata metadata, ReadOnlySpan<byte> da
{
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.
/// </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());
jubilant-enigma marked this conversation as resolved.
Show resolved Hide resolved

// 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];
jubilant-enigma marked this conversation as resolved.
Show resolved Hide resolved
for (int i = 0; i < dataLength; i++)
{
byte parsed = Convert.ToByte(dataSpanString.Substring(i * 2, 2), 16);
jubilant-enigma marked this conversation as resolved.
Show resolved Hide resolved
if (i < ExifHeader.Length)
jubilant-enigma marked this conversation as resolved.
Show resolved Hide resolved
{
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());
jubilant-enigma marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

/// <summary>
Expand Down
19 changes: 19 additions & 0 deletions tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -444,5 +444,24 @@ static void RunTest(string providerDump, string nonContiguousBuffersStr)
"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 changes: 3 additions & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ public static class Png
// 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";
Expand Down
3 changes: 3 additions & 0 deletions tests/Images/Input/Png/raw-profile-type-exif.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.