diff --git a/Directory.Build.props b/Directory.Build.props
index 12a4a5c2a..50c09fbb3 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -31,21 +31,21 @@
- $(DefineConstants);SUPPORTS_MATHF;SUPPORTS_HASHCODE;SUPPORTS_EXTENDED_INTRINSICS;SUPPORTS_SPAN_STREAM;SUPPORTS_ENCODING_STRING;SUPPORTS_RUNTIME_INTRINSICS;SUPPORTS_CODECOVERAGE
+ $(DefineConstants);SUPPORTS_MATHF;SUPPORTS_HASHCODE;SUPPORTS_EXTENDED_INTRINSICS;SUPPORTS_SPAN_STREAM;SUPPORTS_ENCODING_STRING;SUPPORTS_RUNTIME_INTRINSICS;SUPPORTS_CODECOVERAGE;SUPPORTS_HOTPATH
$(DefineConstants);SUPPORTS_MATHF;SUPPORTS_HASHCODE;SUPPORTS_EXTENDED_INTRINSICS;SUPPORTS_SPAN_STREAM;SUPPORTS_ENCODING_STRING;SUPPORTS_CODECOVERAGE
diff --git a/src/ImageSharp/Common/Helpers/InliningOptions.cs b/src/ImageSharp/Common/Helpers/InliningOptions.cs
index 069a426d7..895b6250f 100644
--- a/src/ImageSharp/Common/Helpers/InliningOptions.cs
+++ b/src/ImageSharp/Common/Helpers/InliningOptions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
// Uncomment this for verbose profiler results. DO NOT PUSH TO MAIN!
@@ -13,10 +13,16 @@ namespace SixLabors.ImageSharp
internal static class InliningOptions
{
#if PROFILING
+ public const MethodImplOptions HotPath = MethodImplOptions.NoInlining;
public const MethodImplOptions ShortMethod = MethodImplOptions.NoInlining;
#else
+#if SUPPORTS_HOTPATH
+ public const MethodImplOptions HotPath = MethodImplOptions.AggressiveOptimization;
+#else
+ public const MethodImplOptions HotPath = MethodImplOptions.AggressiveInlining;
+#endif
public const MethodImplOptions ShortMethod = MethodImplOptions.AggressiveInlining;
#endif
public const MethodImplOptions ColdPath = MethodImplOptions.NoInlining;
}
-}
\ No newline at end of file
+}
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/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 4d7de4161..bc7b9d815 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -380,7 +380,12 @@ namespace SixLabors.ImageSharp.Formats.Png
private void InitializeImage(ImageMetadata metadata, out Image image)
where TPixel : unmanaged, IPixel
{
- image = new Image(this.configuration, this.header.Width, this.header.Height, metadata);
+ image = Image.CreateUninitialized(
+ this.configuration,
+ this.header.Width,
+ this.header.Height,
+ metadata);
+
this.bytesPerPixel = this.CalculateBytesPerPixel();
this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1;
this.bytesPerSample = 1;
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 45e1ffd2d..fcbbc6697 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -5,10 +5,8 @@ using System;
using System.Buffers;
using System.Buffers.Binary;
using System.IO;
-using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
@@ -16,7 +14,6 @@ using SixLabors.ImageSharp.Formats.Png.Zlib;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png
{
@@ -633,10 +630,21 @@ namespace SixLabors.ImageSharp.Formats.Png
private void WriteTextChunks(Stream stream, PngMetadata meta)
{
const int MaxLatinCode = 255;
- foreach (PngTextData textData in meta.TextData)
+ for (int i = 0; i < meta.TextData.Count; i++)
{
- bool hasUnicodeCharacters = textData.Value.Any(c => c > MaxLatinCode);
- if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword)))
+ PngTextData textData = meta.TextData[i];
+ bool hasUnicodeCharacters = false;
+ foreach (var c in textData.Value)
+ {
+ if (c > MaxLatinCode)
+ {
+ hasUnicodeCharacters = true;
+ break;
+ }
+ }
+
+ if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) ||
+ !string.IsNullOrWhiteSpace(textData.TranslatedKeyword)))
{
// Write iTXt chunk.
byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword);
@@ -647,7 +655,8 @@ namespace SixLabors.ImageSharp.Formats.Png
byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword);
byte[] languageTag = PngConstants.LanguageEncoding.GetBytes(textData.LanguageTag);
- Span outputBytes = new byte[keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5];
+ Span outputBytes = new byte[keywordBytes.Length + textBytes.Length +
+ translatedKeyword.Length + languageTag.Length + 5];
keywordBytes.CopyTo(outputBytes);
if (textData.Value.Length > this.options.TextCompressionThreshold)
{
@@ -667,7 +676,8 @@ namespace SixLabors.ImageSharp.Formats.Png
if (textData.Value.Length > this.options.TextCompressionThreshold)
{
// Write zTXt chunk.
- byte[] compressedData = this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value));
+ byte[] compressedData =
+ this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value));
Span outputBytes = new byte[textData.Keyword.Length + compressedData.Length + 2];
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
compressedData.CopyTo(outputBytes.Slice(textData.Keyword.Length + 2));
@@ -678,7 +688,8 @@ namespace SixLabors.ImageSharp.Formats.Png
// Write tEXt chunk.
Span outputBytes = new byte[textData.Keyword.Length + textData.Value.Length + 1];
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
- PngConstants.Encoding.GetBytes(textData.Value).CopyTo(outputBytes.Slice(textData.Keyword.Length + 1));
+ PngConstants.Encoding.GetBytes(textData.Value)
+ .CopyTo(outputBytes.Slice(textData.Keyword.Length + 1));
this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray());
}
}
diff --git a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs
index 7398b089b..2083edab1 100644
--- a/src/ImageSharp/Formats/Png/Zlib/Deflater.cs
+++ b/src/ImageSharp/Formats/Png/Zlib/Deflater.cs
@@ -288,8 +288,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.engine = null;
this.isDisposed = true;
}
-
- GC.SuppressFinalize(this);
}
}
}
diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs
index 1a8bb4ab0..c1c86a98b 100644
--- a/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs
+++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs
@@ -362,7 +362,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
more = this.inputEnd - this.inputOff;
}
- Array.Copy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more);
+ Buffer.BlockCopy(this.inputBuf, this.inputOff, this.window, this.strstart + this.lookahead, more);
this.inputOff += more;
this.lookahead += more;
@@ -397,8 +397,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.isDisposed = true;
}
-
- GC.SuppressFinalize(this);
}
[MethodImpl(InliningOptions.ShortMethod)]
@@ -464,6 +462,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
///
/// The current match.
/// True if a match greater than the minimum length is found
+ [MethodImpl(InliningOptions.HotPath)]
private bool FindLongestMatch(int curMatch)
{
int match;
diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs
index 543a1fe30..022d3d4d0 100644
--- a/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs
+++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs
@@ -36,11 +36,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
private const int EofSymbol = 256;
- private static readonly short[] StaticLCodes;
- private static readonly byte[] StaticLLength;
- private static readonly short[] StaticDCodes;
- private static readonly byte[] StaticDLength;
-
private Tree literalTree;
private Tree distTree;
private Tree blTree;
@@ -58,49 +53,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
private int extraBits;
private bool isDisposed;
- // TODO: These should be pre-generated array/readonlyspans.
- static DeflaterHuffman()
- {
- // See RFC 1951 3.2.6
- // Literal codes
- StaticLCodes = new short[LiteralNumber];
- StaticLLength = new byte[LiteralNumber];
-
- int i = 0;
- while (i < 144)
- {
- StaticLCodes[i] = BitReverse((0x030 + i) << 8);
- StaticLLength[i++] = 8;
- }
-
- while (i < 256)
- {
- StaticLCodes[i] = BitReverse((0x190 - 144 + i) << 7);
- StaticLLength[i++] = 9;
- }
-
- while (i < 280)
- {
- StaticLCodes[i] = BitReverse((0x000 - 256 + i) << 9);
- StaticLLength[i++] = 7;
- }
-
- while (i < LiteralNumber)
- {
- StaticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8);
- StaticLLength[i++] = 8;
- }
-
- // Distance codes
- StaticDCodes = new short[DistanceNumber];
- StaticDLength = new byte[DistanceNumber];
- for (i = 0; i < DistanceNumber; i++)
- {
- StaticDCodes[i] = BitReverse(i << 11);
- StaticDLength[i] = 5;
- }
- }
-
///
/// Initializes a new instance of the class.
///
@@ -122,12 +74,80 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer;
}
+#pragma warning disable SA1201 // Elements should appear in the correct order
+
+ // See RFC 1951 3.2.6
+ // Literal codes
+ private static readonly short[] StaticLCodes = new short[]
+ {
+ 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252,
+ 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242,
+ 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250,
+ 6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246,
+ 14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254,
+ 1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, 9,
+ 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, 5,
+ 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, 13,
+ 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, 19,
+ 275, 147, 403, 83, 339, 211, 467, 51, 307, 179, 435, 115, 371, 243, 499,
+ 11, 267, 139, 395, 75, 331, 203, 459, 43, 299, 171, 427, 107, 363, 235, 491,
+ 27, 283, 155, 411, 91, 347, 219, 475, 59, 315, 187, 443, 123, 379, 251, 507,
+ 7, 263, 135, 391, 71, 327, 199, 455, 39, 295, 167, 423, 103, 359, 231, 487,
+ 23, 279, 151, 407, 87, 343, 215, 471, 55, 311, 183, 439, 119, 375, 247, 503,
+ 15, 271, 143, 399, 79, 335, 207, 463, 47, 303, 175, 431, 111, 367, 239, 495,
+ 31, 287, 159, 415, 95, 351, 223, 479, 63, 319, 191, 447, 127, 383, 255, 511,
+ 0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36,
+ 100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163
+ };
+
+ private static ReadOnlySpan StaticLLength => new byte[]
+ {
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8
+ };
+
+ // Distance codes and lengths.
+ private static readonly short[] StaticDCodes = new short[]
+ {
+ 0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14,
+ 30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23
+ };
+
+ private static ReadOnlySpan StaticDLength => new byte[]
+ {
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5
+ };
+#pragma warning restore SA1201 // Elements should appear in the correct order
+
///
/// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes.
///
- private static ReadOnlySpan BitLengthOrder => new byte[] { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };
+ private static ReadOnlySpan BitLengthOrder => new byte[]
+ {
+ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
+ };
- private static ReadOnlySpan Bit4Reverse => new byte[] { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 };
+ private static ReadOnlySpan Bit4Reverse => new byte[]
+ {
+ 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
+ };
///
/// Gets the pending buffer to use.
@@ -413,8 +433,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.distTree = null;
this.isDisposed = true;
}
-
- GC.SuppressFinalize(this);
}
[MethodImpl(InliningOptions.ShortMethod)]
@@ -553,6 +571,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
}
}
+ [MethodImpl(InliningOptions.HotPath)]
public void BuildTree()
{
int numSymbols = this.elementCount;
@@ -964,8 +983,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.isDisposed = true;
}
-
- GC.SuppressFinalize(this);
}
}
}
diff --git a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs
index 731c9e80f..0414ca2f8 100644
--- a/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs
+++ b/src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs
@@ -172,8 +172,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
this.bufferMemoryOwner = null;
this.isDisposed = true;
}
-
- GC.SuppressFinalize(this);
}
}
}
diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs
index 0bdbcc4ab..62ac449b7 100644
--- a/src/ImageSharp/ImageExtensions.cs
+++ b/src/ImageSharp/ImageExtensions.cs
@@ -103,14 +103,18 @@ namespace SixLabors.ImageSharp
///
/// Returns a Base64 encoded string from the given image.
+ /// The result is prepended with a Data URI
+ ///
+ ///
+ /// For example:
+ ///
+ ///
+ ///
///
- ///
- /// The pixel format.
/// The source image
/// The format.
/// The
- public static string ToBase64String(this Image source, IImageFormat format)
- where TPixel : unmanaged, IPixel
+ public static string ToBase64String(this Image source, IImageFormat format)
{
using var stream = new MemoryStream();
source.Save(stream, format);
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