diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs index f4b4f1043..b86f6dff2 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs @@ -37,6 +37,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc this.Initialize(); } + /// + /// Initializes a new instance of the class + /// by making a copy from another IPTC profile. + /// + /// The other IPTC profile, from which the clone should be made from. private IptcProfile(IptcProfile other) { Guard.NotNull(other, nameof(other)); @@ -85,16 +90,16 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc /// The values found with the specified tag. public List GetValues(IptcTag tag) { - var values = new List(); + var iptcValues = new List(); foreach (IptcValue iptcValue in this.Values) { if (iptcValue.Tag == tag) { - values.Add(iptcValue); + iptcValues.Add(iptcValue); } } - return values; + return iptcValues; } /// @@ -157,21 +162,26 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc } /// - /// Sets the value of the specified tag. + /// Sets the value for the specified tag. /// /// The tag of the iptc value. /// The encoding to use when storing the bytes. /// The value. - public void SetValue(IptcTag tag, Encoding encoding, string value) + /// + /// Indicates if length restrictions from the specification should be followed strictly. + /// Defaults to true. + /// + public void SetValue(IptcTag tag, Encoding encoding, string value, bool strict = true) { Guard.NotNull(encoding, nameof(encoding)); - if (!this.IsRepeatable(tag)) + if (!tag.IsRepeatable()) { foreach (IptcValue iptcValue in this.Values) { if (iptcValue.Tag == tag) { + iptcValue.Strict = strict; iptcValue.Encoding = encoding; iptcValue.Value = value; return; @@ -179,7 +189,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc } } - this.values.Add(new IptcValue(tag, encoding, value)); + this.values.Add(new IptcValue(tag, encoding, value, strict)); } /// @@ -187,7 +197,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc /// /// The tag of the iptc value. /// The value. - public void SetValue(IptcTag tag, string value) => this.SetValue(tag, Encoding.UTF8, value); + /// + /// Indicates if length restrictions from the specification should be followed strictly. + /// Defaults to true. + /// + public void SetValue(IptcTag tag, string value, bool strict = true) => this.SetValue(tag, Encoding.UTF8, value, strict); /// /// Updates the data of the profile. @@ -251,56 +265,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc if ((count > 0) && (i + count <= this.Data.Length)) { Buffer.BlockCopy(this.Data, i, iptcData, 0, count); - this.values.Add(new IptcValue(tag, iptcData)); + this.values.Add(new IptcValue(tag, iptcData, false)); } i += count; } } - - private bool IsRepeatable(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; - } - } } } diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs index cd0b62072..135c41e51 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs @@ -4,7 +4,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc { /// - /// All iptc tags. + /// All iptc tags relevant for images. /// public enum IptcTag { @@ -14,222 +14,245 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc Unknown = -1, /// - /// Record version, not repeatable. + /// Record version identifying the version of the Information Interchange Model. + /// Not repeatable. Max length is 2. /// RecordVersion = 0, /// - /// Object type, not repeatable. + /// Object type, not repeatable. Max Length is 67. /// ObjectType = 3, /// - /// Object attribute. + /// Object attribute. Max length is 68. /// ObjectAttribute = 4, /// - /// Object Name, not repeatable. + /// Object Name, not repeatable. Max length is 64. /// Name = 5, /// - /// Edit status, not repeatable. + /// Edit status, not repeatable. Max length is 64. /// EditStatus = 7, /// - /// Editorial update, not repeatable. + /// Editorial update, not repeatable. Max length is 2. /// EditorialUpdate = 8, /// - /// Urgency, not repeatable. + /// Urgency, not repeatable. Max length is 2. /// Urgency = 10, /// - /// Subject Reference. + /// Subject Reference. Max length is 236. /// SubjectReference = 12, /// - /// Category, not repeatable. + /// Category, not repeatable. Max length is 3. /// Category = 15, /// - /// Supplemental categories. + /// Supplemental categories. Max length is 32. /// SupplementalCategories = 20, /// - /// Fixture identifier, not repeatable. + /// Fixture identifier, not repeatable. Max length is 32. /// FixtureIdentifier = 22, /// - /// Keywords. + /// Keywords. Max length is 64. /// Keywords = 25, /// - /// Location code. + /// Location code. Max length is 3. /// LocationCode = 26, /// - /// Location name. + /// Location name. Max length is 64. /// LocationName = 27, /// - /// Release date, not repeatable. + /// Release date. Format should be CCYYMMDD, + /// e.g. "19890317" indicates data for release on 17 March 1989. + /// Not repeatable, max length is 8. /// ReleaseDate = 30, /// - /// Release time, not repeatable. + /// Release time. Format should be HHMMSS±HHMM, + /// e.g. "090000-0500" indicates object for use after 0900 in + /// New York (five hours behind UTC) + /// Not repeatable, max length is 11. /// ReleaseTime = 35, /// - /// Expiration date, not repeatable. + /// Expiration date. Format should be CCYYMMDD, + /// e.g. "19890317" indicates data for release on 17 March 1989. + /// Not repeatable, max length is 8. /// ExpirationDate = 37, /// - /// Expiration time, not repeatable. + /// Expiration time. Format should be HHMMSS±HHMM, + /// e.g. "090000-0500" indicates object for use after 0900 in + /// New York (five hours behind UTC) + /// Not repeatable, max length is 11. /// ExpirationTime = 38, /// - /// Special instructions, not repeatable. + /// Special instructions, not repeatable. Max length is 256. /// SpecialInstructions = 40, /// - /// Action advised, not repeatable. + /// Action advised, not repeatable. Max length is 2. /// ActionAdvised = 42, /// - /// Reference service. + /// Reference service. Max length is 10. /// ReferenceService = 45, /// - /// Reference date. + /// Reference date. Format should be CCYYMMDD, + /// e.g. "19890317" indicates data for release on 17 March 1989. + /// Not repeatable, max length is 8. /// ReferenceDate = 47, /// - /// ReferenceNumber. + /// ReferenceNumber. Max length is 8. /// ReferenceNumber = 50, /// - /// Created date, not repeatable. + /// Created date. Format should be CCYYMMDD, + /// e.g. "19890317" indicates data for release on 17 March 1989. + /// Not repeatable, max length is 8. /// CreatedDate = 55, /// - /// Created time, not repeatable. + /// Created time. Format should be HHMMSS±HHMM, + /// e.g. "090000-0500" indicates object for use after 0900 in + /// New York (five hours behind UTC) + /// Not repeatable, max length is 11. /// CreatedTime = 60, /// - /// Digital creation date, not repeatable. + /// Digital creation date. Format should be CCYYMMDD, + /// e.g. "19890317" indicates data for release on 17 March 1989. + /// Not repeatable, max length is 8. /// DigitalCreationDate = 62, /// - /// Digital creation time, not repeatable. + /// Digital creation time. Format should be HHMMSS±HHMM, + /// e.g. "090000-0500" indicates object for use after 0900 in + /// New York (five hours behind UTC) + /// Not repeatable, max length is 11. /// DigitalCreationTime = 63, /// - /// Originating program, not repeatable. + /// Originating program, not repeatable. Max length is 32. /// OriginatingProgram = 65, /// - /// Program version, not repeatable. + /// Program version, not repeatable. Max length is 10. /// ProgramVersion = 70, /// - /// Object cycle, not repeatable. + /// Object cycle, not repeatable. Max length is 1. /// ObjectCycle = 75, /// - /// Byline. + /// Byline. Max length is 32. /// Byline = 80, /// - /// Byline title. + /// Byline title. Max length is 32. /// BylineTitle = 85, /// - /// City, not repeatable. + /// City, not repeatable. Max length is 32. /// City = 90, /// - /// Sub location, not repeatable. + /// Sub location, not repeatable. Max length is 32. /// SubLocation = 92, /// - /// Province/State, not repeatable. + /// Province/State, not repeatable. Max length is 32. /// ProvinceState = 95, /// - /// Country code, not repeatable. + /// Country code, not repeatable. Max length is 3. /// CountryCode = 100, /// - /// Country, not repeatable. + /// Country, not repeatable. Max length is 64. /// Country = 101, /// - /// Original transmission reference, not repeatable. + /// Original transmission reference, not repeatable. Max length is 32. /// OriginalTransmissionReference = 103, /// - /// Headline, not repeatable. + /// Headline, not repeatable. Max length is 256. /// Headline = 105, /// - /// Credit, not repeatable. + /// Credit, not repeatable. Max length is 32. /// Credit = 110, /// - /// Source, not repeatable. + /// Source, not repeatable. Max length is 32. /// Source = 115, /// - /// Copyright notice, not repeatable. + /// Copyright notice, not repeatable. Max length is 128. /// CopyrightNotice = 116, /// - /// Contact. + /// Contact. Max length 128. /// Contact = 118, /// - /// Caption, not repeatable. + /// Caption, not repeatable. Max length is 2000. /// Caption = 120, @@ -239,17 +262,17 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc LocalCaption = 121, /// - /// Caption writer. + /// Caption writer. Max length is 32. /// CaptionWriter = 122, /// - /// Image type, not repeatable. + /// Image type, not repeatable. Max length is 2. /// ImageType = 130, /// - /// Image orientation, not repeatable. + /// Image orientation, not repeatable. Max length is 1. /// ImageOrientation = 131, diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs new file mode 100644 index 000000000..88d463767 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc +{ + /// + /// Extension methods for IPTC tags. + /// + public static class IptcTagExtensions + { + /// + /// Maximum length of the IPTC value with the given tag according to the specification. + /// + /// The tag to check the max length for. + /// The maximum length. + 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 + }; + } + + /// + /// Determines if the given tag can be repeated according to the specification. + /// + /// The tag to check. + /// True, if the tag can occur multiple times. + 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; + } + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs index a5977fd27..2c2cf5995 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs @@ -28,24 +28,35 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc } this.Tag = other.Tag; + this.Strict = other.Strict; } - internal IptcValue(IptcTag tag, byte[] value) + 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) + 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; + } + /// /// Gets or sets the encoding to use for the Value. /// @@ -66,6 +77,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc /// public IptcTag Tag { get; } + /// + /// Gets or sets a value indicating whether to be enforce value length restrictions according + /// to the specification. + /// + public bool Strict { get; set; } + /// /// Gets or sets the value. /// @@ -76,11 +93,23 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc { if (string.IsNullOrEmpty(value)) { - this.data = new byte[0]; + this.data = Array.Empty(); } else { - this.data = this.encoding.GetBytes(value); + int maxLength = this.Tag.MaxLength(); + byte[] valueBytes; + if (this.Strict && value.Length > maxLength) + { + var cappedValue = value.Substring(0, maxLength); + valueBytes = this.encoding.GetBytes(cappedValue); + } + else + { + valueBytes = this.encoding.GetBytes(value); + } + + this.data = valueBytes; } } } diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/README.md b/src/ImageSharp/Metadata/Profiles/IPTC/README.md index 0b0efc967..1217ca0c7 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/README.md +++ b/src/ImageSharp/Metadata/Profiles/IPTC/README.md @@ -1,9 +1,11 @@ IPTC source code is from [Magick.NET](https://github.com/dlemstra/Magick.NET) -Information about IPTC can be found here in the folowing sources: +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) \ No newline at end of file +- [Adobe File Formats Specification](http://oldschoolprg.x10.mx/downloads/ps6ffspecsv2.pdf) + +- [Tag Overview](https://exiftool.org/TagNames/IPTC.html) \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs index 9f8f8088d..321c7fe5c 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/IPTC/IptcProfileTests.cs @@ -1,6 +1,7 @@ // 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; @@ -15,6 +16,31 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC { private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false }; + public static IEnumerable 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] [WithFile(TestImages.Jpeg.Baseline.Iptc, PixelTypes.Rgba32)] public void ReadIptcMetadata_Works(TestImageProvider provider) @@ -91,16 +117,17 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC // 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); + 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"); + var iptcValue = new IptcValue(IptcTag.Caption, System.Text.Encoding.UTF8, "test", true); // act IptcValue clone = iptcValue.DeepClone(); @@ -132,6 +159,13 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC ContainsIptcValue(iptcValues, IptcTag.Caption, expectedCaption); } + [Fact] + public void IptcProfile_SetNewValue_RespectsMaxLength() + { + // arrange + var profile = new IptcProfile(); + } + [Theory] [InlineData(IptcTag.ObjectAttribute)] [InlineData(IptcTag.SubjectReference)] @@ -153,10 +187,10 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC var profile = new IptcProfile(); var expectedValue1 = "test"; var expectedValue2 = "another one"; - profile.SetValue(tag, expectedValue1); + profile.SetValue(tag, expectedValue1, false); // act - profile.SetValue(tag, expectedValue2); + profile.SetValue(tag, expectedValue2, false); // assert var values = profile.Values.ToList(); @@ -204,10 +238,10 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC // arrange var profile = new IptcProfile(); var expectedValue = "another one"; - profile.SetValue(tag, "test"); + profile.SetValue(tag, "test", false); // act - profile.SetValue(tag, expectedValue); + profile.SetValue(tag, expectedValue, false); // assert var values = profile.Values.ToList(); @@ -244,7 +278,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC // assert Assert.True(result, "removed result should be true"); - ContainsIptcValue(profile.Values, IptcTag.Byline, "test"); + ContainsIptcValue(profile.Values.ToList(), IptcTag.Byline, "test"); } [Fact] @@ -264,10 +298,10 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.IPTC Assert.Equal(2, result.Count); } - private static void ContainsIptcValue(IEnumerable values, IptcTag tag, string value) + private static void ContainsIptcValue(List 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}'"); + 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 WriteAndReadJpeg(Image image)