mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
22 changed files with 1855 additions and 39 deletions
@ -0,0 +1,69 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Memory |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A value-type enumerator for <see cref="MemoryGroup{T}"/> instances.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The element type.</typeparam>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public ref struct MemoryGroupEnumerator<T> |
||||
|
where T : struct |
||||
|
{ |
||||
|
private readonly IMemoryGroup<T> memoryGroup; |
||||
|
private readonly int count; |
||||
|
private int index; |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
internal MemoryGroupEnumerator(MemoryGroup<T>.Owned memoryGroup) |
||||
|
{ |
||||
|
this.memoryGroup = memoryGroup; |
||||
|
this.count = memoryGroup.Count; |
||||
|
this.index = -1; |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
internal MemoryGroupEnumerator(MemoryGroup<T>.Consumed memoryGroup) |
||||
|
{ |
||||
|
this.memoryGroup = memoryGroup; |
||||
|
this.count = memoryGroup.Count; |
||||
|
this.index = -1; |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
internal MemoryGroupEnumerator(MemoryGroupView<T> memoryGroup) |
||||
|
{ |
||||
|
this.memoryGroup = memoryGroup; |
||||
|
this.count = memoryGroup.Count; |
||||
|
this.index = -1; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="System.Collections.Generic.IEnumerator{T}.Current"/>
|
||||
|
public Memory<T> Current |
||||
|
{ |
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
get => this.memoryGroup[this.index]; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="System.Collections.IEnumerator.MoveNext"/>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool MoveNext() |
||||
|
{ |
||||
|
int index = this.index + 1; |
||||
|
|
||||
|
if (index < this.count) |
||||
|
{ |
||||
|
this.index = index; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
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