Browse Source

Merge pull request #1174 from SixLabors/bp/iptc

Add support for encoding and decoding IPTC metadata
pull/1178/head
James Jackson-South 6 years ago
committed by GitHub
parent
commit
eb312e0d5c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
  2. 135
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  3. 68
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  4. 7
      src/ImageSharp/Metadata/ImageMetadata.cs
  5. 3
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  6. BIN
      src/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf
  7. 298
      src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
  8. 397
      src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs
  9. 162
      src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs
  10. 219
      src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
  11. 11
      src/ImageSharp/Metadata/Profiles/IPTC/README.md
  12. 359
      tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs
  13. 2
      tests/ImageSharp.Tests/TestImages.cs
  14. BIN
      tests/Images/Input/Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg
  15. BIN
      tests/Images/Input/Jpg/baseline/iptc.jpg

24
src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs

@ -28,6 +28,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
(byte)'I', (byte)'L', (byte)'E', (byte)'\0'
};
/// <summary>
/// Gets the adobe photoshop APP13 marker which can contain IPTC meta data.
/// </summary>
public static ReadOnlySpan<byte> AdobePhotoshopApp13Marker => new[]
{
(byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', (byte)'\0'
};
/// <summary>
/// Gets the 8BIM marker, which signals the start of a adobe specific image resource block.
/// </summary>
public static ReadOnlySpan<byte> AdobeImageResourceBlockMarker => new[]
{
(byte)'8', (byte)'B', (byte)'I', (byte)'M'
};
/// <summary>
/// Gets a IPTC Image resource ID.
/// </summary>
public static ReadOnlySpan<byte> AdobeIptcMarker => new[]
{
(byte)4, (byte)4
};
/// <summary>
/// Gets the EXIF specific markers.
/// </summary>

135
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private readonly byte[] markerBuffer = new byte[2];
/// <summary>
/// The DC Huffman tables
/// The DC Huffman tables.
/// </summary>
private HuffmanTable[] dcHuffmanTables;
@ -56,37 +57,47 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private HuffmanTable[] acHuffmanTables;
/// <summary>
/// The reset interval determined by RST markers
/// The reset interval determined by RST markers.
/// </summary>
private ushort resetInterval;
/// <summary>
/// Whether the image has an EXIF marker
/// Whether the image has an EXIF marker.
/// </summary>
private bool isExif;
/// <summary>
/// Contains exif data
/// Contains exif data.
/// </summary>
private byte[] exifData;
/// <summary>
/// Whether the image has an ICC marker
/// Whether the image has an ICC marker.
/// </summary>
private bool isIcc;
/// <summary>
/// Contains ICC data
/// Contains ICC data.
/// </summary>
private byte[] iccData;
/// <summary>
/// Contains information about the JFIF marker
/// Whether the image has a IPTC data.
/// </summary>
private bool isIptc;
/// <summary>
/// Contains IPTC data.
/// </summary>
private byte[] iptcData;
/// <summary>
/// Contains information about the JFIF marker.
/// </summary>
private JFifMarker jFif;
/// <summary>
/// Contains information about the Adobe marker
/// Contains information about the Adobe marker.
/// </summary>
private AdobeMarker adobe;
@ -213,6 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.ParseStream(stream);
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitDerivedMetadataProperties();
return this.PostProcessIntoImage<TPixel>();
}
@ -226,6 +238,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.ParseStream(stream, true);
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitDerivedMetadataProperties();
return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.Metadata);
@ -344,10 +357,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
case JpegConstants.Markers.APP10:
case JpegConstants.Markers.APP11:
case JpegConstants.Markers.APP12:
case JpegConstants.Markers.APP13:
this.InputStream.Skip(remaining);
break;
case JpegConstants.Markers.APP13:
this.ProcessApp13Marker(remaining);
break;
case JpegConstants.Markers.APP14:
this.ProcessApp14Marker(remaining);
break;
@ -437,6 +453,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
/// <summary>
/// Initializes the IPTC profile.
/// </summary>
private void InitIptcProfile()
{
if (this.isIptc)
{
var profile = new IptcProfile(this.iptcData);
this.Metadata.IptcProfile = profile;
}
}
/// <summary>
/// Assigns derived metadata properties to <see cref="Metadata"/>, eg. horizontal and vertical resolution if it has a JFIF header.
/// </summary>
@ -582,6 +610,95 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
/// <summary>
/// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop.
/// The content of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks.
/// </summary>
/// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApp13Marker(int remaining)
{
if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length || this.IgnoreMetadata)
{
this.InputStream.Skip(remaining);
return;
}
this.InputStream.Read(this.temp, 0, ProfileResolver.AdobePhotoshopApp13Marker.Length);
remaining -= ProfileResolver.AdobePhotoshopApp13Marker.Length;
if (ProfileResolver.IsProfile(this.temp, ProfileResolver.AdobePhotoshopApp13Marker))
{
var resourceBlockData = new byte[remaining];
this.InputStream.Read(resourceBlockData, 0, remaining);
Span<byte> blockDataSpan = resourceBlockData.AsSpan();
while (blockDataSpan.Length > 12)
{
if (!ProfileResolver.IsProfile(blockDataSpan.Slice(0, 4), ProfileResolver.AdobeImageResourceBlockMarker))
{
return;
}
blockDataSpan = blockDataSpan.Slice(4);
Span<byte> imageResourceBlockId = blockDataSpan.Slice(0, 2);
if (ProfileResolver.IsProfile(imageResourceBlockId, ProfileResolver.AdobeIptcMarker))
{
var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan);
var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength);
int dataStartIdx = 2 + resourceBlockNameLength + 4;
if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize)
{
this.isIptc = true;
this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray();
break;
}
}
else
{
var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan);
var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength);
int dataStartIdx = 2 + resourceBlockNameLength + 4;
if (blockDataSpan.Length < dataStartIdx + resourceDataSize)
{
// Not enough data or the resource data size is wrong.
break;
}
blockDataSpan = blockDataSpan.Slice(dataStartIdx + resourceDataSize);
}
}
}
}
/// <summary>
/// Reads the adobe image resource block name: a Pascal string (padded to make size even).
/// </summary>
/// <param name="blockDataSpan">The span holding the block resource data.</param>
/// <returns>The length of the name.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int ReadImageResourceNameLength(Span<byte> blockDataSpan)
{
byte nameLength = blockDataSpan[2];
var nameDataSize = nameLength == 0 ? 2 : nameLength;
if (nameDataSize % 2 != 0)
{
nameDataSize++;
}
return nameDataSize;
}
/// <summary>
/// Reads the length of a adobe image resource data block.
/// </summary>
/// <param name="blockDataSpan">The span holding the block resource data.</param>
/// <param name="resourceBlockNameLength">The length of the block name.</param>
/// <returns>The block length.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int ReadResourceDataLength(Span<byte> blockDataSpan, int resourceBlockNameLength)
{
return BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4));
}
/// <summary>
/// Processes the application header containing the Adobe identifier
/// which stores image encoding information for DCT filters.

