Skip to content

Commit

Permalink
Merge pull request #2213 from lmerino-ep/main
Browse files Browse the repository at this point in the history
Fix IPTC tags written on jpg files that contains non-English characters can't be correctly displayed on external apps #2212
  • Loading branch information
brianpopow authored Aug 24, 2022
2 parents 13897ae + 41bef5b commit f456ba0
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 30 deletions.
101 changes: 76 additions & 25 deletions src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -20,6 +21,11 @@ public sealed class IptcProfile : IDeepCloneable<IptcProfile>

private const uint MaxStandardDataTagSize = 0x7FFF;

/// <summary>
/// 1:90 Coded Character Set.
/// </summary>
private const byte IptcEnvelopeCodedCharacterSet = 0x5A;

/// <summary>
/// Initializes a new instance of the <see cref="IptcProfile"/> class.
/// </summary>
Expand Down Expand Up @@ -64,6 +70,11 @@ private IptcProfile(IptcProfile other)
}
}

/// <summary>
/// Gets a byte array marking that UTF-8 encoding is used in application records.
/// </summary>
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.

/// <summary>
/// Gets the byte data of the IPTC profile.
/// </summary>
Expand Down Expand Up @@ -194,6 +205,17 @@ public void SetValue(IptcTag tag, Encoding encoding, string value, bool strict =
this.values.Add(new IptcValue(tag, encoding, value, strict));
}

/// <summary>
/// Sets the value of the specified tag.
/// </summary>
/// <param name="tag">The tag of the iptc value.</param>
/// <param name="value">The value.</param>
/// <param name="strict">
/// Indicates if length restrictions from the specification should be followed strictly.
/// Defaults to true.
/// </param>
public void SetValue(IptcTag tag, string value, bool strict = true) => this.SetValue(tag, Encoding.UTF8, value, strict);

/// <summary>
/// Makes sure the datetime is formatted according to the iptc specification.
/// <example>
Expand All @@ -219,17 +241,6 @@ public void SetDateTimeValue(IptcTag tag, DateTimeOffset dateTimeOffset)
this.SetValue(tag, Encoding.UTF8, formattedDate);
}

/// <summary>
/// Sets the value of the specified tag.
/// </summary>
/// <param name="tag">The tag of the iptc value.</param>
/// <param name="value">The value.</param>
/// <param name="strict">
/// Indicates if length restrictions from the specification should be followed strictly.
/// Defaults to true.
/// </param>
public void SetValue(IptcTag tag, string value, bool strict = true) => this.SetValue(tag, Encoding.UTF8, value, strict);

/// <summary>
/// Updates the data of the profile.
/// </summary>
Expand All @@ -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 |
// +==========-+================+=================================================================================+
Expand All @@ -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<byte> recordData, IptcRecordNumber recordNumber, byte recordBinaryRepresentation)
{
Span<byte> 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()
Expand All @@ -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;
Expand All @@ -307,15 +341,32 @@ 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));
}

offset += (int)byteCount;
}
}

/// <summary>
/// Gets if any value has UTF-8 encoding.
/// </summary>
/// <returns>true if any value has UTF-8 encoding.</returns>
private bool HasValuesInUtf8()
{
foreach (IptcValue value in this.values)
{
if (value.Encoding == Encoding.UTF8)
{
return true;
}
}

return false;
}
}
}
21 changes: 21 additions & 0 deletions src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Metadata.Profiles.IPTC
{
/// <summary>
/// Enum for the different record types of a IPTC value.
/// </summary>
internal enum IptcRecordNumber : byte
{
/// <summary>
/// A Envelope Record.
/// </summary>
Envelope = 0x01,

/// <summary>
/// A Application Record.
/// </summary>
Application = 0x02
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ public void Encode_PreservesIptcProfile()
{
// arrange
using var input = new Image<Rgba32>(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();
Expand All @@ -50,7 +52,7 @@ public void Encode_PreservesIptcProfile()
using var output = Image.Load<Rgba32>(memStream);
IptcProfile actual = output.Metadata.IptcProfile;
Assert.NotNull(actual);
IEnumerable<IptcValue> values = input.Metadata.IptcProfile.Values;
IEnumerable<IptcValue> values = expectedProfile.Values;
Assert.Equal(values, actual.Values);
}

Expand Down
20 changes: 18 additions & 2 deletions tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object[]> AllIptcTags()
{
Expand All @@ -27,6 +27,22 @@ public static IEnumerable<object[]> 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)
Expand Down

0 comments on commit f456ba0

Please sign in to comment.