diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
index 306924e62c..b21e4db7c8 100644
--- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
@@ -227,6 +227,7 @@ public void SetDateTimeValue(IptcTag tag, DateTimeOffset dateTimeOffset)
///
/// Updates the data of the profile.
+ /// Adds the UTF-8 character set declaration when any value uses UTF-8.
///
public void UpdateData()
{
@@ -330,14 +331,15 @@ private void Initialize()
}
///
- /// Gets if any value has UTF-8 encoding.
+ /// Determines whether any value uses UTF-8.
///
- /// true if any value has UTF-8 encoding.
+ /// True if any value uses UTF-8.
private bool HasValuesInUtf8()
{
foreach (IptcValue value in this.values)
{
- if (value.Encoding == Encoding.UTF8)
+ // Identify UTF-8 by code page. Cloning an encoding changes its identity, not its code page.
+ if (value.Encoding.CodePage == Encoding.UTF8.CodePage)
{
return true;
}
diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
index 37f99517c7..9009308763 100644
--- a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
+++ b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Text;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
@@ -34,6 +35,88 @@ public void IptcProfile_WithUtf8Data_WritesEnvelopeRecord_Works()
Assert.True(profileBytes.AsSpan(0, 8).SequenceEqual(expectedEnvelopeData));
}
+ ///
+ /// Preserves the UTF-8 declaration when cloning creates a separate encoding instance.
+ ///
+ [Fact]
+ public void IptcProfile_DeepClone_WithUtf8Data_WritesEnvelopeRecord_Works()
+ {
+ // arrange
+ IptcProfile profile = new();
+ profile.SetValue(IptcTag.City, "ESPAÑA");
+ IptcProfile clone = profile.DeepClone();
+ byte[] expectedEnvelopeData = [28, 1, 90, 0, 3, 27, 37, 71];
+
+ // act
+ clone.UpdateData();
+
+ // assert
+ Assert.True(clone.Data.AsSpan(0, 8).SequenceEqual(expectedEnvelopeData));
+ }
+
+ ///
+ /// Declares UTF-8 for a caller-supplied encoding instance.
+ ///
+ [Fact]
+ public void IptcProfile_WithUtf8EncodingInstance_WritesEnvelopeRecord_Works()
+ {
+ // arrange
+ IptcProfile profile = new();
+ profile.SetValue(IptcTag.City, new UTF8Encoding(false), "ESPAÑA");
+ byte[] expectedEnvelopeData = [28, 1, 90, 0, 3, 27, 37, 71];
+
+ // act
+ profile.UpdateData();
+
+ // assert
+ Assert.True(profile.Data.AsSpan(0, 8).SequenceEqual(expectedEnvelopeData));
+ }
+
+ ///
+ /// Keeps a cloned Latin-1 value in its original encoding without a UTF-8 declaration.
+ ///
+ [Fact]
+ public void IptcProfile_DeepClone_WithLatin1Data_DoesNotWriteUtf8EnvelopeRecord()
+ {
+ IptcProfile profile = new();
+ profile.SetValue(IptcTag.City, Encoding.Latin1, "ESPAÑA");
+ IptcProfile clone = profile.DeepClone();
+
+ clone.UpdateData();
+
+ // Only the 2:90 City dataset is present. Latin-1 stores Ñ as D1, without a UTF-8 envelope.
+ byte[] expectedData = [28, 2, 90, 0, 6, 69, 83, 80, 65, 209, 65];
+ Assert.Equal(expectedData, clone.Data);
+ }
+
+ ///
+ /// Writes one UTF-8 declaration after repeated JPEG saves and image clones.
+ ///
+ [Fact]
+ public void WritingClonedImage_WithUtf8Data_WritesEnvelopeRecordOnce()
+ {
+ // arrange
+ using Image image = Image.Load(TestFile.Create(TestImages.Png.TestPattern31x31).Bytes);
+ image.Metadata.IptcProfile = new IptcProfile();
+ image.Metadata.IptcProfile.SetValue(IptcTag.Caption, "ESPAÑA");
+
+ // 1:90 declares UTF-8. The 2:120 Caption dataset contains the seven UTF-8 bytes for ESPAÑA.
+ byte[] expectedData = [28, 1, 90, 0, 3, 27, 37, 71, 28, 2, 120, 0, 7, 69, 83, 80, 65, 195, 145, 65];
+
+ // act
+ using Image firstClone = image.Clone();
+ using Image once = WriteAndReadJpeg(firstClone);
+
+ using Image secondClone = once.Clone();
+ using Image twice = WriteAndReadJpeg(secondClone);
+
+ // assert
+ IptcProfile actual = twice.Metadata.IptcProfile;
+ Assert.Equal(expectedData, actual.Data);
+ IptcValue value = Assert.Single(actual.Values);
+ Assert.Equal("ESPAÑA", value.Value);
+ }
+
[Theory]
[MemberData(nameof(AllIptcTags))]
public void IptcProfile_SetValue_WithStrictEnabled_Works(IptcTag tag)
@@ -227,7 +310,7 @@ public void IptcValue_CloneIsDeep()
public void WritingImage_PreservesIptcProfile()
{
// arrange
- using Image image = new(1, 1);
+ using Image image = Image.Load(TestFile.Create(TestImages.Png.TestPattern31x31).Bytes);
image.Metadata.IptcProfile = new IptcProfile();
const string expectedCaptionWriter = "unittest";
const string expectedCaption = "test";
@@ -383,12 +466,16 @@ private static void ContainsIptcValue(List values, IptcTag tag, strin
Assert.True(values.Contains(new IptcValue(tag, System.Text.Encoding.UTF8.GetBytes(value), false)), $"expected iptc value '{value}' was not found for tag '{tag}'");
}
+ ///
+ /// Saves a JPEG and decodes it without disposing the caller's source image.
+ ///
+ /// The source image. The caller retains ownership.
+ /// The decoded image. The caller must dispose it.
private static Image WriteAndReadJpeg(Image image)
{
using (MemoryStream memStream = new())
{
image.SaveAsJpeg(memStream);
- image.Dispose();
memStream.Position = 0;
return Image.Load(memStream);