diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs index 87b486ea6..325d7780a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs @@ -28,6 +28,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder (byte)'I', (byte)'L', (byte)'E', (byte)'\0' }; + /// + /// Gets the adobe photoshop APP13 marker which can contain IPTC meta data. + /// + public static ReadOnlySpan AdobePhotoshopApp13Marker => new[] + { + (byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', (byte)'\0' + }; + + /// + /// Gets the 8BIM marker, which signals the start of a adobe specific image resource block. + /// + public static ReadOnlySpan AdobeImageResourceBlockMarker => new[] + { + (byte)'8', (byte)'B', (byte)'I', (byte)'M' + }; + + /// + /// Gets a IPTC Image resource ID. + /// + public static ReadOnlySpan AdobeIptcMarker => new[] + { + (byte)4, (byte)4 + }; + /// /// Gets the EXIF specific markers. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 951fec1d4..46f0e694e 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private readonly byte[] markerBuffer = new byte[2]; /// - /// The DC Huffman tables + /// The DC Huffman tables. /// private HuffmanTable[] dcHuffmanTables; @@ -56,37 +57,47 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private HuffmanTable[] acHuffmanTables; /// - /// The reset interval determined by RST markers + /// The reset interval determined by RST markers. /// private ushort resetInterval; /// - /// Whether the image has an EXIF marker + /// Whether the image has an EXIF marker. /// private bool isExif; /// - /// Contains exif data + /// Contains exif data. /// private byte[] exifData; /// - /// Whether the image has an ICC marker + /// Whether the image has an ICC marker. /// private bool isIcc; /// - /// Contains ICC data + /// Contains ICC data. /// private byte[] iccData; /// - /// Contains information about the JFIF marker + /// Whether the image has a IPTC data. + /// + private bool isIptc; + + /// + /// Contains IPTC data. + /// + private byte[] iptcData; + + /// + /// Contains information about the JFIF marker. /// private JFifMarker jFif; /// - /// Contains information about the Adobe marker + /// Contains information about the Adobe marker. /// private AdobeMarker adobe; @@ -213,6 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.ParseStream(stream); this.InitExifProfile(); this.InitIccProfile(); + this.InitIptcProfile(); this.InitDerivedMetadataProperties(); return this.PostProcessIntoImage(); } @@ -226,6 +238,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.ParseStream(stream, true); this.InitExifProfile(); this.InitIccProfile(); + this.InitIptcProfile(); this.InitDerivedMetadataProperties(); return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.Metadata); @@ -344,10 +357,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.APP10: case JpegConstants.Markers.APP11: case JpegConstants.Markers.APP12: - case JpegConstants.Markers.APP13: this.InputStream.Skip(remaining); break; + case JpegConstants.Markers.APP13: + this.ProcessApp13Marker(remaining); + break; + case JpegConstants.Markers.APP14: this.ProcessApp14Marker(remaining); break; @@ -437,6 +453,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } + /// + /// Initializes the IPTC profile. + /// + private void InitIptcProfile() + { + if (this.isIptc) + { + var profile = new IptcProfile(this.iptcData); + this.Metadata.IptcProfile = profile; + } + } + /// /// Assigns derived metadata properties to , eg. horizontal and vertical resolution if it has a JFIF header. /// @@ -582,6 +610,80 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } + /// + /// Processes a App13 marker, which contains IPTC data stored with Adobe Photoshop. + /// The content of an APP13 segment is formed by an identifier string followed by a sequence of resource data blocks. + /// + /// The remaining bytes in the segment block. + private void ProcessApp13Marker(int remaining) + { + if (remaining < ProfileResolver.AdobePhotoshopApp13Marker.Length || this.IgnoreMetadata) + { + this.InputStream.Skip(remaining); + return; + } + + var identifier = new byte[ProfileResolver.AdobePhotoshopApp13Marker.Length]; + this.InputStream.Read(identifier, 0, identifier.Length); + remaining -= identifier.Length; + if (ProfileResolver.IsProfile(identifier, ProfileResolver.AdobePhotoshopApp13Marker)) + { + var resourceBlockData = new byte[remaining]; + this.InputStream.Read(resourceBlockData, 0, remaining); + Span blockDataSpan = resourceBlockData.AsSpan(); + + while (blockDataSpan.Length > 10) + { + if (!ProfileResolver.IsProfile(blockDataSpan.Slice(0, 4), ProfileResolver.AdobeImageResourceBlockMarker)) + { + return; + } + + blockDataSpan = blockDataSpan.Slice(4); + Span imageResourceBlockId = blockDataSpan.Slice(0, 2); + if (ProfileResolver.IsProfile(imageResourceBlockId, ProfileResolver.AdobeIptcMarker)) + { + var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); + var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); + this.isIptc = true; + this.iptcData = blockDataSpan.Slice(2 + resourceBlockNameLength + 4, resourceDataSize).ToArray(); + break; + } + else + { + var resourceBlockNameLength = ReadImageResourceNameLength(blockDataSpan); + var resourceDataSize = ReadResourceDataLength(blockDataSpan, resourceBlockNameLength); + blockDataSpan = blockDataSpan.Slice(2 + resourceBlockNameLength + 4 + resourceDataSize); + } + } + } + } + + /// + /// Reads the adobe image resource block name: a Pascal string (padded to make size even). + /// + /// The span holding the block resource data. + /// The length of the name. + [MethodImpl(InliningOptions.ShortMethod)] + private static int ReadImageResourceNameLength(Span blockDataSpan) + { + byte nameLength = blockDataSpan[2]; + var nameDataSize = nameLength == 0 ? 2 : nameLength; + return nameDataSize; + } + + /// + /// Reads the length of a adobe image resource data block. + /// + /// The span holding the block resource data. + /// The length of the block name. + /// The block length. + [MethodImpl(InliningOptions.ShortMethod)] + private static int ReadResourceDataLength(Span blockDataSpan, int resourceBlockNameLength) + { + return BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4)); + } + /// /// Processes the application header containing the Adobe identifier /// which stores image encoding information for DCT filters. diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index b3751bfbd..4fa07592e 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; namespace SixLabors.ImageSharp.Metadata { @@ -122,6 +123,11 @@ namespace SixLabors.ImageSharp.Metadata /// public IccProfile IccProfile { get; set; } + /// + /// Gets or sets the iptc profile. + /// + public IptcProfile IptcProfile { get; set; } + /// /// Gets the metadata value associated with the specified key. /// diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs new file mode 100644 index 000000000..2b0281b3b --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs @@ -0,0 +1,204 @@ +// 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 +{ + /// + /// Class that can be used to access an Iptc profile. + /// + /// 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 + { + private Collection values; + + private byte[] data; + + /// + /// Initializes a new instance of the class. + /// + public IptcProfile() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The byte array to read the iptc profile from. + public IptcProfile(byte[] data) + { + this.data = data; + } + + /// + /// Gets the values of this iptc profile. + /// + public IEnumerable Values + { + get + { + this.Initialize(); + return this.values; + } + } + + /// + /// Returns the value with the specified tag. + /// + /// The tag of the iptc value. + /// The value with the specified tag. + public IptcValue GetValue(IptcTag tag) + { + foreach (IptcValue iptcValue in this.Values) + { + if (iptcValue.Tag == tag) + { + return iptcValue; + } + } + + return null; + } + + /// + /// Removes the value with the specified tag. + /// + /// The tag of the iptc value. + /// True when the value was fount and removed. + public bool RemoveValue(IptcTag tag) + { + this.Initialize(); + + for (int i = 0; i < this.values.Count; i++) + { + if (this.values[i].Tag == tag) + { + this.values.RemoveAt(i); + return true; + } + } + + return false; + } + + /// + /// Changes the encoding for all the values. + /// + /// The encoding to use when storing the bytes. + public void SetEncoding(Encoding encoding) + { + Guard.NotNull(encoding, nameof(encoding)); + + foreach (IptcValue value in this.Values) + { + value.Encoding = encoding; + } + } + + /// + /// Sets the value of 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) + { + Guard.NotNull(encoding, nameof(encoding)); + + foreach (IptcValue iptcValue in this.Values) + { + if (iptcValue.Tag == tag) + { + iptcValue.Encoding = encoding; + iptcValue.Value = value; + return; + } + } + + this.values.Add(new IptcValue(tag, encoding, value)); + } + + /// + /// Sets the value of the specified tag. + /// + /// The tag of the iptc value. + /// The value. + public void SetValue(IptcTag tag, string value) => this.SetValue(tag, Encoding.UTF8, value); + + /// + /// Updates the data of the profile. + /// + 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(); + + 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)); + } + + i += count; + } + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs new file mode 100644 index 000000000..3e6da0e09 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs @@ -0,0 +1,351 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc +{ + /// + /// All iptc tags. + /// + public enum IptcTag + { + /// + /// Unknown + /// + Unknown = -1, + + /// + /// Record version + /// + RecordVersion = 0, + + /// + /// Object type + /// + ObjectType = 3, + + /// + /// Object attribute + /// + ObjectAttribute = 4, + + /// + /// Title + /// + Title = 5, + + /// + /// Edit status + /// + EditStatus = 7, + + /// + /// Editorial update + /// + EditorialUpdate = 8, + + /// + /// Priority + /// + Priority = 10, + + /// + /// Category + /// + Category = 15, + + /// + /// Supplemental categories + /// + SupplementalCategories = 20, + + /// + /// Fixture identifier + /// + FixtureIdentifier = 22, + + /// + /// Keyword + /// + Keyword = 25, + + /// + /// Location code + /// + LocationCode = 26, + + /// + /// Location name + /// + LocationName = 27, + + /// + /// Release date + /// + ReleaseDate = 30, + + /// + /// Release time + /// + ReleaseTime = 35, + + /// + /// Expiration date + /// + ExpirationDate = 37, + + /// + /// Expiration time + /// + ExpirationTime = 38, + + /// + /// Special instructions + /// + SpecialInstructions = 40, + + /// + /// Action advised + /// + ActionAdvised = 42, + + /// + /// Reference service + /// + ReferenceService = 45, + + /// + /// Reference date + /// + ReferenceDate = 47, + + /// + /// ReferenceNumber + /// + ReferenceNumber = 50, + + /// + /// Created date + /// + CreatedDate = 55, + + /// + /// Created time + /// + CreatedTime = 60, + + /// + /// Digital creation date + /// + DigitalCreationDate = 62, + + /// + /// Digital creation time + /// + DigitalCreationTime = 63, + + /// + /// Originating program + /// + OriginatingProgram = 65, + + /// + /// Program version + /// + ProgramVersion = 70, + + /// + /// Object cycle + /// + ObjectCycle = 75, + + /// + /// Byline + /// + Byline = 80, + + /// + /// Byline title + /// + BylineTitle = 85, + + /// + /// City + /// + City = 90, + + /// + /// Sub location + /// + SubLocation = 92, + + /// + /// Province/State + /// + ProvinceState = 95, + + /// + /// Country code + /// + CountryCode = 100, + + /// + /// Country + /// + Country = 101, + + /// + /// Original transmission reference + /// + OriginalTransmissionReference = 103, + + /// + /// Headline + /// + Headline = 105, + + /// + /// Credit + /// + Credit = 110, + + /// + /// Source + /// + Source = 115, + + /// + /// Copyright notice + /// + CopyrightNotice = 116, + + /// + /// Contact + /// + Contact = 118, + + /// + /// Caption + /// + Caption = 120, + + /// + /// Local caption + /// + LocalCaption = 121, + + /// + /// Caption writer + /// + CaptionWriter = 122, + + /// + /// Image type + /// + ImageType = 130, + + /// + /// Image orientation + /// + ImageOrientation = 131, + + /// + /// Custom field 1 + /// + CustomField1 = 200, + + /// + /// Custom field 2 + /// + CustomField2 = 201, + + /// + /// Custom field 3 + /// + CustomField3 = 202, + + /// + /// Custom field 4 + /// + CustomField4 = 203, + + /// + /// Custom field 5 + /// + CustomField5 = 204, + + /// + /// Custom field 6 + /// + CustomField6 = 205, + + /// + /// Custom field 7 + /// + CustomField7 = 206, + + /// + /// Custom field 8 + /// + CustomField8 = 207, + + /// + /// Custom field 9 + /// + CustomField9 = 208, + + /// + /// Custom field 10 + /// + CustomField10 = 209, + + /// + /// Custom field 11 + /// + CustomField11 = 210, + + /// + /// Custom field 12 + /// + CustomField12 = 211, + + /// + /// Custom field 13 + /// + CustomField13 = 212, + + /// + /// Custom field 14 + /// + CustomField14 = 213, + + /// + /// Custom field 15 + /// + CustomField15 = 214, + + /// + /// Custom field 16 + /// + CustomField16 = 215, + + /// + /// Custom field 17 + /// + CustomField17 = 216, + + /// + /// Custom field 18 + /// + CustomField18 = 217, + + /// + /// Custom field 19 + /// + CustomField19 = 218, + + /// + /// Custom field 20 + /// + CustomField20 = 219, + } +} diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs new file mode 100644 index 000000000..c23a7793e --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs @@ -0,0 +1,167 @@ +// 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 +{ + /// + /// A value of the iptc profile. + /// + public sealed class IptcValue + { + private byte[] data; + private Encoding encoding; + + internal IptcValue(IptcTag tag, byte[] value) + { + Guard.NotNull(value, nameof(value)); + + this.Tag = tag; + this.data = value; + this.encoding = Encoding.UTF8; + } + + internal IptcValue(IptcTag tag, Encoding encoding, string value) + { + this.Tag = tag; + this.encoding = encoding; + this.Value = value; + } + + /// + /// Gets or sets the encoding to use for the Value. + /// + public Encoding Encoding + { + get => this.encoding; + set + { + if (value != null) + { + this.encoding = value; + } + } + } + + /// + /// Gets the tag of the iptc value. + /// + public IptcTag Tag { get; } + + /// + /// Gets or sets the value. + /// + public string Value + { + get => this.encoding.GetString(this.data); + set + { + if (string.IsNullOrEmpty(value)) + { + this.data = new byte[0]; + } + else + { + this.data = this.encoding.GetBytes(value); + } + } + } + + /// + /// Gets the length of the value. + /// + public int Length => this.data.Length; + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare this with. + /// True when the specified object is equal to the current . + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + return this.Equals(obj as IptcValue); + } + + /// + /// Determines whether the specified iptc value is equal to the current . + /// + /// The iptc value to compare this with. + /// True when the specified iptc value is equal to the current . + public bool Equals(IptcValue other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (this.Tag != other.Tag) + { + return false; + } + + byte[] data = other.ToByteArray(); + + if (this.data.Length != data.Length) + { + return false; + } + + for (int i = 0; i < this.data.Length; i++) + { + if (this.data[i] != data[i]) + { + return false; + } + } + + return true; + } + + /// + /// Serves as a hash of this type. + /// + /// A hash code for the current instance. + public override int GetHashCode() => HashCode.Combine(this.data, this.Tag); + + /// + /// Converts this instance to a byte array. + /// + /// A array. + public byte[] ToByteArray() + { + var result = new byte[this.data.Length]; + this.data.CopyTo(result, 0); + return result; + } + + /// + /// Returns a string that represents the current value. + /// + /// A string that represents the current value. + public override string ToString() => this.Value; + + /// + /// Returns a string that represents the current value with the specified encoding. + /// + /// The encoding to use. + /// A string that represents the current value with the specified encoding. + public string ToString(Encoding enc) + { + Guard.NotNull(enc, nameof(enc)); + + return enc.GetString(this.data); + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/README.md b/src/ImageSharp/Metadata/Profiles/IPTC/README.md new file mode 100644 index 000000000..0b0efc967 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/IPTC/README.md @@ -0,0 +1,9 @@ +IPTC source code is from [Magick.NET](https://github.com/dlemstra/Magick.NET) + +Information about IPTC can be found here in the folowing 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