From 14312962b264b4c517c6f460f377db0ed7623035 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 14 Apr 2020 16:23:14 +0200 Subject: [PATCH] Add unit tests for reading IPTC profile --- src/ImageSharp/Metadata/ImageMetadata.cs | 1 + .../Metadata/Profiles/Exif/ExifProfile.cs | 3 + .../Metadata/Profiles/IPTC/IptcProfile.cs | 64 +++++++++---- .../Metadata/Profiles/IPTC/IptcValue.cs | 21 ++++- .../Profiles/IPTC/IptcProfileTests.cs | 88 ++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Jpg/baseline/iptc.jpg | Bin 0 -> 18611 bytes 7 files changed, 159 insertions(+), 19 deletions(-) create mode 100644 tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs create mode 100644 tests/Images/Input/Jpg/baseline/iptc.jpg diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 4fa07592e0..716e89e68d 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 11d0bd01b0..29c21d6113 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 2b0281b3b9..57704949c0 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 c23a7793e4..a5977fd274 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 0000000000..045859c361 --- /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 892568803e..d006c6682a 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 0000000000000000000000000000000000000000..de30930d6b1db7b7bc7239dfd3b4145d814afefb GIT binary patch literal 18611 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


{^xg;q*`?zA!sq*a7t};Y zC8_JqMAV7v9Cfs``$5FU-%D7JSjjJtyEhN~XGqPd2@P+(!i7OziT%sADJ2)0kCm>M zE?&Be`CE%m>RQs%<}Jzb32Sao8t?X4?RhgVWZ6B8z3m6Pta8!C`KxaugSWXnro760 zrFkRt2^l_r^*~YD5L^4A{M}w(IPN{k6d%1a_{zR-wlDe;i=UPJq@q}~WhwT1Dz+iq zV?@Y|4bi!?co*0B{&1L~&fqrvGMPJP|GE={PKC|fI6ty%#ARn$)@Qj)yY$Z{*VgBw zlWGie-SaTvZuhv&_RfaY3f{3` z$GWX?f=j>4s-8+cHV=gz*o|Mrc(i>!>+B( zp7}ENYWT*Vj#hu_x7BXLvDRGI>48TJb!Q(sk9?KNh#sFZ{$vrE?0sC3t6FeDL*7(Z zp0|?rwkoIqks~p;9*$wQ4!$>(|C8tkKmYZ0B?ouDLbh$5>SK7RZm!(4NVILQ?!gzc zJNzMB;gm`xhtR25v8bK<-sX^v-0CO6x62NqiQ6s1gNXPlgO zS$)DWW%z5a1eMvjZoIjf;&&zpXie|z|G(|51JTfIy4iw{xSAk6{A3H#wg0?*~Y zaH^&Uv6h4{;%(jEa{l_v{rSz5AtUqiH%a#mf=O6WOLJWMqo8?54%F-BILw&vEM?%+ zpC_OEcw~hm=f-5njHs5&w>POgp}SU9%u~ z8j|t#vBv}HS+lQMqsEF@zxn{$=!~67b$&yq50d$3y9O*vo-q5!N1jpFw%l+iPN>>E zIn>GP<%7~1^4szM-cwsoDSlmc^jKtrVB4E|^5fWZFS1B43oFNN4Euzd$vherfX!0P zKOMUE@~|+v&fW7?Sy<|bOPdeN`+reCs(!)#s^!_Tkl-wFtG{@aKin8Qx@0EP{?Uob znuYj6IJk-0B+w+n1)DyT2rZk+g0k>^HT&9e_EI1ysJvUuvGPaT#O${t0R51%Wy zp=}v^-?^lp8|UCt^1|JohgS3RHXn)}5axO=HDaPXIJKbi%bZEGo17jC3Tlh;`u#e2 zXZk9SM-^LHuMdB}bX3;iwMPe5&ELz>_^#MF_`DlOl@YN2hR5ZI%~=0iDOzU!y|bym m>o!KjjMOaqY^3T^|IBcYH%C>Kb7|A|j}mfJHSsswYX1*+AgjFq literal 0 HcmV?d00001