Skip to content

Commit f456ba0

Browse files
authored
Merge pull request #2213 from lmerino-ep/main
Fix IPTC tags written on jpg files that contains non-English characters can't be correctly displayed on external apps #2212
2 parents 13897ae + 41bef5b commit f456ba0

File tree

4 files changed

+120
-30
lines changed

4 files changed

+120
-30
lines changed

src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Collections.ObjectModel;
88
using System.Text;
9+
using SixLabors.ImageSharp.Metadata.Profiles.IPTC;
910

1011
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
1112
{
@@ -20,6 +21,11 @@ public sealed class IptcProfile : IDeepCloneable<IptcProfile>
2021

2122
private const uint MaxStandardDataTagSize = 0x7FFF;
2223

24+
/// <summary>
25+
/// 1:90 Coded Character Set.
26+
/// </summary>
27+
private const byte IptcEnvelopeCodedCharacterSet = 0x5A;
28+
2329
/// <summary>
2430
/// Initializes a new instance of the <see cref="IptcProfile"/> class.
2531
/// </summary>
@@ -64,6 +70,11 @@ private IptcProfile(IptcProfile other)
6470
}
6571
}
6672

73+
/// <summary>
74+
/// Gets a byte array marking that UTF-8 encoding is used in application records.
75+
/// </summary>
76+
private static ReadOnlySpan<byte> CodedCharacterSetUtf8Value => new byte[] { 0x1B, 0x25, 0x47 }; // Uses C#'s optimization to refer to the data segment in the assembly directly, no allocation occurs.
77+
6778
/// <summary>
6879
/// Gets the byte data of the IPTC profile.
6980
/// </summary>
@@ -194,6 +205,17 @@ public void SetValue(IptcTag tag, Encoding encoding, string value, bool strict =
194205
this.values.Add(new IptcValue(tag, encoding, value, strict));
195206
}
196207

208+
/// <summary>
209+
/// Sets the value of the specified tag.
210+
/// </summary>
211+
/// <param name="tag">The tag of the iptc value.</param>
212+
/// <param name="value">The value.</param>
213+
/// <param name="strict">
214+
/// Indicates if length restrictions from the specification should be followed strictly.
215+
/// Defaults to true.
216+
/// </param>
217+
public void SetValue(IptcTag tag, string value, bool strict = true) => this.SetValue(tag, Encoding.UTF8, value, strict);
218+
197219
/// <summary>
198220
/// Makes sure the datetime is formatted according to the iptc specification.
199221
/// <example>
@@ -219,17 +241,6 @@ public void SetDateTimeValue(IptcTag tag, DateTimeOffset dateTimeOffset)
219241
this.SetValue(tag, Encoding.UTF8, formattedDate);
220242
}
221243

222-
/// <summary>
223-
/// Sets the value of the specified tag.
224-
/// </summary>
225-
/// <param name="tag">The tag of the iptc value.</param>
226-
/// <param name="value">The value.</param>
227-
/// <param name="strict">
228-
/// Indicates if length restrictions from the specification should be followed strictly.
229-
/// Defaults to true.
230-
/// </param>
231-
public void SetValue(IptcTag tag, string value, bool strict = true) => this.SetValue(tag, Encoding.UTF8, value, strict);
232-
233244
/// <summary>
234245
/// Updates the data of the profile.
235246
/// </summary>
@@ -241,12 +252,25 @@ public void UpdateData()
241252
length += value.Length + 5;
242253
}
243254

255+
bool hasValuesInUtf8 = this.HasValuesInUtf8();
256+
257+
if (hasValuesInUtf8)
258+
{
259+
// Additional length for UTF-8 Tag.
260+
length += 5 + CodedCharacterSetUtf8Value.Length;
261+
}
262+
244263
this.Data = new byte[length];
264+
int offset = 0;
265+
if (hasValuesInUtf8)
266+
{
267+
// Write Envelope Record.
268+
offset = this.WriteRecord(offset, CodedCharacterSetUtf8Value, IptcRecordNumber.Envelope, IptcEnvelopeCodedCharacterSet);
269+
}
245270

246-
int i = 0;
247271
foreach (IptcValue value in this.Values)
248272
{
249-
// Standard DataSet Tag
273+
// Write Application Record.
250274
// +-----------+----------------+---------------------------------------------------------------------------------+
251275
// | Octet Pos | Name | Description |
252276
// +==========-+================+=================================================================================+
@@ -263,17 +287,26 @@ public void UpdateData()
263287
// | | Octet Count | the following data field(32767 or fewer octets). Note that the value of bit 7 of|
264288
// | | | octet 4(most significant bit) always will be 0. |
265289
// +-----------+----------------+---------------------------------------------------------------------------------+
266-
this.Data[i++] = IptcTagMarkerByte;
267-
this.Data[i++] = 2;
268-
this.Data[i++] = (byte)value.Tag;
269-
this.Data[i++] = (byte)(value.Length >> 8);
270-
this.Data[i++] = (byte)value.Length;
271-
if (value.Length > 0)
272-
{
273-
Buffer.BlockCopy(value.ToByteArray(), 0, this.Data, i, value.Length);
274-
i += value.Length;
275-
}
290+
offset = this.WriteRecord(offset, value.ToByteArray(), IptcRecordNumber.Application, (byte)value.Tag);
291+
}
292+
}
293+
294+
private int WriteRecord(int offset, ReadOnlySpan<byte> recordData, IptcRecordNumber recordNumber, byte recordBinaryRepresentation)
295+
{
296+
Span<byte> data = this.Data.AsSpan(offset, 5);
297+
data[0] = IptcTagMarkerByte;
298+
data[1] = (byte)recordNumber;
299+
data[2] = recordBinaryRepresentation;
300+
data[3] = (byte)(recordData.Length >> 8);
301+
data[4] = (byte)recordData.Length;
302+
offset += 5;
303+
if (recordData.Length > 0)
304+
{
305+
recordData.CopyTo(this.Data.AsSpan(offset));
306+
offset += recordData.Length;
276307
}
308+
309+
return offset;
277310
}
278311

