mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
24 changed files with 1795 additions and 106 deletions
Binary file not shown.
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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, |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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) |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
Loading…
Reference in new issue