diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 4fa07592e..716e89e68 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -66,6 +66,7 @@ namespace SixLabors.ImageSharp.Metadata this.ExifProfile = other.ExifProfile?.DeepClone(); this.IccProfile = other.IccProfile?.DeepClone(); + this.IptcProfile = other.IptcProfile?.DeepClone(); } /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index 11d0bd01b..29c21d611 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -57,8 +57,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// by making a copy from another EXIF profile. /// /// The other EXIF profile, where the clone should be made from. + /// is null.> private ExifProfile(ExifProfile other) { + Guard.NotNull(other, nameof(other)); + this.Parts = other.Parts; this.thumbnailLength = other.thumbnailLength; this.thumbnailOffset = other.thumbnailOffset; diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs index 2b0281b3b..57704949c 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs @@ -15,16 +15,15 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc /// This source code is from the Magick.Net project: /// https://github.com/dlemstra/Magick.NET/tree/master/src/Magick.NET/Shared/Profiles/Iptc/IptcProfile.cs /// - public sealed class IptcProfile + public sealed class IptcProfile : IDeepCloneable { private Collection values; - private byte[] data; - /// /// Initializes a new instance of the class. /// public IptcProfile() + : this((byte[])null) { } @@ -34,9 +33,35 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc /// The byte array to read the iptc profile from. public IptcProfile(byte[] data) { - this.data = data; + this.Data = data; + } + + private IptcProfile(IptcProfile other) + { + Guard.NotNull(other, nameof(other)); + + if (other.values != null) + { + this.values = new Collection(); + + 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); + } } + /// + /// Gets the byte data of the IPTC profile. + /// + public byte[] Data { get; private set; } + /// /// Gets the values of this iptc profile. /// @@ -49,6 +74,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc } } + /// + public IptcProfile DeepClone() => new IptcProfile(this); + /// /// Returns the value with the specified tag. /// @@ -143,19 +171,19 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc length += value.Length + 5; } - this.data = new byte[length]; + 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; + 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); + Buffer.BlockCopy(value.ToByteArray(), 0, this.Data, i, value.Length); i += value.Length; } } @@ -170,30 +198,30 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc this.values = new Collection(); - if (this.data == null || this.data[0] != 0x1c) + if (this.Data == null || this.Data[0] != 0x1c) { return; } int i = 0; - while (i + 4 < this.data.Length) + while (i + 4 < this.Data.Length) { - if (this.data[i++] != 28) + if (this.Data[i++] != 28) { continue; } i++; - var tag = (IptcTag)this.data[i++]; + var tag = (IptcTag)this.Data[i++]; - int count = BinaryPrimitives.ReadInt16BigEndian(this.data.AsSpan(i, 2)); + int count = BinaryPrimitives.ReadInt16BigEndian(this.Data.AsSpan(i, 2)); i += 2; var iptcData = new byte[count]; - if ((count > 0) && (i + count <= this.data.Length)) + if ((count > 0) && (i + count <= this.Data.Length)) { - Buffer.BlockCopy(this.data, i, iptcData, 0, count); + Buffer.BlockCopy(this.Data, i, iptcData, 0, count); this.values.Add(new IptcValue(tag, iptcData)); } diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs index c23a7793e..a5977fd27 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs @@ -9,11 +9,27 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc /// /// A value of the iptc profile. /// - public sealed class IptcValue + public sealed class IptcValue : IDeepCloneable { private byte[] data; 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; + } + internal IptcValue(IptcTag tag, byte[] value) { Guard.NotNull(value, nameof(value)); @@ -74,6 +90,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc /// public int Length => this.data.Length; + /// + public IptcValue DeepClone() => new IptcValue(this); + /// /// Determines whether the specified object is equal to the current . /// diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs new file mode 100644 index 000000000..045859c36 --- /dev/null +++ b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +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 }; + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.Iptc, PixelTypes.Rgba32)] + public void ReadIptcProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(JpegDecoder)) + { + Assert.NotNull(image.Metadata.IptcProfile); + IEnumerable iptcValues = image.Metadata.IptcProfile.Values; + 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.Title, "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.Priority, "1"); + ContainsIptcValue(iptcValues, IptcTag.Keyword, "keywords"); + ContainsIptcValue(iptcValues, IptcTag.CopyrightNotice, "copyright"); + } + } + + [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()); + ContainsIptcValue(clone.Values, IptcTag.CaptionWriter, captionWriter); + ContainsIptcValue(clone.Values, IptcTag.Caption, "changed"); + ContainsIptcValue(profile.Values, IptcTag.Caption, caption); + } + + [Fact] + public void IptcValue_CloneIsDeep() + { + // arrange + var iptcValue = new IptcValue(IptcTag.Caption, System.Text.Encoding.UTF8, "test"); + + // act + IptcValue clone = iptcValue.DeepClone(); + clone.Value = "changed"; + + // assert + Assert.NotEqual(iptcValue.Value, clone.Value); + } + + private static void ContainsIptcValue(IEnumerable 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))), $"expected iptc value '{value}' was not found for tag '{tag}'"); + } + } +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 892568803..d006c6682 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -162,6 +162,7 @@ namespace SixLabors.ImageSharp.Tests public const string LowContrast = "Jpg/baseline/AsianCarvingLowContrast.jpg"; public const string Testorig12bit = "Jpg/baseline/testorig12.jpg"; public const string YcckSubsample1222 = "Jpg/baseline/ycck-subsample-1222.jpg"; + public const string Iptc = "Jpg/baseline/iptc.jpg"; public static readonly string[] All = { diff --git a/tests/Images/Input/Jpg/baseline/iptc.jpg b/tests/Images/Input/Jpg/baseline/iptc.jpg new file mode 100644 index 000000000..adb12621f --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/iptc.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c8a0747d9282bfd7e8e7f4a0119c53c702bf600384b786ef9b5263457f38ada +size 18611