279312
private void Initialize()
@@ -298,6 +331,7 @@ private void Initialize()
298331
bool isValidRecordNumber = recordNumber is >= 1 and <= 9;
299332
var tag = (IptcTag)this.Data[offset++];
300333
bool isValidEntry = isValidTagMarker && isValidRecordNumber;
334+
bool isApplicationRecord = recordNumber == (byte)IptcRecordNumber.Application;
301335

302336
uint byteCount = BinaryPrimitives.ReadUInt16BigEndian(this.Data.AsSpan(offset, 2));
303337
offset += 2;
@@ -307,15 +341,32 @@ private void Initialize()
307341
break;
308342
}
309343

310-
if (isValidEntry && byteCount > 0 && (offset <= this.Data.Length - byteCount))
344+
if (isValidEntry && isApplicationRecord && byteCount > 0 && (offset <= this.Data.Length - byteCount))
311345
{
312-
var iptcData = new byte[byteCount];
346+
byte[] iptcData = new byte[byteCount];
313347
Buffer.BlockCopy(this.Data, offset, iptcData, 0, (int)byteCount);
314348
this.values.Add(new IptcValue(tag, iptcData, false));
315349
}
316350

317351
offset += (int)byteCount;
318352
}
319353
}
354+
355+
/// <summary>
356+
/// Gets if any value has UTF-8 encoding.
357+
/// </summary>
358+
/// <returns>true if any value has UTF-8 encoding.</returns>
359+
private bool HasValuesInUtf8()
360+
{
361+
foreach (IptcValue value in this.values)
362+
{
363+
if (value.Encoding == Encoding.UTF8)
364+
{
365+
return true;
366+
}
367+
}
368+
369+
return false;
370+
}
320371
}
321372
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Metadata.Profiles.IPTC
5+
{
6+
/// <summary>
7+
/// Enum for the different record types of a IPTC value.
8+
/// </summary>
9+
internal enum IptcRecordNumber : byte
10+
{
11+
/// <summary>
12+
/// A Envelope Record.
13+
/// </summary>
14+
Envelope = 0x01,
15+
16+
/// <summary>
17+
/// A Application Record.
18+
/// </summary>
19+
Application = 0x02
20+
}
21+
}

tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ public void Encode_PreservesIptcProfile()
3838
{
3939
// arrange
4040
using var input = new Image<Rgba32>(1, 1);
41-
input.Metadata.IptcProfile = new IptcProfile();
42-
input.Metadata.IptcProfile.SetValue(IptcTag.Byline, "unit_test");
41+
var expectedProfile = new IptcProfile();
42+
expectedProfile.SetValue(IptcTag.Country, "ESPAÑA");
43+
expectedProfile.SetValue(IptcTag.City, "unit-test-city");
44+
input.Metadata.IptcProfile = expectedProfile;
4345

4446
// act
4547
using var memStream = new MemoryStream();
@@ -50,7 +52,7 @@ public void Encode_PreservesIptcProfile()
5052
using var output = Image.Load<Rgba32>(memStream);
5153
IptcProfile actual = output.Metadata.IptcProfile;
5254
Assert.NotNull(actual);
53-
IEnumerable<IptcValue> values = input.Metadata.IptcProfile.Values;
55+
IEnumerable<IptcValue> values = expectedProfile.Values;
5456
Assert.Equal(values, actual.Values);
5557
}
5658

tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC
1515
{
1616
public class IptcProfileTests
1717
{
18-
private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false };
18+
private static JpegDecoder JpegDecoder => new() { IgnoreMetadata = false };
1919

20-
private static TiffDecoder TiffDecoder => new TiffDecoder() { IgnoreMetadata = false };
20+
private static TiffDecoder TiffDecoder => new() { IgnoreMetadata = false };
2121

2222
public static IEnumerable<object[]> AllIptcTags()
2323
{
@@ -27,6 +27,22 @@ public static IEnumerable<object[]> AllIptcTags()
2727
}
2828
}
2929

30+
[Fact]
31+
public void IptcProfile_WithUtf8Data_WritesEnvelopeRecord_Works()
32+
{
33+
// arrange
34+
var profile = new IptcProfile();
35+
profile.SetValue(IptcTag.City, "ESPAÑA");
36+
profile.UpdateData();
37+
byte[] expectedEnvelopeData = { 28, 1, 90, 0, 3, 27, 37, 71 };
38+
39+
// act
40+
byte[] profileBytes = profile.Data;
41+
42+
// assert
43+
Assert.True(profileBytes.AsSpan(0, 8).SequenceEqual(expectedEnvelopeData));
44+
}
45+
3046
[Theory]
3147
[MemberData(nameof(AllIptcTags))]
3248
public void IptcProfile_SetValue_WithStrictEnabled_Works(IptcTag tag)

0 commit comments

Comments
 (0)