Browse Source

Add support for reading IPTC metadata

pull/1574/head
Brian Popow 6 years ago
parent
commit
d79de16044
  1. 24
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
  2. 120
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  3. 6
      src/ImageSharp/Metadata/ImageMetadata.cs
  4. 204
      src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
  5. 351
      src/ImageSharp/Metadata/Profiles/IPTC/IptcTag.cs
  6. 167
      src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
  7. 9
      src/ImageSharp/Metadata/Profiles/IPTC/README.md

24
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'
};
/// <summary>
/// Gets the adobe photoshop APP13 marker which can contain IPTC meta data.
/// </summary>
public static ReadOnlySpan<byte> 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'
};
/// <summary>
/// Gets the 8BIM marker, which signals the start of a adobe specific image resource block.
/// </summary>
public static ReadOnlySpan<byte> AdobeImageResourceBlockMarker => new[]
{
(byte)'8', (byte)'B', (byte)'I', (byte)'M'
};
/// <summary>
/// Gets a IPTC Image resource ID.
/// </summary>
public static ReadOnlySpan<byte> AdobeIptcMarker => new[]
{
(byte)4, (byte)4
};
/// <summary>
/// Gets the EXIF specific markers.
/// </summary>

120
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];
/// <summary>
/// The DC Huffman tables
/// The DC Huffman tables.
/// </summary>
private HuffmanTable[] dcHuffmanTables;
@ -56,37 +57,47 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private HuffmanTable[] acHuffmanTables;
/// <summary>
/// The reset interval determined by RST markers
/// The reset interval determined by RST markers.
/// </summary>
private ushort resetInterval;
/// <summary>
/// Whether the image has an EXIF marker
/// Whether the image has an EXIF marker.
/// </summary>
private bool isExif;
/// <summary>
/// Contains exif data
/// Contains exif data.
/// </summary>
private byte[] exifData;
/// <summary>
/// Whether the image has an ICC marker
/// Whether the image has an ICC marker.
/// </summary>
private bool isIcc;
/// <summary>
/// Contains ICC data
/// Contains ICC data.
/// </summary>
private byte[] iccData;
/// <summary>
/// Contains information about the JFIF marker
/// Whether the image has a IPTC data.
/// </summary>
private bool isIptc;
/// <summary>
/// Contains IPTC data.
/// </summary>
private byte[] iptcData;
/// <summary>
/// Contains information about the JFIF marker.
/// </summary>
private JFifMarker jFif;
/// <summary>
/// Contains information about the Adobe marker
/// Contains information about the Adobe marker.
/// </summary>
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<TPixel>();
}
@ -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
}
}
/// <summary>
/// Initializes the IPTC profile.
/// </summary>
private void InitIptcProfile()
{
if (this.isIptc)
{
var profile = new IptcProfile(this.iptcData);
this.Metadata.IptcProfile = profile;
}
}
/// <summary>
/// Assigns derived metadata properties to <see cref="Metadata"/>, eg. horizontal and vertical resolution if it has a JFIF header.
/// </summary>
@ -582,6 +610,80 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="remaining">The remaining bytes in the segment block.</param>
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<byte> blockDataSpan = resourceBlockData.AsSpan();
while (blockDataSpan.Length > 10)
{
if (!ProfileResolver.IsProfile(blockDataSpan.Slice(0, 4), ProfileResolver.AdobeImageResourceBlockMarker))
{
return;
}
blockDataSpan = blockDataSpan.Slice(4);
Span<byte> 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);
}
}
}
}
/// <summary>
/// Reads the adobe image resource block name: a Pascal string (padded to make size even).
/// </summary>
/// <param name="blockDataSpan">The span holding the block resource data.</param>
/// <returns>The length of the name.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int ReadImageResourceNameLength(Span<byte> blockDataSpan)
{
byte nameLength = blockDataSpan[2];
var nameDataSize = nameLength == 0 ? 2 : nameLength;
return nameDataSize;
}
/// <summary>
/// Reads the length of a adobe image resource data block.
/// </summary>
/// <param name="blockDataSpan">The span holding the block resource data.</param>
/// <param name="resourceBlockNameLength">The length of the block name.</param>
/// <returns>The block length.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int ReadResourceDataLength(Span<byte> blockDataSpan, int resourceBlockNameLength)
{
return BinaryPrimitives.ReadInt32BigEndian(blockDataSpan.Slice(2 + resourceBlockNameLength, 4));
}
/// <summary>
/// Processes the application header containing the Adobe identifier
/// which stores image encoding information for DCT filters.

6
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
/// </summary>
public IccProfile IccProfile { get; set; }
/// <summary>
/// Gets or sets the iptc profile.
/// </summary>
public IptcProfile IptcProfile { get; set; }
/// <summary>
/// Gets the metadata value associated with the specified key.
/// </summary>

