diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
index 72c00302dd..ec3eed2650 100644
--- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
@@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
+using SixLabors.ImageSharp.Metadata.Profiles.IPTC;
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
{
@@ -20,6 +21,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
private const uint MaxStandardDataTagSize = 0x7FFF;
+ ///
+ /// 1:90 Coded Character Set.
+ ///
+ private const byte IptcEnvelopeCodedCharacterSet = 0x5A;
+
///
/// Initializes a new instance of the class.
///
@@ -64,6 +70,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
}
}
+ ///
+ /// Gets a byte array marking that UTF-8 encoding is used in application records.
+ ///
+ private static ReadOnlySpan CodedCharacterSetUtf8Value => new byte[] { 0x1B, 0x25, 0x47 }; // Uses C#'s optimization to refer to the data segment in the assembly directly, no allocation occurs.
+
///
/// Gets the byte data of the IPTC profile.
///
@@ -194,6 +205,17 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
this.values.Add(new IptcValue(tag, encoding, value, strict));
}
+ ///
+ /// 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);
+
///
/// Makes sure the datetime is formatted according to the iptc specification.
///
@@ -219,17 +241,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
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.
///
@@ -241,12 +252,25 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
length += value.Length + 5;
}
+ bool hasValuesInUtf8 = this.HasValuesInUtf8();
+
+ if (hasValuesInUtf8)
+ {
+ // Additional length for UTF-8 Tag.
+ length += 5 + CodedCharacterSetUtf8Value.Length;
+ }
+
this.Data = new byte[length];
+ int offset = 0;
+ if (hasValuesInUtf8)
+ {
+ // Write Envelope Record.
+ offset = this.WriteRecord(offset, CodedCharacterSetUtf8Value, IptcRecordNumber.Envelope, IptcEnvelopeCodedCharacterSet);
+ }
- int i = 0;
foreach (IptcValue value in this.Values)
{
- // Standard DataSet Tag
+ // Write Application Record.
// +-----------+----------------+---------------------------------------------------------------------------------+
// | Octet Pos | Name | Description |
// +==========-+================+=================================================================================+
@@ -263,17 +287,26 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
// | | Octet Count | the following data field(32767 or fewer octets). Note that the value of bit 7 of|
// | | | octet 4(most significant bit) always will be 0. |
// +-----------+----------------+---------------------------------------------------------------------------------+
- this.Data[i++] = IptcTagMarkerByte;
- 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;
- }
+ offset = this.WriteRecord(offset, value.ToByteArray(), IptcRecordNumber.Application, (byte)value.Tag);
+ }
+ }
+
+ private int WriteRecord(int offset, ReadOnlySpan recordData, IptcRecordNumber recordNumber, byte recordBinaryRepresentation)
+ {
+ Span data = this.Data.AsSpan(offset, 5);
+ data[0] = IptcTagMarkerByte;
+ data[1] = (byte)recordNumber;
+ data[2] = recordBinaryRepresentation;
+ data[3] = (byte)(recordData.Length >> 8);
+ data[4] = (byte)recordData.Length;
+ offset += 5;
+ if (recordData.Length > 0)
+ {
+ recordData.CopyTo(this.Data.AsSpan(offset));
+ offset += recordData.Length;
}
+
+ return offset;
}
private void Initialize()
@@ -298,6 +331,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
bool isValidRecordNumber = recordNumber is >= 1 and <= 9;
var tag = (IptcTag)this.Data[offset++];
bool isValidEntry = isValidTagMarker && isValidRecordNumber;
+ bool isApplicationRecord = recordNumber == (byte)IptcRecordNumber.Application;
uint byteCount = BinaryPrimitives.ReadUInt16BigEndian(this.Data.AsSpan(offset, 2));
offset += 2;
@@ -307,9 +341,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
break;
}
- if (isValidEntry && byteCount > 0 && (offset <= this.Data.Length - byteCount))
+ if (isValidEntry && isApplicationRecord && byteCount > 0 && (offset <= this.Data.Length - byteCount))
{
- var iptcData = new byte[byteCount];
+ byte[] iptcData = new byte[byteCount];
Buffer.BlockCopy(this.Data, offset, iptcData, 0, (int)byteCount);
this.values.Add(new IptcValue(tag, iptcData, false));
}
@@ -317,5 +351,22 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
offset += (int)byteCount;
}
}
+
+ ///
+ /// Gets if any value has UTF-8 encoding.
+ ///
+ /// true if any value has UTF-8 encoding.
+ private bool HasValuesInUtf8()
+ {
+ foreach (IptcValue value in this.values)
+ {
+ if (value.Encoding == Encoding.UTF8)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
}
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
new file mode 100644
index 0000000000..52e4c47a45
--- /dev/null
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Metadata.Profiles.IPTC
+{
+ ///
+ /// Enum for the different record types of a IPTC value.
+ ///
+ internal enum IptcRecordNumber : byte
+ {
+ ///
+ /// A Envelope Record.
+ ///
+ Envelope = 0x01,
+
+ ///
+ /// A Application Record.
+ ///
+ Application = 0x02
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs
index 60f45664d3..23b2f749d8 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs
@@ -38,8 +38,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
// arrange
using var input = new Image(1, 1);
- input.Metadata.IptcProfile = new IptcProfile();
- input.Metadata.IptcProfile.SetValue(IptcTag.Byline, "unit_test");
+ var expectedProfile = new IptcProfile();
+ expectedProfile.SetValue(IptcTag.Country, "ESPAÑA");
+ expectedProfile.SetValue(IptcTag.City, "unit-test-city");
+ input.Metadata.IptcProfile = expectedProfile;
// act
using var memStream = new MemoryStream();
@@ -50,7 +52,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var output = Image.Load(memStream);
IptcProfile actual = output.Metadata.IptcProfile;
Assert.NotNull(actual);
- IEnumerable values = input.Metadata.IptcProfile.Values;
+ IEnumerable values = expectedProfile.Values;
Assert.Equal(values, actual.Values);
}
diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
index a91d12e568..7d486f0af8 100644
--- a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
+++ b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
@@ -27,6 +27,22 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC
}
}
+ [Fact]
+ public void IptcProfile_WithUtf8Data_WritesEnvelopeRecord_Works()
+ {
+ // arrange
+ var profile = new IptcProfile();
+ profile.SetValue(IptcTag.City, "ESPAÑA");
+ profile.UpdateData();
+ byte[] expectedEnvelopeData = { 28, 1, 90, 0, 3, 27, 37, 71 };
+
+ // act
+ byte[] profileBytes = profile.Data;
+
+ // assert
+ Assert.True(profileBytes.AsSpan(0, 8).SequenceEqual(expectedEnvelopeData));
+ }
+
[Theory]
[MemberData(nameof(AllIptcTags))]
public void IptcProfile_SetValue_WithStrictEnabled_Works(IptcTag tag)