diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
index 87b486ea6..325d7780a 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
@@ -28,6 +28,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
(byte)'I', (byte)'L', (byte)'E', (byte)'\0'
};
+ ///
+ /// Gets the adobe photoshop APP13 marker which can contain IPTC meta data.
+ ///
+ public static ReadOnlySpan AdobePhotoshopApp13Marker => new[]
+ {
+ (byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', (byte)'\0'
+ };
+
+ ///
+ /// Gets the 8BIM marker, which signals the start of a adobe specific image resource block.
+ ///
+ public static ReadOnlySpan AdobeImageResourceBlockMarker => new[]
+ {
+ (byte)'8', (byte)'B', (byte)'I', (byte)'M'
+ };
+
+ ///
+ /// Gets a IPTC Image resource ID.
+ ///
+ public static ReadOnlySpan AdobeIptcMarker => new[]
+ {
+ (byte)4, (byte)4
+ };
+
///
/// Gets the EXIF specific markers.
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index 951fec1d4..4000fa0f6 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
@@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg
@@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private readonly byte[] markerBuffer = new byte[2];
///
- /// The DC Huffman tables
+ /// The DC Huffman tables.
///
private HuffmanTable[] dcHuffmanTables;
@@ -56,37 +57,47 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private HuffmanTable[] acHuffmanTables;
///
- /// The reset interval determined by RST markers
+ /// The reset interval determined by RST markers.
///
private ushort resetInterval;
///
- /// Whether the image has an EXIF marker
+ /// Whether the image has an EXIF marker.
///
private bool isExif;
///
- /// Contains exif data
+ /// Contains exif data.
///
private byte[] exifData;
///
- /// Whether the image has an ICC marker
+ /// Whether the image has an ICC marker.
///
private bool isIcc;
///
- /// Contains ICC data
+ /// Contains ICC data.
///
private byte[] iccData;
///
- /// Contains information about the JFIF marker
+ /// Whether the image has a IPTC data.
+ ///
+ private bool isIptc;
+
+ ///
+ /// Contains IPTC data.
+ ///
+ private byte[] iptcData;
+
+ ///
+ /// Contains information about the JFIF marker.
///
private JFifMarker jFif;
///
- /// Contains information about the Adobe marker
+ /// Contains information about the Adobe marker.
///
private AdobeMarker adobe;
@@ -213,6 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.ParseStream(stream);
this.InitExifProfile();
this.InitIccProfile();
+ this.InitIptcProfile();
this.InitDerivedMetadataProperties();
return this.PostProcessIntoImage();
}
@@ -226,6 +238,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.ParseStream(stream, true);
this.InitExifProfile();
this.InitIccProfile();
+ this.InitIptcProfile();
this.InitDerivedMetadataProperties();
return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.Metadata);
@@ -344,10 +357,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.APP10:
case JpegConstants.Markers.APP11:
case JpegConstants.Markers.APP12:
- case JpegConstants.Markers.APP13:
this.InputStream.Skip(remaining);
break;
+ case JpegConstants.Markers.APP13:
+ this.ProcessApp13Marker(remaining);
+ break;
+
case JpegConstants.Markers.APP14:
this.ProcessApp14Marker(remaining);
break;
@@ -437,6 +453,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
+ ///
+ /// Initializes the IPTC profile.
+ ///
+ private void InitIptcProfile()
+ {
+ if (this.isIptc)
+ {
+ var profile = new IptcProfile(this.iptcData);
+ this.Metadata.IptcProfile = profile;
+ }
+ }
+
///
/// Assigns derived metadata properties to , eg. horizontal and vertical resolution if it has a JFIF header.
///
@@ -582,6 +610,95 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
+ ///
+ /// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop.
+ /// The content of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks.
+ ///
+ /// The remaining bytes in the segment block.
+ private void ProcessApp13Marker(int remaining)
+ {
+ if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length || this.IgnoreMetadata)
+ {
+ this.InputStream.Skip(remaining);
+ return;
+ }
+
+ this.InputStream.Read(this.temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length);
+ remaining -= ProfileResolver.AdobePhotoshopApp13Marker.Length;
+ if (ProfileResolver.IsProfile(this.temp, ProfileResolver.AdobePhotoshopApp13Marker))
+ {
+ var resourceBlockData = new byte[remaining];
+ this.InputStream.Read(resourceBlockData, 0, remaining);
+ Span blockDataSpan = resourceBlockData.AsSpan();
+
+ while (blockDataSpan.Length > 12)
+ {
+ if (!ProfileResolver.IsProfile(blockDataSpan.Slice(0, 4), ProfileResolver.AdobeImageResourceBlockMarker))
+ {
+ return;
+ }
+
+ blockDataSpan = blockDataSpan.Slice(4);
+ Span imageResourceBlockId = blockDataSpan.Slice(0, 2);
+ if (ProfileResolver.IsProfile(imageResourceBlockId, ProfileResolver.AdobeIptcMarker))
+ {
+ var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan);
+ var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength);
+ int dataStartIdx = 2 + resourceBlockNameLength + 4;
+ if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize)
+ {
+ this.isIptc = true;
+ this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray();
+ break;
+ }
+ }
+ else
+ {
+ var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan);
+ var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength);
+ int dataStartIdx = 2 + resourceBlockNameLength + 4;
+ if (blockDataSpan.Length < dataStartIdx + resourceDataSize)
+ {
+ // Not enough data or the resource data size is wrong.
+ break;
+ }
+
+ blockDataSpan = blockDataSpan.Slice(dataStartIdx + resourceDataSize);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Reads the adobe image resource block name: a Pascal string (padded to make size even).
+ ///
+ /// The span holding the block resource data.
+ /// The length of the name.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static int ReadImageResourceNameLength(Span blockDataSpan)
+ {
+ byte nameLength = blockDataSpan[2];
+ var nameDataSize = nameLength == 0 ? 2 : nameLength;
+ if (nameDataSize % 2 != 0)
+ {
+ nameDataSize++;
+ }
+
+ return nameDataSize;
+ }
+
+ ///
+ /// Reads the length of a adobe image resource data block.
+ ///
+ /// The span holding the block resource data.
+ /// The length of the block name.
+ /// The block length.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static int ReadResourceDataLength(Span blockDataSpan, int resourceBlockNameLength)
+ {
+ return BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4));
+ }
+
///
/// Processes the application header containing the Adobe identifier
/// which stores image encoding information for DCT filters.
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index 32f4d2287..eed95c6b0 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
@@ -4,6 +4,7 @@
using System;
using System.Buffers.Binary;
using System.IO;
+using System.Linq;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -13,6 +14,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg
@@ -231,7 +233,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Write the Start Of Image marker.
this.WriteApplicationHeader(metadata);
- // Write Exif and ICC profiles
+ // Write Exif, ICC and IPTC profiles
this.WriteProfiles(metadata);
// Write the quantization tables.
@@ -647,9 +649,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Writes the EXIF profile.
///
/// The exif profile.
- ///
- /// Thrown if the EXIF profile size exceeds the limit
- ///
private void WriteExifProfile(ExifProfile exifProfile)
{
if (exifProfile is null || exifProfile.Values.Count == 0)
@@ -697,16 +696,68 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
+ ///
+ /// Writes the IPTC metadata.
+ ///
+ /// The iptc metadata to write.
+ ///
+ /// Thrown if the IPTC profile size exceeds the limit of 65533 bytes.
+ ///
+ private void WriteIptcProfile(IptcProfile iptcProfile)
+ {
+ const int Max = 65533;
+ if (iptcProfile is null || !iptcProfile.Values.Any())
+ {
+ return;
+ }
+
+ iptcProfile.UpdateData();
+ byte[] data = iptcProfile.Data;
+ if (data.Length == 0)
+ {
+ return;
+ }
+
+ if (data.Length > Max)
+ {
+ throw new ImageFormatException($"Iptc profile size exceeds limit of {Max} bytes");
+ }
+
+ var app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length +
+ ProfileResolver.AdobeImageResourceBlockMarker.Length +
+ ProfileResolver.AdobeIptcMarker.Length +
+ 2 + 4 + data.Length;
+ this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13);
+ this.outputStream.Write(ProfileResolver.AdobePhotoshopApp13Marker);
+ this.outputStream.Write(ProfileResolver.AdobeImageResourceBlockMarker);
+ this.outputStream.Write(ProfileResolver.AdobeIptcMarker);
+ this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even)
+ this.outputStream.WriteByte(0);
+ BinaryPrimitives.WriteInt32BigEndian(this.buffer, data.Length);
+ this.outputStream.Write(this.buffer, 0, 4);
+ this.outputStream.Write(data, 0, data.Length);
+ }
+
///
/// Writes the App1 header.
///
- /// The length of the data the app1 marker contains
+ /// The length of the data the app1 marker contains.
private void WriteApp1Header(int app1Length)
+ {
+ this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1);
+ }
+
+ ///
+ /// Writes a AppX header.
+ ///
+ /// The length of the data the app marker contains.
+ /// The app marker to write.
+ private void WriteAppHeader(int length, byte appMarker)
{
this.buffer[0] = JpegConstants.Markers.XFF;
- this.buffer[1] = JpegConstants.Markers.APP1; // Application Marker
- this.buffer[2] = (byte)((app1Length >> 8) & 0xFF);
- this.buffer[3] = (byte)(app1Length & 0xFF);
+ this.buffer[1] = appMarker;
+ this.buffer[2] = (byte)((length >> 8) & 0xFF);
+ this.buffer[3] = (byte)(length & 0xFF);
this.outputStream.Write(this.buffer, 0, 4);
}
@@ -805,6 +856,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
metadata.SyncProfiles();
this.WriteExifProfile(metadata.ExifProfile);
this.WriteIccProfile(metadata.IccProfile);
+ this.WriteIptcProfile(metadata.IptcProfile);
}
///
diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs
index b3751bfbd..716e89e68 100644
--- a/src/ImageSharp/Metadata/ImageMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageMetadata.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
namespace SixLabors.ImageSharp.Metadata
{
@@ -65,6 +66,7 @@ namespace SixLabors.ImageSharp.Metadata
this.ExifProfile = other.ExifProfile?.DeepClone();
this.IccProfile = other.IccProfile?.DeepClone();
+ this.IptcProfile = other.IptcProfile?.DeepClone();
}
///
@@ -122,6 +124,11 @@ namespace SixLabors.ImageSharp.Metadata
///
public IccProfile IccProfile { get; set; }
+ ///
+ /// Gets or sets the iptc profile.
+ ///
+ public IptcProfile IptcProfile { get; set; }
+
///
/// Gets the metadata value associated with the specified key.
///
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
index 11d0bd01b..29c21d611 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
@@ -57,8 +57,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// by making a copy from another EXIF profile.
///
/// The other EXIF profile, where the clone should be made from.
+ /// is null.>
private ExifProfile(ExifProfile other)
{
+ Guard.NotNull(other, nameof(other));
+
this.Parts = other.Parts;
this.thumbnailLength = other.thumbnailLength;
this.thumbnailOffset = other.thumbnailOffset;
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf b/src/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf
new file mode 100644
index 000000000..b00355181
Binary files /dev/null and b/src/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf differ
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
new file mode 100644
index 000000000..9206e4377
--- /dev/null
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
@@ -0,0 +1,298 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Text;
+
+namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
+{
+ ///
+ /// Represents an IPTC profile providing access to the collection of values.
+ ///
+ public sealed class IptcProfile : IDeepCloneable
+ {
+ private Collection values;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public IptcProfile()
+ : this((byte[])null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The byte array to read the iptc profile from.
+ public IptcProfile(byte[] data)
+ {
+ this.Data = data;
+ this.Initialize();
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// by making a copy from another IPTC profile.
+ ///
+ /// The other IPTC profile, from which the clone should be made from.
+ private IptcProfile(IptcProfile other)
+ {
+ Guard.NotNull(other, nameof(other));
+
+ if (other.values != null)
+ {
+ this.values = new Collection();
+
+ foreach (IptcValue value in other.Values)
+ {
+ this.values.Add(value.DeepClone());
+ }
+ }
+
+ if (other.Data != null)
+ {
+ this.Data = new byte[other.Data.Length];
+ other.Data.AsSpan().CopyTo(this.Data);
+ }
+ }
+
+ ///
+ /// Gets the byte data of the IPTC profile.
+ ///
+ public byte[] Data { get; private set; }
+
+ ///
+ /// Gets the values of this iptc profile.
+ ///
+ public IEnumerable Values
+ {
+ get
+ {
+ this.Initialize();
+ return this.values;
+ }
+ }
+
+ ///
+ public IptcProfile DeepClone() => new IptcProfile(this);
+
+ ///
+ /// Returns all value with the specified tag.
+ ///
+ /// The tag of the iptc value.
+ /// The values found with the specified tag.
+ public List GetValues(IptcTag tag)
+ {
+ var iptcValues = new List();
+ foreach (IptcValue iptcValue in this.Values)
+ {
+ if (iptcValue.Tag == tag)
+ {
+ iptcValues.Add(iptcValue);
+ }
+ }
+
+ return iptcValues;
+ }
+
+ ///
+ /// Removes all values with the specified tag.
+ ///
+ /// The tag of the iptc value to remove.
+ /// True when the value was found and removed.
+ public bool RemoveValue(IptcTag tag)
+ {
+ this.Initialize();
+
+ bool removed = false;
+ for (int i = this.values.Count - 1; i >= 0; i--)
+ {
+ if (this.values[i].Tag == tag)
+ {
+ this.values.RemoveAt(i);
+ removed = true;
+ }
+ }
+
+ return removed;
+ }
+
+ ///
+ /// Removes values with the specified tag and value.
+ ///
+ /// The tag of the iptc value to remove.
+ /// The value of the iptc item to remove.
+ /// True when the value was found and removed.
+ public bool RemoveValue(IptcTag tag, string value)
+ {
+ this.Initialize();
+
+ bool removed = false;
+ for (int i = this.values.Count - 1; i >= 0; i--)
+ {
+ if (this.values[i].Tag == tag && this.values[i].Value.Equals(value))
+ {
+ this.values.RemoveAt(i);
+ removed = true;
+ }
+ }
+
+ return removed;
+ }
+
+ ///
+ /// Changes the encoding for all the values.
+ ///
+ /// The encoding to use when storing the bytes.
+ public void SetEncoding(Encoding encoding)
+ {
+ Guard.NotNull(encoding, nameof(encoding));
+
+ foreach (IptcValue value in this.Values)
+ {
+ value.Encoding = encoding;
+ }
+ }
+
+ ///
+ /// Sets the value for the specified tag.
+ ///
+ /// The tag of the iptc value.
+ /// The encoding to use when storing the bytes.
+ /// The value.
+ ///
+ /// Indicates if length restrictions from the specification should be followed strictly.
+ /// Defaults to true.
+ ///
+ public void SetValue(IptcTag tag, Encoding encoding, string value, bool strict = true)
+ {
+ Guard.NotNull(encoding, nameof(encoding));
+ Guard.NotNull(value, nameof(value));
+
+ if (!tag.IsRepeatable())
+ {
+ foreach (IptcValue iptcValue in this.Values)
+ {
+ if (iptcValue.Tag == tag)
+ {
+ iptcValue.Strict = strict;
+ iptcValue.Encoding = encoding;
+ iptcValue.Value = value;
+ return;
+ }
+ }
+ }
+
+ this.values.Add(new IptcValue(tag, encoding, value, strict));
+ }
+
+ ///
+ /// Makes sure the datetime is formatted according to the iptc specification.
+ ///
+ /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
+ /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
+ /// two hours ahead of UTC.
+ ///
+ ///
+ /// The tag of the iptc value.
+ /// The datetime.
+ public void SetDateTimeValue(IptcTag tag, DateTimeOffset dateTimeOffset)
+ {
+ if (!tag.IsDate() && !tag.IsTime())
+ {
+ throw new ArgumentException("iptc tag is not a time or date type");
+ }
+
+ var formattedDate = tag.IsDate()
+ ? dateTimeOffset.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture)
+ : dateTimeOffset.ToString("HHmmsszzzz", System.Globalization.CultureInfo.InvariantCulture)
+ .Replace(":", string.Empty);
+
+ this.SetValue(tag, Encoding.UTF8, formattedDate);
+ }
+
+ ///
+ /// Sets the value of the specified tag.
+ ///
+ /// The tag of the iptc value.
+ /// The value.
+ ///
+ /// Indicates if length restrictions from the specification should be followed strictly.
+ /// Defaults to true.
+ ///
+ public void SetValue(IptcTag tag, string value, bool strict = true) => this.SetValue(tag, Encoding.UTF8, value, strict);
+
+ ///
+ /// Updates the data of the profile.
+ ///
+ public void UpdateData()
+ {
+ var length = 0;
+ foreach (IptcValue value in this.Values)
+ {
+ length += value.Length + 5;
+ }
+
+ this.Data = new byte[length];
+
+ int i = 0;
+ foreach (IptcValue value in this.Values)
+ {
+ this.Data[i++] = 28;
+ this.Data[i++] = 2;
+ this.Data[i++] = (byte)value.Tag;
+ this.Data[i++] = (byte)(value.Length >> 8);
+ this.Data[i++] = (byte)value.Length;
+ if (value.Length > 0)
+ {
+ Buffer.BlockCopy(value.ToByteArray(), 0, this.Data, i, value.Length);
+ i += value.Length;
+ }
+ }
+ }
+
+ private void Initialize()
+ {
+ if (this.values != null)
+ {
+ return;
+ }
+
+ this.values = new Collection();
+
+ if (this.Data == null || this.Data[0] != 0x1c)
+ {
+ return;
+ }
+
+ int i = 0;
+ while (i + 4 < this.Data.Length)
+ {
+ if (this.Data[i++] != 28)
+ {
+ continue;
+ }
+
+ i++;
+
+ var tag = (IptcTag)this.Data[i++];
+
+ int count = BinaryPrimitives.ReadInt16BigEndian(this.Data.AsSpan(i, 2));
+ i += 2;
+
+ var iptcData = new byte[count];
+ if ((count > 0) && (i + count <= this.Data.Length))
+ {
+ Buffer.BlockCopy(this.Data, i, iptcData, 0, count);
+ this.values.Add(new IptcValue(tag, iptcData, false));
+ }
+
+ i += count;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs
new file mode 100644
index 000000000..7258a0291
--- /dev/null
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs
@@ -0,0 +1,397 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
+{
+ ///
+ /// Provides enumeration of all IPTC tags relevant for images.
+ ///
+ public enum IptcTag
+ {
+ ///
+ /// Unknown.
+ ///
+ Unknown = -1,
+
+ ///
+ /// Record version identifying the version of the Information Interchange Model.
+ /// Not repeatable. Max length is 2.
+ ///
+ RecordVersion = 0,
+
+ ///
+ /// Object type, not repeatable. Max Length is 67.
+ ///
+ ObjectType = 3,
+
+ ///
+ /// Object attribute. Max length is 68.
+ ///
+ ObjectAttribute = 4,
+
+ ///
+ /// Object Name, not repeatable. Max length is 64.
+ ///
+ Name = 5,
+
+ ///
+ /// Edit status, not repeatable. Max length is 64.
+ ///
+ EditStatus = 7,
+
+ ///
+ /// Editorial update, not repeatable. Max length is 2.
+ ///
+ EditorialUpdate = 8,
+
+ ///
+ /// Urgency, not repeatable. Max length is 2.
+ ///
+ Urgency = 10,
+
+ ///
+ /// Subject Reference. Max length is 236.
+ ///
+ SubjectReference = 12,
+
+ ///
+ /// Category, not repeatable. Max length is 3.
+ ///
+ Category = 15,
+
+ ///
+ /// Supplemental categories. Max length is 32.
+ ///
+ SupplementalCategories = 20,
+
+ ///
+ /// Fixture identifier, not repeatable. Max length is 32.
+ ///
+ FixtureIdentifier = 22,
+
+ ///
+ /// Keywords. Max length is 64.
+ ///
+ Keywords = 25,
+
+ ///
+ /// Location code. Max length is 3.
+ ///
+ LocationCode = 26,
+
+ ///
+ /// Location name. Max length is 64.
+ ///
+ LocationName = 27,
+
+ ///
+ /// Release date. Format should be CCYYMMDD.
+ /// Not repeatable, max length is 8.
+ ///
+ /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
+ ///
+ ///
+ ReleaseDate = 30,
+
+ ///
+ /// Release time. Format should be HHMMSS±HHMM.
+ /// Not repeatable, max length is 11.
+ ///
+ /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
+ /// two hours ahead of UTC.
+ ///
+ ///
+ ReleaseTime = 35,
+
+ ///
+ /// Expiration date. Format should be CCYYMMDD.
+ /// Not repeatable, max length is 8.
+ ///
+ /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
+ ///
+ ///
+ ExpirationDate = 37,
+
+ ///
+ /// Expiration time. Format should be HHMMSS±HHMM.
+ /// Not repeatable, max length is 11.
+ ///
+ /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
+ /// two hours ahead of UTC.
+ ///
+ ///
+ ExpirationTime = 38,
+
+ ///
+ /// Special instructions, not repeatable. Max length is 256.
+ ///
+ SpecialInstructions = 40,
+
+ ///
+ /// Action advised, not repeatable. Max length is 2.
+ ///
+ ActionAdvised = 42,
+
+ ///
+ /// Reference service. Max length is 10.
+ ///
+ ReferenceService = 45,
+
+ ///
+ /// Reference date. Format should be CCYYMMDD.
+ /// Not repeatable, max length is 8.
+ ///
+ /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
+ ///
+ ///
+ ReferenceDate = 47,
+
+ ///
+ /// ReferenceNumber. Max length is 8.
+ ///
+ ReferenceNumber = 50,
+
+ ///
+ /// Created date. Format should be CCYYMMDD.
+ /// Not repeatable, max length is 8.
+ ///
+ /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
+ ///
+ ///
+ CreatedDate = 55,
+
+ ///
+ /// Created time. Format should be HHMMSS±HHMM.
+ /// Not repeatable, max length is 11.
+ ///
+ /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
+ /// two hours ahead of UTC.
+ ///
+ ///
+ CreatedTime = 60,
+
+ ///
+ /// Digital creation date. Format should be CCYYMMDD.
+ /// Not repeatable, max length is 8.
+ ///
+ /// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
+ ///
+ ///
+ DigitalCreationDate = 62,
+
+ ///
+ /// Digital creation time. Format should be HHMMSS±HHMM.
+ /// Not repeatable, max length is 11.
+ ///
+ /// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
+ /// two hours ahead of UTC.
+ ///
+ ///
+ DigitalCreationTime = 63,
+
+ ///
+ /// Originating program, not repeatable. Max length is 32.
+ ///
+ OriginatingProgram = 65,
+
+ ///
+ /// Program version, not repeatable. Max length is 10.
+ ///
+ ProgramVersion = 70,
+
+ ///
+ /// Object cycle, not repeatable. Max length is 1.
+ ///
+ ObjectCycle = 75,
+
+ ///
+ /// Byline. Max length is 32.
+ ///
+ Byline = 80,
+
+ ///
+ /// Byline title. Max length is 32.
+ ///
+ BylineTitle = 85,
+
+ ///
+ /// City, not repeatable. Max length is 32.
+ ///
+ City = 90,
+
+ ///
+ /// Sub location, not repeatable. Max length is 32.
+ ///
+ SubLocation = 92,
+
+ ///
+ /// Province/State, not repeatable. Max length is 32.
+ ///
+ ProvinceState = 95,
+
+ ///
+ /// Country code, not repeatable. Max length is 3.
+ ///
+ CountryCode = 100,
+
+ ///
+ /// Country, not repeatable. Max length is 64.
+ ///
+ Country = 101,
+
+ ///
+ /// Original transmission reference, not repeatable. Max length is 32.
+ ///
+ OriginalTransmissionReference = 103,
+
+ ///
+ /// Headline, not repeatable. Max length is 256.
+ ///
+ Headline = 105,
+
+ ///
+ /// Credit, not repeatable. Max length is 32.
+ ///
+ Credit = 110,
+
+ ///
+ /// Source, not repeatable. Max length is 32.
+ ///
+ Source = 115,
+
+ ///
+ /// Copyright notice, not repeatable. Max length is 128.
+ ///
+ CopyrightNotice = 116,
+
+ ///
+ /// Contact. Max length 128.
+ ///
+ Contact = 118,
+
+ ///
+ /// Caption, not repeatable. Max length is 2000.
+ ///
+ Caption = 120,
+
+ ///
+ /// Local caption.
+ ///
+ LocalCaption = 121,
+
+ ///
+ /// Caption writer. Max length is 32.
+ ///
+ CaptionWriter = 122,
+
+ ///
+ /// Image type, not repeatable. Max length is 2.
+ ///
+ ImageType = 130,
+
+ ///
+ /// Image orientation, not repeatable. Max length is 1.
+ ///
+ ImageOrientation = 131,
+
+ ///
+ /// Custom field 1
+ ///
+ CustomField1 = 200,
+
+ ///
+ /// Custom field 2
+ ///
+ CustomField2 = 201,
+
+ ///
+ /// Custom field 3
+ ///
+ CustomField3 = 202,
+
+ ///
+ /// Custom field 4
+ ///
+ CustomField4 = 203,
+
+ ///
+ /// Custom field 5
+ ///
+ CustomField5 = 204,
+
+ ///
+ /// Custom field 6
+ ///
+ CustomField6 = 205,
+
+ ///
+ /// Custom field 7
+ ///
+ CustomField7 = 206,
+
+ ///
+ /// Custom field 8
+ ///
+ CustomField8 = 207,
+
+ ///
+ /// Custom field 9
+ ///
+ CustomField9 = 208,
+
+ ///
+ /// Custom field 10
+ ///
+ CustomField10 = 209,
+
+ ///
+ /// Custom field 11
+ ///
+ CustomField11 = 210,
+
+ ///
+ /// Custom field 12
+ ///
+ CustomField12 = 211,
+
+ ///
+ /// Custom field 13
+ ///
+ CustomField13 = 212,
+
+ ///
+ /// Custom field 14
+ ///
+ CustomField14 = 213,
+
+ ///
+ /// Custom field 15
+ ///
+ CustomField15 = 214,
+
+ ///
+ /// Custom field 16
+ ///
+ CustomField16 = 215,
+
+ ///
+ /// Custom field 17
+ ///
+ CustomField17 = 216,
+
+ ///
+ /// Custom field 18
+ ///
+ CustomField18 = 217,
+
+ ///
+ /// Custom field 19
+ ///
+ CustomField19 = 218,
+
+ ///
+ /// Custom field 20
+ ///
+ CustomField20 = 219,
+ }
+}
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs
new file mode 100644
index 000000000..6b39769a7
--- /dev/null
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs
@@ -0,0 +1,162 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
+{
+ ///
+ /// Extension methods for IPTC tags.
+ ///
+ public static class IptcTagExtensions
+ {
+ ///
+ /// Maximum length of the IPTC value with the given tag according to the specification.
+ ///
+ /// The tag to check the max length for.
+ /// The maximum length.
+ public static int MaxLength(this IptcTag tag)
+ {
+ return tag switch
+ {
+ IptcTag.RecordVersion => 2,
+ IptcTag.ObjectType => 67,
+ IptcTag.ObjectAttribute => 68,
+ IptcTag.Name => 64,
+ IptcTag.EditStatus => 64,
+ IptcTag.EditorialUpdate => 2,
+ IptcTag.Urgency => 1,
+ IptcTag.SubjectReference => 236,
+ IptcTag.Category => 3,
+ IptcTag.SupplementalCategories => 32,
+ IptcTag.FixtureIdentifier => 32,
+ IptcTag.Keywords => 64,
+ IptcTag.LocationCode => 3,
+ IptcTag.LocationName => 64,
+ IptcTag.ReleaseDate => 8,
+ IptcTag.ReleaseTime => 11,
+ IptcTag.ExpirationDate => 8,
+ IptcTag.ExpirationTime => 11,
+ IptcTag.SpecialInstructions => 256,
+ IptcTag.ActionAdvised => 2,
+ IptcTag.ReferenceService => 10,
+ IptcTag.ReferenceDate => 8,
+ IptcTag.ReferenceNumber => 8,
+ IptcTag.CreatedDate => 8,
+ IptcTag.CreatedTime => 11,
+ IptcTag.DigitalCreationDate => 8,
+ IptcTag.DigitalCreationTime => 11,
+ IptcTag.OriginatingProgram => 32,
+ IptcTag.ProgramVersion => 10,
+ IptcTag.ObjectCycle => 1,
+ IptcTag.Byline => 32,
+ IptcTag.BylineTitle => 32,
+ IptcTag.City => 32,
+ IptcTag.SubLocation => 32,
+ IptcTag.ProvinceState => 32,
+ IptcTag.CountryCode => 3,
+ IptcTag.Country => 64,
+ IptcTag.OriginalTransmissionReference => 32,
+ IptcTag.Headline => 256,
+ IptcTag.Credit => 32,
+ IptcTag.Source => 32,
+ IptcTag.CopyrightNotice => 128,
+ IptcTag.Contact => 128,
+ IptcTag.Caption => 2000,
+ IptcTag.CaptionWriter => 32,
+ IptcTag.ImageType => 2,
+ IptcTag.ImageOrientation => 1,
+ _ => 256
+ };
+ }
+
+ ///
+ /// Determines if the given tag can be repeated according to the specification.
+ ///
+ /// The tag to check.
+ /// True, if the tag can occur multiple times.
+ public static bool IsRepeatable(this IptcTag tag)
+ {
+ switch (tag)
+ {
+ case IptcTag.RecordVersion:
+ case IptcTag.ObjectType:
+ case IptcTag.Name:
+ case IptcTag.EditStatus:
+ case IptcTag.EditorialUpdate:
+ case IptcTag.Urgency:
+ case IptcTag.Category:
+ case IptcTag.FixtureIdentifier:
+ case IptcTag.ReleaseDate:
+ case IptcTag.ReleaseTime:
+ case IptcTag.ExpirationDate:
+ case IptcTag.ExpirationTime:
+ case IptcTag.SpecialInstructions:
+ case IptcTag.ActionAdvised:
+ case IptcTag.CreatedDate:
+ case IptcTag.CreatedTime:
+ case IptcTag.DigitalCreationDate:
+ case IptcTag.DigitalCreationTime:
+ case IptcTag.OriginatingProgram:
+ case IptcTag.ProgramVersion:
+ case IptcTag.ObjectCycle:
+ case IptcTag.City:
+ case IptcTag.SubLocation:
+ case IptcTag.ProvinceState:
+ case IptcTag.CountryCode:
+ case IptcTag.Country:
+ case IptcTag.OriginalTransmissionReference:
+ case IptcTag.Headline:
+ case IptcTag.Credit:
+ case IptcTag.Source:
+ case IptcTag.CopyrightNotice:
+ case IptcTag.Caption:
+ case IptcTag.ImageType:
+ case IptcTag.ImageOrientation:
+ return false;
+
+ default:
+ return true;
+ }
+ }
+
+ ///
+ /// Determines if the tag is a datetime tag which needs to be formatted as CCYYMMDD.
+ ///
+ /// The tag to check.
+ /// True, if its a datetime tag.
+ public static bool IsDate(this IptcTag tag)
+ {
+ switch (tag)
+ {
+ case IptcTag.CreatedDate:
+ case IptcTag.DigitalCreationDate:
+ case IptcTag.ExpirationDate:
+ case IptcTag.ReferenceDate:
+ case IptcTag.ReleaseDate:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Determines if the tag is a time tag which need to be formatted as HHMMSS±HHMM.
+ ///
+ /// The tag to check.
+ /// True, if its a time tag.
+ public static bool IsTime(this IptcTag tag)
+ {
+ switch (tag)
+ {
+ case IptcTag.CreatedTime:
+ case IptcTag.DigitalCreationTime:
+ case IptcTag.ExpirationTime:
+ case IptcTag.ReleaseTime:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
new file mode 100644
index 000000000..e63781012
--- /dev/null
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
@@ -0,0 +1,219 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Text;
+
+namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
+{
+ ///
+ /// Represents a single value of the IPTC profile.
+ ///
+ public sealed class IptcValue : IDeepCloneable
+ {
+ private byte[] data = Array.Empty();
+ private Encoding encoding;
+
+ internal IptcValue(IptcValue other)
+ {
+ if (other.data != null)
+ {
+ this.data = new byte[other.data.Length];
+ other.data.AsSpan().CopyTo(this.data);
+ }
+
+ if (other.Encoding != null)
+ {
+ this.Encoding = (Encoding)other.Encoding.Clone();
+ }
+
+ this.Tag = other.Tag;
+ this.Strict = other.Strict;
+ }
+
+ internal IptcValue(IptcTag tag, byte[] value, bool strict)
+ {
+ Guard.NotNull(value, nameof(value));
+
+ this.Strict = strict;
+ this.Tag = tag;
+ this.data = value;
+ this.encoding = Encoding.UTF8;
+ }
+
+ internal IptcValue(IptcTag tag, Encoding encoding, string value, bool strict)
+ {
+ this.Strict = strict;
+ this.Tag = tag;
+ this.encoding = encoding;
+ this.Value = value;
+ }
+
+ internal IptcValue(IptcTag tag, string value, bool strict)
+ {
+ this.Strict = strict;
+ this.Tag = tag;
+ this.encoding = Encoding.UTF8;
+ this.Value = value;
+ }
+
+ ///
+ /// Gets or sets the encoding to use for the Value.
+ ///
+ public Encoding Encoding
+ {
+ get => this.encoding;
+ set
+ {
+ if (value != null)
+ {
+ this.encoding = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets the tag of the iptc value.
+ ///
+ public IptcTag Tag { get; }
+
+ ///
+ /// Gets or sets a value indicating whether to be enforce value length restrictions according
+ /// to the specification.
+ ///
+ public bool Strict { get; set; }
+
+ ///
+ /// Gets or sets the value.
+ ///
+ public string Value
+ {
+ get => this.encoding.GetString(this.data);
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ this.data = Array.Empty();
+ }
+ else
+ {
+ int maxLength = this.Tag.MaxLength();
+ byte[] valueBytes;
+ if (this.Strict && value.Length > maxLength)
+ {
+ var cappedValue = value.Substring(0, maxLength);
+ valueBytes = this.encoding.GetBytes(cappedValue);
+
+ // It is still possible that the bytes of the string exceed the limit.
+ if (valueBytes.Length > maxLength)
+ {
+ throw new ArgumentException($"The iptc value exceeds the limit of {maxLength} bytes for the tag {this.Tag}");
+ }
+ }
+ else
+ {
+ valueBytes = this.encoding.GetBytes(value);
+ }
+
+ this.data = valueBytes;
+ }
+ }
+ }
+
+ ///
+ /// Gets the length of the value.
+ ///
+ public int Length => this.data.Length;
+
+ ///
+ public IptcValue DeepClone() => new IptcValue(this);
+
+ ///
+ /// Determines whether the specified object is equal to the current .
+ ///
+ /// The object to compare this with.
+ /// True when the specified object is equal to the current .
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(this, obj))
+ {
+ return true;
+ }
+
+ return this.Equals(obj as IptcValue);
+ }
+
+ ///
+ /// Determines whether the specified iptc value is equal to the current .
+ ///
+ /// The iptc value to compare this with.
+ /// True when the specified iptc value is equal to the current .
+ public bool Equals(IptcValue other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ if (this.Tag != other.Tag)
+ {
+ return false;
+ }
+
+ if (this.data.Length != other.data.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < this.data.Length; i++)
+ {
+ if (this.data[i] != other.data[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Serves as a hash of this type.
+ ///
+ /// A hash code for the current instance.
+ public override int GetHashCode() => HashCode.Combine(this.data, this.Tag);
+
+ ///
+ /// Converts this instance to a byte array.
+ ///
+ /// A array.
+ public byte[] ToByteArray()
+ {
+ var result = new byte[this.data.Length];
+ this.data.CopyTo(result, 0);
+ return result;
+ }
+
+ ///
+ /// Returns a string that represents the current value.
+ ///
+ /// A string that represents the current value.
+ public override string ToString() => this.Value;
+
+ ///
+ /// Returns a string that represents the current value with the specified encoding.
+ ///
+ /// The encoding to use.
+ /// A string that represents the current value with the specified encoding.
+ public string ToString(Encoding encoding)
+ {
+ Guard.NotNull(encoding, nameof(encoding));
+
+ return encoding.GetString(this.data);
+ }
+ }
+}
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/README.md b/src/ImageSharp/Metadata/Profiles/IPTC/README.md
new file mode 100644
index 000000000..1217ca0c7
--- /dev/null
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/README.md
@@ -0,0 +1,11 @@
+IPTC source code is from [Magick.NET](https://github.com/dlemstra/Magick.NET)
+
+Information about IPTC can be found here in the following sources:
+
+- [metacpan.org, APP13-segment](https://metacpan.org/pod/Image::MetaData::JPEG::Structures#Structure-of-a-Photoshop-style-APP13-segment)
+
+- [iptc.org](https://www.iptc.org/std/photometadata/documentation/userguide/)
+
+- [Adobe File Formats Specification](http://oldschoolprg.x10.mx/downloads/ps6ffspecsv2.pdf)
+
+- [Tag Overview](https://exiftool.org/TagNames/IPTC.html)
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
new file mode 100644
index 000000000..d9f44cef9
--- /dev/null
+++ b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
@@ -0,0 +1,359 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
+using SixLabors.ImageSharp.PixelFormats;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC
+{
+ public class IptcProfileTests
+ {
+ private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false };
+
+ public static IEnumerable