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.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 @@ public sealed class IptcProfile : IDeepCloneable 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 @@ private IptcProfile(IptcProfile other) } } + /// + /// 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 @@ public void SetValue(IptcTag tag, Encoding encoding, string value, bool strict = 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 @@ public void SetDateTimeValue(IptcTag tag, DateTimeOffset dateTimeOffset) 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 @@ public void UpdateData() 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 @@ public void UpdateData() // | | 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 @@ private void Initialize() 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 @@ private void Initialize() 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 @@ private void Initialize() 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 @@ public void Encode_PreservesIptcProfile() { // 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 @@ public void Encode_PreservesIptcProfile() 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 c9972aa25d..70b08b9ec8 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs @@ -15,9 +15,9 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC { public class IptcProfileTests { - private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false }; + private static JpegDecoder JpegDecoder => new() { IgnoreMetadata = false }; - private static TiffDecoder TiffDecoder => new TiffDecoder() { IgnoreMetadata = false }; + private static TiffDecoder TiffDecoder => new() { IgnoreMetadata = false }; public static IEnumerable AllIptcTags() { @@ -27,6 +27,22 @@ public static IEnumerable AllIptcTags() } } + [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)