204
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
{
/// <summary>
/// Class that can be used to access an Iptc profile.
/// </summary>
/// <remarks>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
/// </remarks>
public sealed class IptcProfile
{
private Collection<IptcValue> values;
private byte[] data;
/// <summary>
/// Initializes a new instance of the <see cref="IptcProfile"/> class.
/// </summary>
public IptcProfile()
{
}
/// <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;
}
/// <summary>
/// Gets the values of this iptc profile.
/// </summary>
public IEnumerable<IptcValue> Values
{
get
{
this.Initialize();
return this.values;
}
}
/// <summary>
/// Returns the value with the specified tag.
/// </summary>
/// <param name="tag">The tag of the iptc value.</param>
/// <returns>The value with the specified tag.</returns>
public IptcValue GetValue(IptcTag tag)
{
foreach (IptcValue iptcValue in this.Values)
{
if (iptcValue.Tag == tag)
{
return iptcValue;
}
}
return null;
}
/// <summary>
/// Removes the value with the specified tag.
/// </summary>
/// <param name="tag">The tag of the iptc value.</param>
/// <returns>True when the value was fount and removed.</returns>
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;
}
/// <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 of 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>
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));
}
/// <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>
public void SetValue(IptcTag tag, string value) => this.SetValue(tag, Encoding.UTF8, value);
/// <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));
}
i += count;
}
}
}
}

351
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
{
/// <summary>
/// All iptc tags.
/// </summary>
public enum IptcTag
{
/// <summary>
/// Unknown
/// </summary>
Unknown = -1,
/// <summary>
/// Record version
/// </summary>
RecordVersion = 0,
/// <summary>
/// Object type
/// </summary>
ObjectType = 3,
/// <summary>
/// Object attribute
/// </summary>
ObjectAttribute = 4,
/// <summary>
/// Title
/// </summary>
Title = 5,
/// <summary>
/// Edit status
/// </summary>
EditStatus = 7,
/// <summary>
/// Editorial update
/// </summary>
EditorialUpdate = 8,
/// <summary>
/// Priority
/// </summary>
Priority = 10,
/// <summary>
/// Category
/// </summary>
Category = 15,
/// <summary>
/// Supplemental categories
/// </summary>
SupplementalCategories = 20,
/// <summary>
/// Fixture identifier
/// </summary>
FixtureIdentifier = 22,
/// <summary>
/// Keyword
/// </summary>
Keyword = 25,
/// <summary>
/// Location code
/// </summary>
LocationCode = 26,
/// <summary>
/// Location name
/// </summary>
LocationName = 27,
/// <summary>
/// Release date
/// </summary>
ReleaseDate = 30,
/// <summary>
/// Release time
/// </summary>
ReleaseTime = 35,
/// <summary>
/// Expiration date
/// </summary>
ExpirationDate = 37,
/// <summary>
/// Expiration time
/// </summary>
ExpirationTime = 38,
/// <summary>
/// Special instructions
/// </summary>
SpecialInstructions = 40,
/// <summary>
/// Action advised
/// </summary>
ActionAdvised = 42,
/// <summary>
/// Reference service
/// </summary>
ReferenceService = 45,
/// <summary>
/// Reference date
/// </summary>
ReferenceDate = 47,
/// <summary>
/// ReferenceNumber
/// </summary>
ReferenceNumber = 50,
/// <summary>
/// Created date
/// </summary>
CreatedDate = 55,
/// <summary>
/// Created time
/// </summary>
CreatedTime = 60,
/// <summary>
/// Digital creation date
/// </summary>
DigitalCreationDate = 62,
/// <summary>
/// Digital creation time
/// </summary>
DigitalCreationTime = 63,
/// <summary>
/// Originating program
/// </summary>
OriginatingProgram = 65,
/// <summary>
/// Program version
/// </summary>
ProgramVersion = 70,
/// <summary>
/// Object cycle
/// </summary>
ObjectCycle = 75,
/// <summary>
/// Byline
/// </summary>
Byline = 80,
/// <summary>
/// Byline title
/// </summary>
BylineTitle = 85,
/// <summary>
/// City
/// </summary>
City = 90,
/// <summary>
/// Sub location
/// </summary>
SubLocation = 92,
/// <summary>
/// Province/State
/// </summary>
ProvinceState = 95,
/// <summary>
/// Country code
/// </summary>
CountryCode = 100,
/// <summary>
/// Country
/// </summary>
Country = 101,
/// <summary>
/// Original transmission reference
/// </summary>
OriginalTransmissionReference = 103,
/// <summary>
/// Headline
/// </summary>
Headline = 105,
/// <summary>
/// Credit
/// </summary>
Credit = 110,
/// <summary>
/// Source
/// </summary>
Source = 115,
/// <summary>
/// Copyright notice
/// </summary>
CopyrightNotice = 116,
/// <summary>
/// Contact
/// </summary>
Contact = 118,
/// <summary>
/// Caption
/// </summary>
Caption = 120,
/// <summary>
/// Local caption
/// </summary>
LocalCaption = 121,
/// <summary>
/// Caption writer
/// </summary>
CaptionWriter = 122,
/// <summary>
/// Image type
/// </summary>
ImageType = 130,
/// <summary>
/// Image orientation
/// </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,
}
}

167
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
{
/// <summary>
/// A value of the iptc profile.
/// </summary>
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;
}
/// <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 the value.
/// </summary>
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);
}
}
}
/// <summary>
/// Gets the length of the value.
/// </summary>
public int Length => this.data.Length;
/// <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;
}
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;
}
/// <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="enc">The encoding to use.</param>
/// <returns>A string that represents the current value with the specified encoding.</returns>
public string ToString(Encoding enc)
{
Guard.NotNull(enc, nameof(enc));
return enc.GetString(this.data);
}
}
}

9
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)
Loading…
Cancel
Save