68
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -4,6 +4,7 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
@ -13,6 +14,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -231,7 +233,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Write the Start Of Image marker.
this.WriteApplicationHeader(metadata);
// Write Exif and ICC profiles
// Write Exif, ICC and IPTC profiles
this.WriteProfiles(metadata);
// Write the quantization tables.
@ -647,9 +649,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Writes the EXIF profile.
/// </summary>
/// <param name="exifProfile">The exif profile.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the EXIF profile size exceeds the limit
/// </exception>
private void WriteExifProfile(ExifProfile exifProfile)
{
if (exifProfile is null || exifProfile.Values.Count == 0)
@ -697,16 +696,68 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
/// <summary>
/// Writes the IPTC metadata.
/// </summary>
/// <param name="iptcProfile">The iptc metadata to write.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the IPTC profile size exceeds the limit of 65533 bytes.
/// </exception>
private void WriteIptcProfile(IptcProfile iptcProfile)
{
const int Max = 65533;
if (iptcProfile is null || !iptcProfile.Values.Any())
{
return;
}
iptcProfile.UpdateData();
byte[] data = iptcProfile.Data;
if (data.Length == 0)
{
return;
}
if (data.Length > Max)
{
throw new ImageFormatException($"Iptc profile size exceeds limit of {Max} bytes");
}
var app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length +
ProfileResolver.AdobeImageResourceBlockMarker.Length +
ProfileResolver.AdobeIptcMarker.Length +
2 + 4 + data.Length;
this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13);
this.outputStream.Write(ProfileResolver.AdobePhotoshopApp13Marker);
this.outputStream.Write(ProfileResolver.AdobeImageResourceBlockMarker);
this.outputStream.Write(ProfileResolver.AdobeIptcMarker);
this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even)
this.outputStream.WriteByte(0);
BinaryPrimitives.WriteInt32BigEndian(this.buffer, data.Length);
this.outputStream.Write(this.buffer, 0, 4);
this.outputStream.Write(data, 0, data.Length);
}
/// <summary>
/// Writes the App1 header.
/// </summary>
/// <param name="app1Length">The length of the data the app1 marker contains</param>
/// <param name="app1Length">The length of the data the app1 marker contains.</param>
private void WriteApp1Header(int app1Length)
{
this.WriteAppHeader(app1Length, JpegConstants.Markers.APP1);
}
/// <summary>
/// Writes a AppX header.
/// </summary>
/// <param name="length">The length of the data the app marker contains.</param>
/// <param name="appMarker">The app marker to write.</param>
private void WriteAppHeader(int length, byte appMarker)
{
this.buffer[0] = JpegConstants.Markers.XFF;
this.buffer[1] = JpegConstants.Markers.APP1; // Application Marker
this.buffer[2] = (byte)((app1Length >> 8) & 0xFF);
this.buffer[3] = (byte)(app1Length & 0xFF);
this.buffer[1] = appMarker;
this.buffer[2] = (byte)((length >> 8) & 0xFF);
this.buffer[3] = (byte)(length & 0xFF);
this.outputStream.Write(this.buffer, 0, 4);
}
@ -805,6 +856,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
metadata.SyncProfiles();
this.WriteExifProfile(metadata.ExifProfile);
this.WriteIccProfile(metadata.IccProfile);
this.WriteIptcProfile(metadata.IptcProfile);
}
/// <summary>

7
src/ImageSharp/Metadata/ImageMetadata.cs

@ -5,6 +5,7 @@ using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
namespace SixLabors.ImageSharp.Metadata
{
@ -65,6 +66,7 @@ namespace SixLabors.ImageSharp.Metadata
this.ExifProfile = other.ExifProfile?.DeepClone();
this.IccProfile = other.IccProfile?.DeepClone();
this.IptcProfile = other.IptcProfile?.DeepClone();
}
/// <summary>
@ -122,6 +124,11 @@ namespace SixLabors.ImageSharp.Metadata
/// </summary>
public IccProfile IccProfile { get; set; }
/// <summary>
/// Gets or sets the iptc profile.
/// </summary>
public IptcProfile IptcProfile { get; set; }
/// <summary>
/// Gets the metadata value associated with the specified key.
/// </summary>

3
src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs

@ -57,8 +57,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// by making a copy from another EXIF profile.
/// </summary>
/// <param name="other">The other EXIF profile, where the clone should be made from.</param>
/// <exception cref="ArgumentNullException"><paramref name="other"/> is null.</exception>>
private ExifProfile(ExifProfile other)
{
Guard.NotNull(other, nameof(other));
this.Parts = other.Parts;
this.thumbnailLength = other.thumbnailLength;
this.thumbnailOffset = other.thumbnailOffset;

BIN
src/ImageSharp/Metadata/Profiles/IPTC/IIMV4.2_IPTC.pdf

Binary file not shown.

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

@ -0,0 +1,298 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
{
/// <summary>
/// Represents an IPTC profile providing access to the collection of values.
/// </summary>
public sealed class IptcProfile : IDeepCloneable<IptcProfile>
{
private Collection<IptcValue> values;
/// <summary>
/// Initializes a new instance of the <see cref="IptcProfile"/> class.
/// </summary>
public IptcProfile()
: this((byte[])null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="IptcProfile"/> class.
/// </summary>
/// <param name="data">The byte array to read the iptc profile from.</param>
public IptcProfile(byte[] data)
{
this.Data = data;
this.Initialize();
}
/// <summary>
/// Initializes a new instance of the <see cref="IptcProfile"/> class
/// by making a copy from another IPTC profile.
/// </summary>
/// <param name="other">The other IPTC profile, from which the clone should be made from.</param>
private IptcProfile(IptcProfile other)
{
Guard.NotNull(other, nameof(other));
if (other.values != null)
{
this.values = new Collection<IptcValue>();
foreach (IptcValue value in other.Values)
{
this.values.Add(value.DeepClone());
}
}
if (other.Data != null)
{
this.Data = new byte[other.Data.Length];
other.Data.AsSpan().CopyTo(this.Data);
}
}
/// <summary>
/// Gets the byte data of the IPTC profile.
/// </summary>
public byte[] Data { get; private set; }
/// <summary>
/// Gets the values of this iptc profile.
/// </summary>
public IEnumerable<IptcValue> Values
{
get
{
this.Initialize();
return this.values;
}
}
/// <inheritdoc/>
public IptcProfile DeepClone() => new IptcProfile(this);
/// <summary>
/// Returns all value with the specified tag.
/// </summary>
/// <param name="tag">The tag of the iptc value.</param>
/// <returns>The values found with the specified tag.</returns>
public List<IptcValue> GetValues(IptcTag tag)
{
var iptcValues = new List<IptcValue>();
foreach (IptcValue iptcValue in this.Values)
{
if (iptcValue.Tag == tag)
{
iptcValues.Add(iptcValue);
}
}
return iptcValues;
}
/// <summary>
/// Removes all values with the specified tag.
/// </summary>
/// <param name="tag">The tag of the iptc value to remove.</param>
/// <returns>True when the value was found and removed.</returns>
public bool RemoveValue(IptcTag tag)
{
this.Initialize();
bool removed = false;
for (int i = this.values.Count - 1; i >= 0; i--)
{
if (this.values[i].Tag == tag)
{
this.values.RemoveAt(i);
removed = true;
}
}
return removed;
}
/// <summary>
/// Removes values with the specified tag and value.
/// </summary>
/// <param name="tag">The tag of the iptc value to remove.</param>
/// <param name="value">The value of the iptc item to remove.</param>
/// <returns>True when the value was found and removed.</returns>
public bool RemoveValue(IptcTag tag, string value)
{
this.Initialize();
bool removed = false;
for (int i = this.values.Count - 1; i >= 0; i--)
{
if (this.values[i].Tag == tag && this.values[i].Value.Equals(value))
{
this.values.RemoveAt(i);
removed = true;
}
}
return removed;
}
/// <summary>
/// Changes the encoding for all the values.
/// </summary>
/// <param name="encoding">The encoding to use when storing the bytes.</param>
public void SetEncoding(Encoding encoding)
{
Guard.NotNull(encoding, nameof(encoding));
foreach (IptcValue value in this.Values)
{
value.Encoding = encoding;
}
}
/// <summary>
/// Sets the value for the specified tag.
/// </summary>
/// <param name="tag">The tag of the iptc value.</param>
/// <param name="encoding">The encoding to use when storing the bytes.</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, Encoding encoding, string value, bool strict = true)
{
Guard.NotNull(encoding, nameof(encoding));
Guard.NotNull(value, nameof(value));
if (!tag.IsRepeatable())
{
foreach (IptcValue iptcValue in this.Values)
{
if (iptcValue.Tag == tag)
{
iptcValue.Strict = strict;
iptcValue.Encoding = encoding;
iptcValue.Value = value;
return;
}
}
}
this.values.Add(new IptcValue(tag, encoding, value, strict));
}
/// <summary>
/// Makes sure the datetime is formatted according to the iptc specification.
/// <example>
/// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
/// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
/// two hours ahead of UTC.
/// </example>
/// </summary>
/// <param name="tag">The tag of the iptc value.</param>
/// <param name="dateTimeOffset">The datetime.</param>
public void SetDateTimeValue(IptcTag tag, DateTimeOffset dateTimeOffset)
{
if (!tag.IsDate() && !tag.IsTime())
{
throw new ArgumentException("iptc tag is not a time or date type");
}
var formattedDate = tag.IsDate()
? dateTimeOffset.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture)
: dateTimeOffset.ToString("HHmmsszzzz", System.Globalization.CultureInfo.InvariantCulture)
.Replace(":", string.Empty);
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>
public void UpdateData()
{
var length = 0;
foreach (IptcValue value in this.Values)
{
length += value.Length + 5;
}
this.Data = new byte[length];
int i = 0;
foreach (IptcValue value in this.Values)
{
this.Data[i++] = 28;
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;
}
}
}
private void Initialize()
{
if (this.values != null)
{
return;
}
this.values = new Collection<IptcValue>();
if (this.Data == null || this.Data[0] != 0x1c)
{
return;
}
int i = 0;
while (i + 4 < this.Data.Length)
{
if (this.Data[i++] != 28)
{
continue;
}
i++;
var tag = (IptcTag)this.Data[i++];
int count = BinaryPrimitives.ReadInt16BigEndian(this.Data.AsSpan(i, 2));
i += 2;
var iptcData = new byte[count];
if ((count > 0) && (i + count <= this.Data.Length))
{
Buffer.BlockCopy(this.Data, i, iptcData, 0, count);
this.values.Add(new IptcValue(tag, iptcData, false));
}
i += count;
}
}
}
}

397
src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs

@ -0,0 +1,397 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
{
/// <summary>
/// Provides enumeration of all IPTC tags relevant for images.
/// </summary>
public enum IptcTag
{
/// <summary>
/// Unknown.
/// </summary>
Unknown = -1,
/// <summary>
/// Record version identifying the version of the Information Interchange Model.
/// Not repeatable. Max length is 2.
/// </summary>
RecordVersion = 0,
/// <summary>
/// Object type, not repeatable. Max Length is 67.
/// </summary>
ObjectType = 3,
/// <summary>
/// Object attribute. Max length is 68.
/// </summary>
ObjectAttribute = 4,
/// <summary>
/// Object Name, not repeatable. Max length is 64.
/// </summary>
Name = 5,
/// <summary>
/// Edit status, not repeatable. Max length is 64.
/// </summary>
EditStatus = 7,
/// <summary>
/// Editorial update, not repeatable. Max length is 2.
/// </summary>
EditorialUpdate = 8,
/// <summary>
/// Urgency, not repeatable. Max length is 2.
/// </summary>
Urgency = 10,
/// <summary>
/// Subject Reference. Max length is 236.
/// </summary>
SubjectReference = 12,
/// <summary>
/// Category, not repeatable. Max length is 3.
/// </summary>
Category = 15,
/// <summary>
/// Supplemental categories. Max length is 32.
/// </summary>
SupplementalCategories = 20,
/// <summary>
/// Fixture identifier, not repeatable. Max length is 32.
/// </summary>
FixtureIdentifier = 22,
/// <summary>
/// Keywords. Max length is 64.
/// </summary>
Keywords = 25,
/// <summary>
/// Location code. Max length is 3.
/// </summary>
LocationCode = 26,
/// <summary>
/// Location name. Max length is 64.
/// </summary>
LocationName = 27,
/// <summary>
/// Release date. Format should be CCYYMMDD.
/// Not repeatable, max length is 8.
/// <example>
/// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
/// </example>
/// </summary>
ReleaseDate = 30,
/// <summary>
/// Release time. Format should be HHMMSS±HHMM.
/// Not repeatable, max length is 11.
/// <example>
/// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
/// two hours ahead of UTC.
/// </example>
/// </summary>
ReleaseTime = 35,
/// <summary>
/// Expiration date. Format should be CCYYMMDD.
/// Not repeatable, max length is 8.
/// <example>
/// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
/// </example>
/// </summary>
ExpirationDate = 37,
/// <summary>
/// Expiration time. Format should be HHMMSS±HHMM.
/// Not repeatable, max length is 11.
/// <example>
/// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
/// two hours ahead of UTC.
/// </example>
/// </summary>
ExpirationTime = 38,
/// <summary>
/// Special instructions, not repeatable. Max length is 256.
/// </summary>
SpecialInstructions = 40,
/// <summary>
/// Action advised, not repeatable. Max length is 2.
/// </summary>
ActionAdvised = 42,
/// <summary>
/// Reference service. Max length is 10.
/// </summary>
ReferenceService = 45,
/// <summary>
/// Reference date. Format should be CCYYMMDD.
/// Not repeatable, max length is 8.
/// <example>
/// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
/// </example>
/// </summary>
ReferenceDate = 47,
/// <summary>
/// ReferenceNumber. Max length is 8.
/// </summary>
ReferenceNumber = 50,
/// <summary>
/// Created date. Format should be CCYYMMDD.
/// Not repeatable, max length is 8.
/// <example>
/// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
/// </example>
/// </summary>
CreatedDate = 55,
/// <summary>
/// Created time. Format should be HHMMSS±HHMM.
/// Not repeatable, max length is 11.
/// <example>
/// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
/// two hours ahead of UTC.
/// </example>
/// </summary>
CreatedTime = 60,
/// <summary>
/// Digital creation date. Format should be CCYYMMDD.
/// Not repeatable, max length is 8.
/// <example>
/// A date will be formatted as CCYYMMDD, e.g. "19890317" for 17 March 1989.
/// </example>
/// </summary>
DigitalCreationDate = 62,
/// <summary>
/// Digital creation time. Format should be HHMMSS±HHMM.
/// Not repeatable, max length is 11.
/// <example>
/// A time value will be formatted as HHMMSS±HHMM, e.g. "090000+0200" for 9 o'clock Berlin time,
/// two hours ahead of UTC.
/// </example>
/// </summary>
DigitalCreationTime = 63,
/// <summary>
/// Originating program, not repeatable. Max length is 32.
/// </summary>
OriginatingProgram = 65,
/// <summary>
/// Program version, not repeatable. Max length is 10.
/// </summary>
ProgramVersion = 70,
/// <summary>
/// Object cycle, not repeatable. Max length is 1.
/// </summary>
ObjectCycle = 75,
/// <summary>
/// Byline. Max length is 32.
/// </summary>
Byline = 80,
/// <summary>
/// Byline title. Max length is 32.
/// </summary>
BylineTitle = 85,
/// <summary>
/// City, not repeatable. Max length is 32.
/// </summary>
City = 90,
/// <summary>
/// Sub location, not repeatable. Max length is 32.
/// </summary>
SubLocation = 92,
/// <summary>
/// Province/State, not repeatable. Max length is 32.
/// </summary>
ProvinceState = 95,
/// <summary>
/// Country code, not repeatable. Max length is 3.
/// </summary>
CountryCode = 100,
/// <summary>
/// Country, not repeatable. Max length is 64.
/// </summary>
Country = 101,
/// <summary>
/// Original transmission reference, not repeatable. Max length is 32.
/// </summary>
OriginalTransmissionReference = 103,
/// <summary>
/// Headline, not repeatable. Max length is 256.
/// </summary>
Headline = 105,
/// <summary>
/// Credit, not repeatable. Max length is 32.
/// </summary>
Credit = 110,
/// <summary>
/// Source, not repeatable. Max length is 32.
/// </summary>
Source = 115,
/// <summary>
/// Copyright notice, not repeatable. Max length is 128.
/// </summary>
CopyrightNotice = 116,
/// <summary>
/// Contact. Max length 128.
/// </summary>
Contact = 118,
/// <summary>
/// Caption, not repeatable. Max length is 2000.
/// </summary>
Caption = 120,
/// <summary>
/// Local caption.
/// </summary>
LocalCaption = 121,
/// <summary>
/// Caption writer. Max length is 32.
/// </summary>
CaptionWriter = 122,
/// <summary>
/// Image type, not repeatable. Max length is 2.
/// </summary>
ImageType = 130,
/// <summary>
/// Image orientation, not repeatable. Max length is 1.
/// </summary>
ImageOrientation = 131,
/// <summary>
/// Custom field 1
/// </summary>
CustomField1 = 200,
/// <summary>
/// Custom field 2
/// </summary>
CustomField2 = 201,
/// <summary>
/// Custom field 3
/// </summary>
CustomField3 = 202,
/// <summary>
/// Custom field 4
/// </summary>
CustomField4 = 203,
/// <summary>
/// Custom field 5
/// </summary>
CustomField5 = 204,
/// <summary>
/// Custom field 6
/// </summary>
CustomField6 = 205,
/// <summary>
/// Custom field 7
/// </summary>
CustomField7 = 206,
/// <summary>
/// Custom field 8
/// </summary>
CustomField8 = 207,
/// <summary>
/// Custom field 9
/// </summary>
CustomField9 = 208,
/// <summary>
/// Custom field 10
/// </summary>
CustomField10 = 209,
/// <summary>
/// Custom field 11
/// </summary>
CustomField11 = 210,
/// <summary>
/// Custom field 12
/// </summary>
CustomField12 = 211,
/// <summary>
/// Custom field 13
/// </summary>
CustomField13 = 212,
/// <summary>
/// Custom field 14
/// </summary>
CustomField14 = 213,
/// <summary>
/// Custom field 15
/// </summary>
CustomField15 = 214,
/// <summary>
/// Custom field 16
/// </summary>
CustomField16 = 215,
/// <summary>
/// Custom field 17
/// </summary>
CustomField17 = 216,
/// <summary>
/// Custom field 18
/// </summary>
CustomField18 = 217,
/// <summary>
/// Custom field 19
/// </summary>
CustomField19 = 218,
/// <summary>
/// Custom field 20
/// </summary>
CustomField20 = 219,
}
}

162
src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs

@ -0,0 +1,162 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
{
/// <summary>
/// Extension methods for IPTC tags.
/// </summary>
public static class IptcTagExtensions
{
/// <summary>
/// Maximum length of the IPTC value with the given tag according to the specification.
/// </summary>
/// <param name="tag">The tag to check the max length for.</param>
/// <returns>The maximum length.</returns>
public static int MaxLength(this IptcTag tag)
{
return tag switch
{
IptcTag.RecordVersion => 2,
IptcTag.ObjectType => 67,
IptcTag.ObjectAttribute => 68,
IptcTag.Name => 64,
IptcTag.EditStatus => 64,
IptcTag.EditorialUpdate => 2,
IptcTag.Urgency => 1,
IptcTag.SubjectReference => 236,
IptcTag.Category => 3,
IptcTag.SupplementalCategories => 32,
IptcTag.FixtureIdentifier => 32,
IptcTag.Keywords => 64,
IptcTag.LocationCode => 3,
IptcTag.LocationName => 64,
IptcTag.ReleaseDate => 8,
IptcTag.ReleaseTime => 11,
IptcTag.ExpirationDate => 8,
IptcTag.ExpirationTime => 11,
IptcTag.SpecialInstructions => 256,
IptcTag.ActionAdvised => 2,
IptcTag.ReferenceService => 10,
IptcTag.ReferenceDate => 8,
IptcTag.ReferenceNumber => 8,
IptcTag.CreatedDate => 8,
IptcTag.CreatedTime => 11,
IptcTag.DigitalCreationDate => 8,
IptcTag.DigitalCreationTime => 11,
IptcTag.OriginatingProgram => 32,
IptcTag.ProgramVersion => 10,
IptcTag.ObjectCycle => 1,
IptcTag.Byline => 32,
IptcTag.BylineTitle => 32,
IptcTag.City => 32,
IptcTag.SubLocation => 32,
IptcTag.ProvinceState => 32,
IptcTag.CountryCode => 3,
IptcTag.Country => 64,
IptcTag.OriginalTransmissionReference => 32,
IptcTag.Headline => 256,
IptcTag.Credit => 32,
IptcTag.Source => 32,
IptcTag.CopyrightNotice => 128,
IptcTag.Contact => 128,
IptcTag.Caption => 2000,
IptcTag.CaptionWriter => 32,
IptcTag.ImageType => 2,
IptcTag.ImageOrientation => 1,
_ => 256
};
}
/// <summary>
/// Determines if the given tag can be repeated according to the specification.
/// </summary>
/// <param name="tag">The tag to check.</param>
/// <returns>True, if the tag can occur multiple times.</returns>
public static bool IsRepeatable(this IptcTag tag)
{
switch (tag)
{
case IptcTag.RecordVersion:
case IptcTag.ObjectType:
case IptcTag.Name:
case IptcTag.EditStatus:
case IptcTag.EditorialUpdate:
case IptcTag.Urgency:
case IptcTag.Category:
case IptcTag.FixtureIdentifier:
case IptcTag.ReleaseDate:
case IptcTag.ReleaseTime:
case IptcTag.ExpirationDate:
case IptcTag.ExpirationTime:
case IptcTag.SpecialInstructions:
case IptcTag.ActionAdvised:
case IptcTag.CreatedDate:
case IptcTag.CreatedTime:
case IptcTag.DigitalCreationDate:
case IptcTag.DigitalCreationTime:
case IptcTag.OriginatingProgram:
case IptcTag.ProgramVersion:
case IptcTag.ObjectCycle:
case IptcTag.City:
case IptcTag.SubLocation:
case IptcTag.ProvinceState:
case IptcTag.CountryCode:
case IptcTag.Country:
case IptcTag.OriginalTransmissionReference:
case IptcTag.Headline:
case IptcTag.Credit:
case IptcTag.Source:
case IptcTag.CopyrightNotice:
case IptcTag.Caption:
case IptcTag.ImageType:
case IptcTag.ImageOrientation:
return false;
default:
return true;
}
}
/// <summary>
/// Determines if the tag is a datetime tag which needs to be formatted as CCYYMMDD.
/// </summary>
/// <param name="tag">The tag to check.</param>
/// <returns>True, if its a datetime tag.</returns>
public static bool IsDate(this IptcTag tag)
{
switch (tag)
{
case IptcTag.CreatedDate:
case IptcTag.DigitalCreationDate:
case IptcTag.ExpirationDate:
case IptcTag.ReferenceDate:
case IptcTag.ReleaseDate:
return true;
default:
return false;
}
}
/// <summary>
/// Determines if the tag is a time tag which need to be formatted as HHMMSS±HHMM.
/// </summary>
/// <param name="tag">The tag to check.</param>
/// <returns>True, if its a time tag.</returns>
public static bool IsTime(this IptcTag tag)
{
switch (tag)
{
case IptcTag.CreatedTime:
case IptcTag.DigitalCreationTime:
case IptcTag.ExpirationTime:
case IptcTag.ReleaseTime:
return true;
default:
return false;
}
}
}
}

219
src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs

@ -0,0 +1,219 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc
{
/// <summary>
/// Represents a single value of the IPTC profile.
/// </summary>
public sealed class IptcValue : IDeepCloneable<IptcValue>
{
private byte[] data = Array.Empty<byte>();
private Encoding encoding;
internal IptcValue(IptcValue other)
{
if (other.data != null)
{
this.data = new byte[other.data.Length];
other.data.AsSpan().CopyTo(this.data);
}
if (other.Encoding != null)
{
this.Encoding = (Encoding)other.Encoding.Clone();
}
this.Tag = other.Tag;
this.Strict = other.Strict;
}
internal IptcValue(IptcTag tag, byte[] value, bool strict)
{
Guard.NotNull(value, nameof(value));
this.Strict = strict;
this.Tag = tag;
this.data = value;
this.encoding = Encoding.UTF8;
}
internal IptcValue(IptcTag tag, Encoding encoding, string value, bool strict)
{
this.Strict = strict;
this.Tag = tag;
this.encoding = encoding;
this.Value = value;
}
internal IptcValue(IptcTag tag, string value, bool strict)
{
this.Strict = strict;
this.Tag = tag;
this.encoding = Encoding.UTF8;
this.Value = value;
}
/// <summary>
/// Gets or sets the encoding to use for the Value.
/// </summary>
public Encoding Encoding
{
get => this.encoding;
set
{
if (value != null)
{
this.encoding = value;
}
}
}
/// <summary>
/// Gets the tag of the iptc value.
/// </summary>
public IptcTag Tag { get; }
/// <summary>
/// Gets or sets a value indicating whether to be enforce value length restrictions according
/// to the specification.
/// </summary>
public bool Strict { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
public string Value
{
get => this.encoding.GetString(this.data);
set
{
if (string.IsNullOrEmpty(value))
{
this.data = Array.Empty<byte>();
}
else
{
int maxLength = this.Tag.MaxLength();
byte[] valueBytes;
if (this.Strict && value.Length > maxLength)
{
var cappedValue = value.Substring(0, maxLength);
valueBytes = this.encoding.GetBytes(cappedValue);
// It is still possible that the bytes of the string exceed the limit.
if (valueBytes.Length > maxLength)
{
throw new ArgumentException($"The iptc value exceeds the limit of {maxLength} bytes for the tag {this.Tag}");
}
}
else
{
valueBytes = this.encoding.GetBytes(value);
}
this.data = valueBytes;
}
}
}
/// <summary>
/// Gets the length of the value.
/// </summary>
public int Length => this.data.Length;
/// <inheritdoc/>
public IptcValue DeepClone() => new IptcValue(this);
/// <summary>
/// Determines whether the specified object is equal to the current <see cref="IptcValue"/>.
/// </summary>
/// <param name="obj">The object to compare this <see cref="IptcValue"/> with.</param>
/// <returns>True when the specified object is equal to the current <see cref="IptcValue"/>.</returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}
return this.Equals(obj as IptcValue);
}
/// <summary>
/// Determines whether the specified iptc value is equal to the current <see cref="IptcValue"/>.
/// </summary>
/// <param name="other">The iptc value to compare this <see cref="IptcValue"/> with.</param>
/// <returns>True when the specified iptc value is equal to the current <see cref="IptcValue"/>.</returns>
public bool Equals(IptcValue other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
if (this.Tag != other.Tag)
{
return false;
}
if (this.data.Length != other.data.Length)
{
return false;
}
for (int i = 0; i < this.data.Length; i++)
{
if (this.data[i] != other.data[i])
{
return false;
}
}
return true;
}
/// <summary>
/// Serves as a hash of this type.
/// </summary>
/// <returns>A hash code for the current instance.</returns>
public override int GetHashCode() => HashCode.Combine(this.data, this.Tag);
/// <summary>
/// Converts this instance to a byte array.
/// </summary>
/// <returns>A <see cref="byte"/> array.</returns>
public byte[] ToByteArray()
{
var result = new byte[this.data.Length];
this.data.CopyTo(result, 0);
return result;
}
/// <summary>
/// Returns a string that represents the current value.
/// </summary>
/// <returns>A string that represents the current value.</returns>
public override string ToString() => this.Value;
/// <summary>
/// Returns a string that represents the current value with the specified encoding.
/// </summary>
/// <param name="encoding">The encoding to use.</param>
/// <returns>A string that represents the current value with the specified encoding.</returns>
public string ToString(Encoding encoding)
{
Guard.NotNull(encoding, nameof(encoding));
return encoding.GetString(this.data);
}
}
}

11
src/ImageSharp/Metadata/Profiles/IPTC/README.md

@ -0,0 +1,11 @@
IPTC source code is from [Magick.NET](https://github.com/dlemstra/Magick.NET)
Information about IPTC can be found here in the following sources:
- [metacpan.org, APP13-segment](https://metacpan.org/pod/Image::MetaData::JPEG::Structures#Structure-of-a-Photoshop-style-APP13-segment)
- [iptc.org](https://www.iptc.org/std/photometadata/documentation/userguide/)
- [Adobe File Formats Specification](http://oldschoolprg.x10.mx/downloads/ps6ffspecsv2.pdf)
- [Tag Overview](https://exiftool.org/TagNames/IPTC.html)

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

@ -0,0 +1,359 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC
{
public class IptcProfileTests
{
private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false };
public static IEnumerable<object[]> AllIptcTags()
{
foreach (object tag in Enum.GetValues(typeof(IptcTag)))
{
yield return new object[] { tag };
}
}
[Theory]
[MemberData("AllIptcTags")]
public void IptcProfile_SetValue_WithStrictOption_Works(IptcTag tag)
{
// arrange
var profile = new IptcProfile();
var value = new string('s', tag.MaxLength() + 1);
var expectedLength = tag.MaxLength();
// act
profile.SetValue(tag, value);
// assert
IptcValue actual = profile.GetValues(tag).First();
Assert.Equal(expectedLength, actual.Value.Length);
}
[Theory]
[InlineData(IptcTag.DigitalCreationDate)]
[InlineData(IptcTag.ExpirationDate)]
[InlineData(IptcTag.CreatedDate)]
[InlineData(IptcTag.ReferenceDate)]
[InlineData(IptcTag.ReleaseDate)]
public void IptcProfile_SetDateValue_Works(IptcTag tag)
{
// arrange
var profile = new IptcProfile();
var datetime = new DateTimeOffset(new DateTime(1994, 3, 17));
// act
profile.SetDateTimeValue(tag, datetime);
// assert
IptcValue actual = profile.GetValues(tag).First();
Assert.Equal("19940317", actual.Value);
}
[Theory]
[InlineData(IptcTag.CreatedTime)]
[InlineData(IptcTag.DigitalCreationTime)]
[InlineData(IptcTag.ExpirationTime)]
[InlineData(IptcTag.ReleaseTime)]
public void IptcProfile_SetTimeValue_Works(IptcTag tag)
{
// arrange
var profile = new IptcProfile();
var dateTimeUtc = new DateTime(1994, 3, 17, 14, 15, 16, DateTimeKind.Utc);
DateTimeOffset dateTimeOffset = new DateTimeOffset(dateTimeUtc).ToOffset(TimeSpan.FromHours(2));
// act
profile.SetDateTimeValue(tag, dateTimeOffset);
// assert
IptcValue actual = profile.GetValues(tag).First();
Assert.Equal("161516+0200", actual.Value);
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Iptc, PixelTypes.Rgba32)]
public void ReadIptcMetadata_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(JpegDecoder))
{
Assert.NotNull(image.Metadata.IptcProfile);
var iptcValues = image.Metadata.IptcProfile.Values.ToList();
ContainsIptcValue(iptcValues, IptcTag.Caption, "description");
ContainsIptcValue(iptcValues, IptcTag.CaptionWriter, "description writer");
ContainsIptcValue(iptcValues, IptcTag.Headline, "headline");
ContainsIptcValue(iptcValues, IptcTag.SpecialInstructions, "special instructions");
ContainsIptcValue(iptcValues, IptcTag.Byline, "author");
ContainsIptcValue(iptcValues, IptcTag.BylineTitle, "author title");
ContainsIptcValue(iptcValues, IptcTag.Credit, "credits");
ContainsIptcValue(iptcValues, IptcTag.Source, "source");
ContainsIptcValue(iptcValues, IptcTag.Name, "title");
ContainsIptcValue(iptcValues, IptcTag.CreatedDate, "20200414");
ContainsIptcValue(iptcValues, IptcTag.City, "city");
ContainsIptcValue(iptcValues, IptcTag.SubLocation, "sublocation");
ContainsIptcValue(iptcValues, IptcTag.ProvinceState, "province-state");
ContainsIptcValue(iptcValues, IptcTag.Country, "country");
ContainsIptcValue(iptcValues, IptcTag.Category, "category");
ContainsIptcValue(iptcValues, IptcTag.Urgency, "1");
ContainsIptcValue(iptcValues, IptcTag.Keywords, "keywords");
ContainsIptcValue(iptcValues, IptcTag.CopyrightNotice, "copyright");
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.App13WithEmptyIptc, PixelTypes.Rgba32)]
public void ReadApp13_WithEmptyIptc_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(JpegDecoder);
Assert.Null(image.Metadata.IptcProfile);
}
[Fact]
public void IptcProfile_ToAndFromByteArray_Works()
{
// arrange
var profile = new IptcProfile();
var expectedCaptionWriter = "unittest";
var expectedCaption = "test";
profile.SetValue(IptcTag.CaptionWriter, expectedCaptionWriter);
profile.SetValue(IptcTag.Caption, expectedCaption);
// act
profile.UpdateData();
byte[] profileBytes = profile.Data;
var profileFromBytes = new IptcProfile(profileBytes);
// assert
var iptcValues = profileFromBytes.Values.ToList();
ContainsIptcValue(iptcValues, IptcTag.CaptionWriter, expectedCaptionWriter);
ContainsIptcValue(iptcValues, IptcTag.Caption, expectedCaption);
}
[Fact]
public void IptcProfile_CloneIsDeep()
{
// arrange
var profile = new IptcProfile();
var captionWriter = "unittest";
var caption = "test";
profile.SetValue(IptcTag.CaptionWriter, captionWriter);
profile.SetValue(IptcTag.Caption, caption);
// act
IptcProfile clone = profile.DeepClone();
clone.SetValue(IptcTag.Caption, "changed");
// assert
Assert.Equal(2, clone.Values.Count());
var cloneValues = clone.Values.ToList();
ContainsIptcValue(cloneValues, IptcTag.CaptionWriter, captionWriter);
ContainsIptcValue(cloneValues, IptcTag.Caption, "changed");
ContainsIptcValue(profile.Values.ToList(), IptcTag.Caption, caption);
}
[Fact]
public void IptcValue_CloneIsDeep()
{
// arrange
var iptcValue = new IptcValue(IptcTag.Caption, System.Text.Encoding.UTF8, "test", true);
// act
IptcValue clone = iptcValue.DeepClone();
clone.Value = "changed";
// assert
Assert.NotEqual(iptcValue.Value, clone.Value);
}
[Fact]
public void WritingImage_PreservesIptcProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
image.Metadata.IptcProfile = new IptcProfile();
var expectedCaptionWriter = "unittest";
var expectedCaption = "test";
image.Metadata.IptcProfile.SetValue(IptcTag.CaptionWriter, expectedCaptionWriter);
image.Metadata.IptcProfile.SetValue(IptcTag.Caption, expectedCaption);
// act
Image<Rgba32> reloadedImage = WriteAndReadJpeg(image);
// assert
IptcProfile actual = reloadedImage.Metadata.IptcProfile;
Assert.NotNull(actual);
var iptcValues = actual.Values.ToList();
ContainsIptcValue(iptcValues, IptcTag.CaptionWriter, expectedCaptionWriter);
ContainsIptcValue(iptcValues, IptcTag.Caption, expectedCaption);
}
[Fact]
public void IptcProfile_SetNewValue_RespectsMaxLength()
{
// arrange
var profile = new IptcProfile();
}
[Theory]
[InlineData(IptcTag.ObjectAttribute)]
[InlineData(IptcTag.SubjectReference)]
[InlineData(IptcTag.SupplementalCategories)]
[InlineData(IptcTag.Keywords)]
[InlineData(IptcTag.LocationCode)]
[InlineData(IptcTag.LocationName)]
[InlineData(IptcTag.ReferenceService)]
[InlineData(IptcTag.ReferenceDate)]
[InlineData(IptcTag.ReferenceNumber)]
[InlineData(IptcTag.Byline)]
[InlineData(IptcTag.BylineTitle)]
[InlineData(IptcTag.Contact)]
[InlineData(IptcTag.LocalCaption)]
[InlineData(IptcTag.CaptionWriter)]
public void IptcProfile_AddRepeatable_Works(IptcTag tag)
{
// arrange
var profile = new IptcProfile();
var expectedValue1 = "test";
var expectedValue2 = "another one";
profile.SetValue(tag, expectedValue1, false);
// act
profile.SetValue(tag, expectedValue2, false);
// assert
var values = profile.Values.ToList();
Assert.Equal(2, values.Count);
ContainsIptcValue(values, tag, expectedValue1);
ContainsIptcValue(values, tag, expectedValue2);
}
[Theory]
[InlineData(IptcTag.RecordVersion)]
[InlineData(IptcTag.ObjectType)]
[InlineData(IptcTag.Name)]
[InlineData(IptcTag.EditStatus)]
[InlineData(IptcTag.EditorialUpdate)]
[InlineData(IptcTag.Urgency)]
[InlineData(IptcTag.Category)]
[InlineData(IptcTag.FixtureIdentifier)]
[InlineData(IptcTag.ReleaseDate)]
[InlineData(IptcTag.ReleaseTime)]
[InlineData(IptcTag.ExpirationDate)]
[InlineData(IptcTag.ExpirationTime)]
[InlineData(IptcTag.SpecialInstructions)]
[InlineData(IptcTag.ActionAdvised)]
[InlineData(IptcTag.CreatedDate)]
[InlineData(IptcTag.CreatedTime)]
[InlineData(IptcTag.DigitalCreationDate)]
[InlineData(IptcTag.DigitalCreationTime)]
[InlineData(IptcTag.OriginatingProgram)]
[InlineData(IptcTag.ProgramVersion)]
[InlineData(IptcTag.ObjectCycle)]
[InlineData(IptcTag.City)]
[InlineData(IptcTag.SubLocation)]
[InlineData(IptcTag.ProvinceState)]
[InlineData(IptcTag.CountryCode)]
[InlineData(IptcTag.Country)]
[InlineData(IptcTag.OriginalTransmissionReference)]
[InlineData(IptcTag.Headline)]
[InlineData(IptcTag.Credit)]
[InlineData(IptcTag.CopyrightNotice)]
[InlineData(IptcTag.Caption)]
[InlineData(IptcTag.ImageType)]
[InlineData(IptcTag.ImageOrientation)]
public void IptcProfile_AddNoneRepeatable_DoesOverrideOldValue(IptcTag tag)
{
// arrange
var profile = new IptcProfile();
var expectedValue = "another one";
profile.SetValue(tag, "test", false);
// act
profile.SetValue(tag, expectedValue, false);
// assert
var values = profile.Values.ToList();
Assert.Equal(1, values.Count);
ContainsIptcValue(values, tag, expectedValue);
}
[Fact]
public void IptcProfile_RemoveByTag_RemovesAllEntrys()
{
// arange
var profile = new IptcProfile();
profile.SetValue(IptcTag.Byline, "test");
profile.SetValue(IptcTag.Byline, "test2");
// act
var result = profile.RemoveValue(IptcTag.Byline);
// assert
Assert.True(result, "removed result should be true");
Assert.Empty(profile.Values);
}
[Fact]
public void IptcProfile_RemoveByTagAndValue_Works()
{
// arange
var profile = new IptcProfile();
profile.SetValue(IptcTag.Byline, "test");
profile.SetValue(IptcTag.Byline, "test2");
// act
var result = profile.RemoveValue(IptcTag.Byline, "test2");
// assert
Assert.True(result, "removed result should be true");
ContainsIptcValue(profile.Values.ToList(), IptcTag.Byline, "test");
}
[Fact]
public void IptcProfile_GetValue_RetrievesAllEntrys()
{
// arange
var profile = new IptcProfile();
profile.SetValue(IptcTag.Byline, "test");
profile.SetValue(IptcTag.Byline, "test2");
profile.SetValue(IptcTag.Caption, "test");
// act
List<IptcValue> result = profile.GetValues(IptcTag.Byline);
// assert
Assert.NotNull(result);
Assert.Equal(2, result.Count);
}
private static void ContainsIptcValue(List<IptcValue> values, IptcTag tag, string value)
{
Assert.True(values.Any(val => val.Tag == tag), $"Missing iptc tag {tag}");
Assert.True(values.Contains(new IptcValue(tag, System.Text.Encoding.UTF8.GetBytes(value), false)), $"expected iptc value '{value}' was not found for tag '{tag}'");
}
private static Image<Rgba32> WriteAndReadJpeg(Image<Rgba32> image)
{
using (var memStream = new MemoryStream())
{
image.SaveAsJpeg(memStream);
image.Dispose();
memStream.Position = 0;
return Image.Load<Rgba32>(memStream);
}
}
}
}

2
tests/ImageSharp.Tests/TestImages.cs

@ -162,6 +162,8 @@ namespace SixLabors.ImageSharp.Tests
public const string LowContrast = "Jpg/baseline/AsianCarvingLowContrast.jpg";
public const string Testorig12bit = "Jpg/baseline/testorig12.jpg";
public const string YcckSubsample1222 = "Jpg/baseline/ycck-subsample-1222.jpg";
public const string Iptc = "Jpg/baseline/iptc.jpg";
public const string App13WithEmptyIptc = "Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg";
public static readonly string[] All =
{

BIN
tests/Images/Input/Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
tests/Images/Input/Jpg/baseline/iptc.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Loading…
Cancel
Save