diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs
index 17d13e86df..c5d8879853 100644
--- a/src/ImageSharp/Formats/Png/PngConstants.cs
+++ b/src/ImageSharp/Formats/Png/PngConstants.cs
@@ -62,6 +62,21 @@ internal static class PngConstants
///
public const int MinTextKeywordLength = 1;
+ ///
+ /// Specifies the keyword used to identify the Exif raw profile in image metadata.
+ ///
+ public const string ExifRawProfileKeyword = "Raw profile type exif";
+
+ ///
+ /// Specifies the profile keyword used to identify raw IPTC metadata within image files.
+ ///
+ public const string IptcRawProfileKeyword = "Raw profile type iptc";
+
+ ///
+ /// The IPTC resource id in Photoshop IRB. 0x0404 (big endian).
+ ///
+ public const ushort AdobeIptcResourceId = 0x0404;
+
///
/// Gets the header bytes identifying a Png.
///
@@ -100,4 +115,31 @@ internal static class PngConstants
(byte)'m',
(byte)'p'
];
+
+ ///
+ /// Gets the ASCII bytes for the "Photoshop 3.0" identifier used in some PNG metadata payloads.
+ /// This value is null-terminated.
+ ///
+ public static ReadOnlySpan AdobePhotoshop30 =>
+ [
+ (byte)'P',
+ (byte)'h',
+ (byte)'o',
+ (byte)'t',
+ (byte)'o',
+ (byte)'s',
+ (byte)'h',
+ (byte)'o',
+ (byte)'p',
+ (byte)' ',
+ (byte)'3',
+ (byte)'.',
+ (byte)'0',
+ 0
+ ];
+
+ ///
+ /// Gets the ASCII bytes for the "8BIM" signature used in Photoshop resources.
+ ///
+ public static ReadOnlySpan EightBim => [(byte)'8', (byte)'B', (byte)'I', (byte)'M'];
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index bff4d30ee5..271474a7e5 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -21,6 +21,7 @@
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
@@ -1440,14 +1441,19 @@ private void ReadCompressedTextChunk(ImageMetadata baseMetadata, PngMetadata met
/// object unmodified.
private static bool TryReadTextChunkMetadata(ImageMetadata baseMetadata, string chunkName, string chunkText)
{
- if (chunkName.Equals("Raw profile type exif", StringComparison.OrdinalIgnoreCase) &&
+ if (chunkName.Equals(PngConstants.ExifRawProfileKeyword, StringComparison.OrdinalIgnoreCase) &&
TryReadLegacyExifTextChunk(baseMetadata, chunkText))
{
// Successfully parsed legacy exif data from text
return true;
}
- // TODO: "Raw profile type iptc", potentially others?
+ if (chunkName.Equals(PngConstants.IptcRawProfileKeyword, StringComparison.OrdinalIgnoreCase) &&
+ TryReadLegacyIptcTextChunk(baseMetadata, chunkText))
+ {
+ // Successfully parsed legacy iptc data from text
+ return true;
+ }
// No special chunk data identified
return false;
@@ -1571,6 +1577,214 @@ private static bool TryReadLegacyExifTextChunk(ImageMetadata metadata, string da
return true;
}
+ ///
+ /// Reads iptc data encoded into a text chunk with the name "Raw profile type iptc".
+ /// This convention is used by ImageMagick/exiftool/exiv2/digiKam and stores a byte-count
+ /// followed by hex-encoded bytes.
+ ///
+ /// The to store the decoded iptc tags into.
+ /// The contents of the "Raw profile type iptc" text chunk.
+ private static bool TryReadLegacyIptcTextChunk(ImageMetadata metadata, string data)
+ {
+ // Preserve first IPTC found.
+ if (metadata.IptcProfile != null)
+ {
+ return true;
+ }
+
+ ReadOnlySpan dataSpan = data.AsSpan().TrimStart();
+
+ // Must start with the "iptc" identifier (case-insensitive).
+ // Common real-world format (ImageMagick/ExifTool) is:
+ // "IPTC profile\n \n"
+ if (dataSpan.Length < 4 || !StringEqualsInsensitive(dataSpan[..4], "iptc".AsSpan()))
+ {
+ return false;
+ }
+
+ // Skip the remainder of the first line ("IPTC profile", etc).
+ int firstLineEnd = dataSpan.IndexOf('\n');
+ if (firstLineEnd < 0)
+ {
+ return false;
+ }
+
+ dataSpan = dataSpan[(firstLineEnd + 1)..].TrimStart();
+
+ // Next line contains the decimal byte length (often indented).
+ int dataLengthEnd = dataSpan.IndexOf('\n');
+ if (dataLengthEnd < 0)
+ {
+ return false;
+ }
+
+ int dataLength;
+ try
+ {
+ dataLength = ParseInt32(dataSpan[..dataLengthEnd]);
+ }
+ catch
+ {
+ return false;
+ }
+
+ if (dataLength <= 0)
+ {
+ return false;
+ }
+
+ // Skip to the hex-encoded data.
+ dataSpan = dataSpan[(dataLengthEnd + 1)..].Trim();
+
+ byte[] iptcBlob = new byte[dataLength];
+
+ try
+ {
+ int written = 0;
+
+ for (; written < dataLength;)
+ {
+ ReadOnlySpan lineSpan = dataSpan;
+
+ int newlineIndex = dataSpan.IndexOf('\n');
+ if (newlineIndex != -1)
+ {
+ lineSpan = dataSpan[..newlineIndex];
+ }
+
+ // Important: handle CRLF and any incidental whitespace.
+ lineSpan = lineSpan.Trim(); // removes ' ', '\t', '\r', '\n', etc.
+
+ if (!lineSpan.IsEmpty)
+ {
+ written += HexConverter.HexStringToBytes(lineSpan, iptcBlob.AsSpan()[written..]);
+ }
+
+ if (newlineIndex == -1)
+ {
+ break;
+ }
+
+ dataSpan = dataSpan[(newlineIndex + 1)..];
+ }
+
+ if (written != dataLength)
+ {
+ return false;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+
+ // Prefer IRB extraction if this is Photoshop-style data (8BIM resource blocks).
+ byte[] iptcPayload = TryExtractIptcFromPhotoshopIrb(iptcBlob, out byte[] extracted)
+ ? extracted
+ : iptcBlob;
+
+ metadata.IptcProfile = new IptcProfile(iptcPayload);
+ return true;
+ }
+
+ ///
+ /// Attempts to extract IPTC metadata from a Photoshop Image Resource Block (IRB) contained within the specified
+ /// data buffer.
+ ///
+ /// This method scans the provided data for a Photoshop IRB block containing IPTC metadata and
+ /// extracts it if present. The method does not validate the contents of the IPTC data beyond locating the
+ /// appropriate resource block.
+ /// A read-only span of bytes containing the Photoshop IRB data to search for embedded IPTC metadata.
+ /// When this method returns, contains the extracted IPTC metadata as a byte array if found; otherwise, an undefined
+ /// value.
+ /// if IPTC metadata is successfully extracted from the IRB data; otherwise, .
+ private static bool TryExtractIptcFromPhotoshopIrb(ReadOnlySpan data, out byte[] iptcBytes)
+ {
+ iptcBytes = default!;
+
+ ReadOnlySpan adobePhotoshop30 = PngConstants.AdobePhotoshop30;
+
+ // Some writers include the "Photoshop 3.0\0" header, some store just IRB blocks.
+ if (data.Length >= adobePhotoshop30.Length && data[..adobePhotoshop30.Length].SequenceEqual(adobePhotoshop30))
+ {
+ data = data[adobePhotoshop30.Length..];
+ }
+
+ ReadOnlySpan eightBim = PngConstants.EightBim;
+ ushort adobeIptcResourceId = PngConstants.AdobeIptcResourceId;
+ while (data.Length >= 12)
+ {
+ if (!data[..4].SequenceEqual(eightBim))
+ {
+ return false;
+ }
+
+ data = data[4..];
+
+ // Resource ID (2 bytes, big endian)
+ if (data.Length < 2)
+ {
+ return false;
+ }
+
+ ushort resourceId = (ushort)((data[0] << 8) | data[1]);
+ data = data[2..];
+
+ // Pascal string name (1-byte length, then bytes), padded to even.
+ if (data.Length < 1)
+ {
+ return false;
+ }
+
+ int nameLen = data[0];
+ int nameFieldLen = 1 + nameLen;
+ if ((nameFieldLen & 1) != 0)
+ {
+ nameFieldLen++; // pad to even
+ }
+
+ if (data.Length < nameFieldLen + 4)
+ {
+ return false;
+ }
+
+ data = data[nameFieldLen..];
+
+ // Resource data size (4 bytes, big endian)
+ int size = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
+ data = data[4..];
+
+ if (size < 0 || data.Length < size)
+ {
+ return false;
+ }
+
+ ReadOnlySpan payload = data[..size];
+
+ // Data is padded to even.
+ int advance = size;
+ if ((advance & 1) != 0)
+ {
+ advance++;
+ }
+
+ if (resourceId == adobeIptcResourceId)
+ {
+ iptcBytes = payload.ToArray();
+ return true;
+ }
+
+ if (data.Length < advance)
+ {
+ return false;
+ }
+
+ data = data[advance..];
+ }
+
+ return false;
+ }
+
///
/// Reads the color profile chunk. The data is stored similar to the zTXt chunk.
///
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 2b01affea2..2bb97221cc 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -8,6 +8,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
+using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
@@ -217,6 +218,7 @@ public void Encode(Image image, Stream stream, CancellationToken
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
+ this.WriteIptcChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
if (image.Frames.Count > 1)
@@ -889,6 +891,163 @@ private void WriteXmpChunk(Stream stream, ImageMetadata meta)
this.WriteChunk(stream, PngChunkType.InternationalText, payload);
}
+ ///
+ /// Writes the IPTC metadata from the specified image metadata to the provided stream as a compressed zTXt chunk in
+ /// PNG format, if IPTC data is present.
+ ///
+ /// The containing image data.
+ /// The image metadata.
+ private void WriteIptcChunk(Stream stream, ImageMetadata meta)
+ {
+ if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
+ {
+ return;
+ }
+
+ if (meta.IptcProfile is null || !meta.IptcProfile.Values.Any())
+ {
+ return;
+ }
+
+ meta.IptcProfile.UpdateData();
+
+ byte[]? iptcData = meta.IptcProfile.Data;
+ if (iptcData?.Length is 0 or null)
+ {
+ return;
+ }
+
+ // For interoperability, wrap raw IPTC (IIM) in a Photoshop IRB (8BIM, resource 0x0404),
+ // since "Raw profile type iptc" commonly stores IRB payloads.
+ using IMemoryOwner irb = this.BuildPhotoshopIrbForIptc(iptcData);
+
+ Span irbSpan = irb.GetSpan();
+
+ // Build "raw profile" textual wrapper:
+ // "IPTC profile\n\n\n"
+ string rawProfileText = BuildRawProfileText("IPTC profile", irbSpan);
+
+ byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(rawProfileText));
+
+ // zTXt layout: keyword (latin-1) + 0 + compression-method(0) + compressed-data
+ const string iptcRawProfileKeyword = PngConstants.IptcRawProfileKeyword;
+ int payloadLength = iptcRawProfileKeyword.Length + compressedData.Length + 2;
+
+ using IMemoryOwner payload = this.memoryAllocator.Allocate(payloadLength);
+ Span outputBytes = payload.GetSpan();
+
+ PngConstants.Encoding.GetBytes(iptcRawProfileKeyword).CopyTo(outputBytes);
+ int bytesWritten = iptcRawProfileKeyword.Length;
+ outputBytes[bytesWritten++] = 0; // Null separator
+ outputBytes[bytesWritten++] = 0; // Compression method: deflate
+ compressedData.CopyTo(outputBytes[bytesWritten..]);
+
+ this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes);
+ }
+
+ ///
+ /// Builds a Photoshop Image Resource Block (IRB) containing the specified IPTC-IIM data.
+ ///
+ /// The returned IRB uses resource ID 0x0404 and an empty Pascal string for the name, as required
+ /// for IPTC-NAA record embedding in Photoshop files. The data is padded to ensure even length, as specified by the
+ /// IRB format.
+ ///
+ /// The IPTC-IIM data to embed in the IRB, provided as a read-only span of bytes. The data is included as-is in the
+ /// resulting block.
+ ///
+ ///
+ /// A byte array representing the Photoshop IRB with the embedded IPTC-IIM data, formatted according to the
+ /// Photoshop specification.
+ ///
+ private IMemoryOwner BuildPhotoshopIrbForIptc(ReadOnlySpan iptcIim)
+ {
+ // IRB block:
+ // 4 bytes: "8BIM"
+ // 2 bytes: resource id 0x0404 (big endian)
+ // 2 bytes: pascal name (len=0) + pad to even => 0x00 0x00
+ // 4 bytes: data size (big endian)
+ // n bytes: IPTC-IIM data
+ // pad to even
+ int pad = (iptcIim.Length & 1) != 0 ? 1 : 0;
+ IMemoryOwner bufferOwner = this.memoryAllocator.Allocate(4 + 2 + 2 + 4 + iptcIim.Length + pad);
+ Span buffer = bufferOwner.GetSpan();
+
+ int bytesWritten = 0;
+ PngConstants.EightBim.CopyTo(buffer);
+ bytesWritten += 4;
+
+ buffer[bytesWritten++] = 0x04;
+ buffer[bytesWritten++] = 0x04;
+
+ buffer[bytesWritten++] = 0x00; // Pascal name length
+ buffer[bytesWritten++] = 0x00; // pad to even
+
+ int size = iptcIim.Length;
+ buffer[bytesWritten++] = (byte)((size >> 24) & 0xFF);
+ buffer[bytesWritten++] = (byte)((size >> 16) & 0xFF);
+ buffer[bytesWritten++] = (byte)((size >> 8) & 0xFF);
+ buffer[bytesWritten++] = (byte)(size & 0xFF);
+
+ iptcIim.CopyTo(buffer[bytesWritten..]);
+
+ // Final pad byte already zero-initialized if needed
+ return bufferOwner;
+ }
+
+ ///
+ /// Builds a formatted text representation of a binary profile, including a header, the payload length, and the
+ /// payload as hexadecimal text.
+ ///
+ ///
+ /// The hexadecimal payload is formatted with 64 bytes per line to improve readability. The
+ /// output consists of the header line, a line with the payload length, and one or more lines of hexadecimal
+ /// text.
+ ///
+ /// The header text to include at the beginning of the profile. This is written as the first line of the output.
+ /// The binary payload to encode as hexadecimal text. The payload is split into lines of 64 bytes each.
+ ///
+ /// A string containing the header, the payload length, and the hexadecimal representation of the payload, each on
+ /// separate lines.
+ ///
+ private static string BuildRawProfileText(string header, ReadOnlySpan payload)
+ {
+ // Hex text can be multi-line
+ // Use 64 bytes per line (128 hex chars) to keep the chunk readable.
+ const int bytesPerLine = 64;
+
+ int hexChars = payload.Length * 2;
+ int lineCount = (payload.Length + (bytesPerLine - 1)) / bytesPerLine;
+ int newlineCount = 2 + lineCount; // header line + length line + hex lines
+ int capacity = header.Length + 32 + hexChars + newlineCount;
+
+ StringBuilder sb = new(capacity);
+ sb.Append(header).Append('\n');
+ sb.Append(payload.Length).Append('\n');
+
+ int i = 0;
+ while (i < payload.Length)
+ {
+ int take = Math.Min(bytesPerLine, payload.Length - i);
+ AppendHex(sb, payload.Slice(i, take));
+ sb.Append('\n');
+ i += take;
+ }
+
+ return sb.ToString();
+ }
+
+ private static void AppendHex(StringBuilder sb, ReadOnlySpan data)
+ {
+ const string hex = "0123456789ABCDEF";
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ byte b = data[i];
+ _ = sb.Append(hex[b >> 4]);
+ _ = sb.Append(hex[b & 0x0F]);
+ }
+ }
+
///
/// Writes the CICP profile chunk
///
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
index 2d5fe6a09a..bbbeb83e09 100644
--- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
@@ -9,12 +9,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.IPTC;
internal enum IptcRecordNumber : byte
{
///
- /// A Envelope Record.
+ /// An Envelope Record.
///
Envelope = 0x01,
///
- /// A Application Record.
+ /// An Application Record.
///
Application = 0x02
}
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
index 7735810b3f..65daf59368 100644
--- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics;
+using System.Globalization;
using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc;
@@ -9,7 +10,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc;
///
/// Represents a single value of the IPTC profile.
///
-[DebuggerDisplay("{Tag} = {ToString(),nq} ({GetType().Name,nq})")]
+[DebuggerDisplay("{Tag} = {DebuggerDisplayValue(),nq} ({GetType().Name,nq})")]
public sealed class IptcValue : IDeepCloneable
{
private byte[] data = [];
@@ -213,4 +214,37 @@ public string ToString(Encoding encoding)
return encoding.GetString(this.data);
}
+
+ private string DebuggerDisplayValue()
+ {
+ // IPTC RecordVersion (2:00) is a 2-byte binary value, commonly 0x0004.
+ // Showing it as UTF-8 produces control characters like "\0\u0004".
+ if (this.Tag == IptcTag.RecordVersion && this.data.Length == 2)
+ {
+ int version = (this.data[0] << 8) | this.data[1];
+ return version.ToString(CultureInfo.InvariantCulture);
+ }
+
+ // Prefer readable text if it looks like it, otherwise show hex.
+ // (Avoid surprising debugger output for binary payloads.)
+ bool printable = true;
+ for (int i = 0; i < this.data.Length; i++)
+ {
+ byte b = this.data[i];
+
+ // If any byte is an ASCII control character, treat this value as binary.
+ if (b is < 0x20 or 0x7F)
+ {
+ printable = false;
+ break;
+ }
+ }
+
+ if (printable)
+ {
+ return this.Value;
+ }
+
+ return Convert.ToHexString(this.data);
+ }
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
index a0c552a221..42048426ee 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
@@ -6,6 +6,7 @@
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
+using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
@@ -31,16 +32,16 @@ public void CloneIsDeep()
ColorType = PngColorType.GrayscaleWithAlpha,
InterlaceMethod = PngInterlaceMode.Adam7,
Gamma = 2,
- TextData = new List { new("name", "value", "foo", "bar") },
+ TextData = [new("name", "value", "foo", "bar")],
RepeatCount = 123,
AnimateRootFrame = false
};
- PngMetadata clone = (PngMetadata)meta.DeepClone();
+ PngMetadata clone = meta.DeepClone();
- Assert.True(meta.BitDepth == clone.BitDepth);
- Assert.True(meta.ColorType == clone.ColorType);
- Assert.True(meta.InterlaceMethod == clone.InterlaceMethod);
+ Assert.Equal(meta.BitDepth, clone.BitDepth);
+ Assert.Equal(meta.ColorType, clone.ColorType);
+ Assert.Equal(meta.InterlaceMethod, clone.InterlaceMethod);
Assert.True(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
@@ -53,15 +54,47 @@ public void CloneIsDeep()
clone.Gamma = 1;
clone.RepeatCount = 321;
- Assert.False(meta.BitDepth == clone.BitDepth);
- Assert.False(meta.ColorType == clone.ColorType);
- Assert.False(meta.InterlaceMethod == clone.InterlaceMethod);
+ Assert.NotEqual(meta.BitDepth, clone.BitDepth);
+ Assert.NotEqual(meta.ColorType, clone.ColorType);
+ Assert.NotEqual(meta.InterlaceMethod, clone.InterlaceMethod);
Assert.False(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
Assert.False(meta.RepeatCount == clone.RepeatCount);
}
+ [Theory]
+ [WithFile(TestImages.Png.IptcMetadata, PixelTypes.Rgba32)]
+ public void Decoder_CanReadIptcProfile(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder.Instance);
+ Assert.NotNull(image.Metadata.IptcProfile);
+ Assert.Equal("test1, test2", image.Metadata.IptcProfile.GetValues(IptcTag.Keywords)[0].Value);
+ Assert.Equal("\0\u0004", image.Metadata.IptcProfile.GetValues(IptcTag.RecordVersion)[0].Value);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Png.IptcMetadata, PixelTypes.Rgba32)]
+ public void Encoder_CanWriteIptcProfile(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder.Instance);
+ Assert.NotNull(image.Metadata.IptcProfile);
+ Assert.Equal("test1, test2", image.Metadata.IptcProfile.GetValues(IptcTag.Keywords)[0].Value);
+ Assert.Equal("\0\u0004", image.Metadata.IptcProfile.GetValues(IptcTag.RecordVersion)[0].Value);
+
+ using MemoryStream memoryStream = new();
+ image.Save(memoryStream, new PngEncoder());
+
+ memoryStream.Position = 0;
+
+ using Image decoded = PngDecoder.Instance.Decode(DecoderOptions.Default, memoryStream);
+ Assert.NotNull(decoded.Metadata.IptcProfile);
+ Assert.Equal("test1, test2", decoded.Metadata.IptcProfile.GetValues(IptcTag.Keywords)[0].Value);
+ Assert.Equal("\0\u0004", decoded.Metadata.IptcProfile.GetValues(IptcTag.RecordVersion)[0].Value);
+ }
+
[Theory]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
public void Decoder_CanReadTextData(TestImageProvider provider)
@@ -337,7 +370,6 @@ public void Identify_ReadsLegacyExifData(string imagePath)
Assert.Equal(42, (int)exif.GetValue(ExifTag.ImageNumber).Value);
}
-
[Theory]
[InlineData(PixelColorType.Binary, PngColorType.Palette)]
[InlineData(PixelColorType.Indexed, PngColorType.Palette)]
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index f6cd776e47..6b4a866669 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -62,6 +62,7 @@ public static class Png
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";
public const string XmpColorPalette = "Png/xmp-colorpalette.png";
public const string AdamHeadsHlg = "Png/adamHeadsHLG.png";
+ public const string IptcMetadata = "Png/iptc-profile.png";
// Animated
// https://philip.html5.org/tests/apng/tests.html
diff --git a/tests/Images/Input/Png/iptc-profile.png b/tests/Images/Input/Png/iptc-profile.png
new file mode 100644
index 0000000000..fa4199a0c8
--- /dev/null
+++ b/tests/Images/Input/Png/iptc-profile.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ae7f5d11145762b6544b3e289fc6c3bcb13a5f4cd8511b02280da683bec4c96e
+size 448011