From 3f75c330b1e16b63b441e1d2e6ac6e9af6efb426 Mon Sep 17 00:00:00 2001 From: dirk Date: Fri, 12 Aug 2016 14:29:50 +0200 Subject: [PATCH 1/4] Added Guard for true and false. Former-commit-id: 0036dcc3316425aa6709918eb22f08d6a95c76d0 Former-commit-id: e928511040809e280a1e638a4341403d15f1ed32 Former-commit-id: cfe1b6808c914762286fcd23a95dca444cdaa954 --- .../Common/Helpers/Guard.cs | 58 +++++++++++++++++++ .../Helpers/GuardTests.cs | 38 ++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/ImageProcessorCore/Common/Helpers/Guard.cs b/src/ImageProcessorCore/Common/Helpers/Guard.cs index 96c7023d4..d11addf2a 100644 --- a/src/ImageProcessorCore/Common/Helpers/Guard.cs +++ b/src/ImageProcessorCore/Common/Helpers/Guard.cs @@ -188,5 +188,63 @@ namespace ImageProcessorCore $"Value must be greater than or equal to {min} and less than or equal to {max}."); } } + + /// + /// Verifies, that the method parameter with specified target value is true + /// and throws an exception if it is found to be so. + /// + /// + /// The target value, which cannot be false. + /// + /// + /// The name of the parameter that is to be checked. + /// + /// + /// The error message, if any to add to the exception. + /// + /// + /// is null + /// + public static void IsTrue(bool target, string parameterName, string message = "") + { + if (!target) + { + if (string.IsNullOrWhiteSpace(message)) + { + throw new ArgumentException(parameterName, message); + } + + throw new ArgumentException(parameterName); + } + } + + /// + /// Verifies, that the method parameter with specified target value is false + /// and throws an exception if it is found to be so. + /// + /// + /// The target value, which cannot be true. + /// + /// + /// The name of the parameter that is to be checked. + /// + /// + /// The error message, if any to add to the exception. + /// + /// + /// is null + /// + public static void IsFalse(bool target, string parameterName, string message = "") + { + if (target) + { + if (string.IsNullOrWhiteSpace(message)) + { + throw new ArgumentException(parameterName, message); + } + + throw new ArgumentException(parameterName); + } + } } } diff --git a/tests/ImageProcessorCore.Tests/Helpers/GuardTests.cs b/tests/ImageProcessorCore.Tests/Helpers/GuardTests.cs index 14b81708f..7c33470c3 100644 --- a/tests/ImageProcessorCore.Tests/Helpers/GuardTests.cs +++ b/tests/ImageProcessorCore.Tests/Helpers/GuardTests.cs @@ -200,5 +200,43 @@ namespace ImageProcessorCore.Tests.Helpers Exception ex = Record.Exception(() => Guard.MustBeBetweenOrEqualTo(0, -1, 1, "foo")); Assert.Null(ex); } + + /// + /// Tests that the method throws when the argument is false. + /// + [Fact] + public void IsTrueThrowsWhenArgIsFalse() + { + Assert.Throws(() => Guard.IsTrue(false, "foo")); + } + + /// + /// Tests that the method does not throw when the argument is true. + /// + [Fact] + public void IsTrueDoesThrowsWhenArgIsTrue() + { + Exception ex = Record.Exception(() => Guard.IsTrue(true, "foo")); + Assert.Null(ex); + } + + /// + /// Tests that the method throws when the argument is true. + /// + [Fact] + public void IsFalseThrowsWhenArgIsFalse() + { + Assert.Throws(() => Guard.IsFalse(true, "foo")); + } + + /// + /// Tests that the method does not throw when the argument is false. + /// + [Fact] + public void IsFalseDoesThrowsWhenArgIsTrue() + { + Exception ex = Record.Exception(() => Guard.IsFalse(false, "foo")); + Assert.Null(ex); + } } } \ No newline at end of file From ec047a3c14885ce94ce670729283e23a67f560b2 Mon Sep 17 00:00:00 2001 From: dirk Date: Fri, 12 Aug 2016 14:35:36 +0200 Subject: [PATCH 2/4] Add new TestImages class to get easier access to individual test images. Former-commit-id: 9a2532c9dfb7d153dcc67d2fe3978771e90e07cc Former-commit-id: 9dd731103d2774c5c5923d1b57e90c66d78349f5 Former-commit-id: 7c8dd93df1e327e3b816aa04721d35f16957215a --- .../ImageProcessorCore.Tests/FileTestBase.cs | 30 +++++------ tests/ImageProcessorCore.Tests/TestImages.cs | 52 +++++++++++++++++++ 2 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 tests/ImageProcessorCore.Tests/TestImages.cs diff --git a/tests/ImageProcessorCore.Tests/FileTestBase.cs b/tests/ImageProcessorCore.Tests/FileTestBase.cs index aff63e8d2..a0f447510 100644 --- a/tests/ImageProcessorCore.Tests/FileTestBase.cs +++ b/tests/ImageProcessorCore.Tests/FileTestBase.cs @@ -19,21 +19,21 @@ namespace ImageProcessorCore.Tests /// protected static readonly List Files = new List { - //"TestImages/Formats/Png/pl.png", - //"TestImages/Formats/Png/pd.png", - //"TestImages/Formats/Jpg/Floorplan.jpeg", // Perf: Enable for local testing only - "TestImages/Formats/Jpg/Calliphora.jpg", - //"TestImages/Formats/Jpg/turtle.jpg", - //"TestImages/Formats/Jpg/fb.jpg", // Perf: Enable for local testing only - //"TestImages/Formats/Jpg/progress.jpg", // Perf: Enable for local testing only - //"TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg", // Perf: Enable for local testing only - "TestImages/Formats/Bmp/Car.bmp", - // "TestImages/Formats/Bmp/neg_height.bmp", // Perf: Enable for local testing only - //"TestImages/Formats/Png/blur.png", // Perf: Enable for local testing only - //"TestImages/Formats/Png/indexed.png", // Perf: Enable for local testing only - "TestImages/Formats/Png/splash.png", - "TestImages/Formats/Gif/rings.gif", - //"TestImages/Formats/Gif/giphy.gif" // Perf: Enable for local testing only + //TestImages.Png.P1, + //TestImages.Png.Pd, + //TestImages.Jpg.Floorplan, // Perf: Enable for local testing only + TestImages.Jpg.Calliphora, + //TestImages.Jpg.Turtle, + //TestImages.Jpg.Fb, // Perf: Enable for local testing only + //TestImages.Jpg.Progress, // Perf: Enable for local testing only + //TestImages.Jpg.Gamma_dalai_lama_gray. // Perf: Enable for local testing only + TestImages.Bmp.Car, + //TestImages.Bmp.Neg_height, // Perf: Enable for local testing only + //TestImages.Png.Blur, // Perf: Enable for local testing only + //TestImages.Png.Indexed, // Perf: Enable for local testing only + TestImages.Png.Splash, + TestImages.Gif.Rings, + //TestImages.Gif.Giphy // Perf: Enable for local testing only }; protected void ProgressUpdate(object sender, ProgressEventArgs e) diff --git a/tests/ImageProcessorCore.Tests/TestImages.cs b/tests/ImageProcessorCore.Tests/TestImages.cs new file mode 100644 index 000000000..f9c5bb243 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/TestImages.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + /// + /// Class that contains all the test images. + /// + public static class TestImages + { + public static class Png + { + private static readonly string folder = "TestImages/Formats/Png/"; + + public static string P1 { get { return folder + "pl.png"; } } + public static string Pd { get { return folder + "pd.png"; } } + public static string Blur { get { return folder + "blur.png"; } } + public static string Indexed { get { return folder + "indexed.png"; } } + public static string Splash { get { return folder + "splash.png"; } } + } + + public static class Jpg + { + private static readonly string folder = "TestImages/Formats/Jpg/"; + + public static string Floorplan { get { return folder + "Floorplan.jpeg"; } } + public static string Calliphora { get { return folder + "Calliphora.jpg"; } } + public static string Turtle { get { return folder + "turtle.jpg"; } } + public static string Fb { get { return folder + "fb.jpg"; } } + public static string Progress { get { return folder + "progress.jpg"; } } + public static string Gamma_dalai_lama_gray { get { return folder + "gamma_dalai_lama_gray.jpg"; } } + } + + public static class Bmp + { + private static readonly string folder = "TestImages/Formats/Bmp/"; + + public static string Car { get { return folder + "Car.bmp"; } } + public static string Neg_height { get { return folder + "neg_height.bmp"; } } + } + + public static class Gif + { + private static readonly string folder = "TestImages/Formats/Gif/"; + + public static string Rings { get { return folder + "rings.gif"; } } + public static string Giphy { get { return folder + "giphy.gif"; } } + } + } +} From 3cccbf024957ebbf6bd0528c96bbcc787bbb2bb6 Mon Sep 17 00:00:00 2001 From: dirk Date: Sat, 13 Aug 2016 10:55:34 +0200 Subject: [PATCH 3/4] Added class that can be used to read and edit an Exif profile and added a property for this to the Image class. Former-commit-id: 11d6858f985ac5f9e1c17f19d1dcc418a05bbadd Former-commit-id: bfed05308fafbf01141966edb8fadc991c78b437 Former-commit-id: f97b6f7e891f5158021033ad3fb4344d1d5632d8 --- src/ImageProcessorCore/Image/Image.cs | 10 + .../Profiles/Exif/ExifDataType.cs | 78 ++ .../Profiles/Exif/ExifParts.cs | 42 + .../Profiles/Exif/ExifProfile.cs | 227 +++++ .../Profiles/Exif/ExifReader.cs | 426 ++++++++ .../Profiles/Exif/ExifTag.cs | 927 ++++++++++++++++++ .../Profiles/Exif/ExifValue.cs | 595 +++++++++++ .../Profiles/Exif/ExifWriter.cs | 407 ++++++++ .../Profiles/Exif/README.md | 3 + 9 files changed, 2715 insertions(+) create mode 100644 src/ImageProcessorCore/Profiles/Exif/ExifDataType.cs create mode 100644 src/ImageProcessorCore/Profiles/Exif/ExifParts.cs create mode 100644 src/ImageProcessorCore/Profiles/Exif/ExifProfile.cs create mode 100644 src/ImageProcessorCore/Profiles/Exif/ExifReader.cs create mode 100644 src/ImageProcessorCore/Profiles/Exif/ExifTag.cs create mode 100644 src/ImageProcessorCore/Profiles/Exif/ExifValue.cs create mode 100644 src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs create mode 100644 src/ImageProcessorCore/Profiles/Exif/README.md diff --git a/src/ImageProcessorCore/Image/Image.cs b/src/ImageProcessorCore/Image/Image.cs index a53c87c5f..ff91e2372 100644 --- a/src/ImageProcessorCore/Image/Image.cs +++ b/src/ImageProcessorCore/Image/Image.cs @@ -91,6 +91,11 @@ namespace ImageProcessorCore this.HorizontalResolution = other.HorizontalResolution; this.VerticalResolution = other.VerticalResolution; this.CurrentImageFormat = other.CurrentImageFormat; + + if (other.ExifProfile != null) + { + this.ExifProfile = new ExifProfile(other.ExifProfile); + } } /// @@ -185,6 +190,11 @@ namespace ImageProcessorCore /// public IImageFormat CurrentImageFormat { get; internal set; } + /// + /// Gets or sets the Exif profile. + /// + public ExifProfile ExifProfile { get; set; } + /// public override IPixelAccessor Lock() { diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifDataType.cs b/src/ImageProcessorCore/Profiles/Exif/ExifDataType.cs new file mode 100644 index 000000000..517b76011 --- /dev/null +++ b/src/ImageProcessorCore/Profiles/Exif/ExifDataType.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + /// + /// Specifies exif data types. + /// + public enum ExifDataType + { + /// + /// Unknown + /// + Unknown, + + /// + /// Byte + /// + Byte, + + /// + /// Ascii + /// + Ascii, + + /// + /// Short + /// + Short, + + /// + /// Long + /// + Long, + + /// + /// Rational + /// + Rational, + + /// + /// SignedByte + /// + SignedByte, + + /// + /// Undefined + /// + Undefined, + + /// + /// SignedShort + /// + SignedShort, + + /// + /// SignedLong + /// + SignedLong, + + /// + /// SignedRational + /// + SignedRational, + + /// + /// SingleFloat + /// + SingleFloat, + + /// + /// DoubleFloat + /// + DoubleFloat + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifParts.cs b/src/ImageProcessorCore/Profiles/Exif/ExifParts.cs new file mode 100644 index 000000000..3ad53430b --- /dev/null +++ b/src/ImageProcessorCore/Profiles/Exif/ExifParts.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + + /// + /// Specifies which parts will be written when the profile is added to an image. + /// + [Flags] + public enum ExifParts + { + + /// + /// None + /// + None = 0, + + /// + /// IfdTags + /// + IfdTags = 1, + + /// + /// ExifTags + /// + ExifTags = 4, + + /// + /// GPSTags + /// + GPSTags = 8, + + /// + /// All + /// + All = IfdTags | ExifTags | GPSTags + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifProfile.cs b/src/ImageProcessorCore/Profiles/Exif/ExifProfile.cs new file mode 100644 index 000000000..2caa22859 --- /dev/null +++ b/src/ImageProcessorCore/Profiles/Exif/ExifProfile.cs @@ -0,0 +1,227 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.IO; + + /// + /// Class that can be used to access an Exif profile. + /// + public sealed class ExifProfile + { + private byte[] data; + private Collection values; + private List invalidTags; + private int thumbnailOffset; + private int thumbnailLength; + + /// + /// Initializes a new instance of the class. + /// + ///The byte array to read the exif profile from. + public ExifProfile() + : this((byte[])null) + { + } + + /// + /// Initializes a new instance of the class. + /// + ///The byte array to read the exif profile from. + public ExifProfile(byte[] data) + { + Parts = ExifParts.All; + BestPrecision = false; + this.data = data; + this.invalidTags = new List(); + } + + /// + /// Initializes a new instance of the class + /// by making a copy from another exif profile. + /// + /// The other exif profile, where the clone should be made from. + /// is null. + public ExifProfile(ExifProfile other) + { + Guard.NotNull(other, nameof(other)); + + Parts = other.Parts; + BestPrecision = other.BestPrecision; + + this.thumbnailLength = other.thumbnailLength; + this.thumbnailOffset = other.thumbnailOffset; + this.invalidTags = new List(other.invalidTags); + if (other.values != null) + { + this.values = new Collection(); + foreach(ExifValue value in other.values) + { + this.values.Add(new ExifValue(value)); + } + } + else + { + this.data = other.data; + } + } + + /// + /// Specifies if rationals should be stored with the best precision possible. This is disabled + /// by default, setting this to true will have an impact on the performance. + /// + public bool BestPrecision + { + get; + set; + } + + /// + /// Specifies which parts will be written when the profile is added to an image. + /// + public ExifParts Parts + { + get; + set; + } + + /// + /// Returns the tags that where found but contained an invalid value. + /// + public IEnumerable InvalidTags + { + get + { + return this.invalidTags; + } + } + + /// + /// Returns the values of this exif profile. + /// + public IEnumerable Values + { + get + { + InitializeValues(); + return this.values; + } + } + + /// + /// Returns the thumbnail in the exif profile when available. + /// + /// The pixel format. + /// The packed format. long, float. + public Image CreateThumbnail() + where T : IPackedVector + where TP : struct + { + InitializeValues(); + + if (this.thumbnailOffset == 0 || this.thumbnailLength == 0) + return null; + + if (this.data.Length < (this.thumbnailOffset + this.thumbnailLength)) + return null; + + using (MemoryStream memStream = new MemoryStream(this.data, this.thumbnailOffset, this.thumbnailLength)) + { + return new Image(memStream); + } + } + + /// + /// Returns the value with the specified tag. + /// + ///The tag of the exif value. + public ExifValue GetValue(ExifTag tag) + { + foreach (ExifValue exifValue in Values) + { + if (exifValue.Tag == tag) + return exifValue; + } + + return null; + } + + /// + /// Removes the value with the specified tag. + /// + ///The tag of the exif value. + public bool RemoveValue(ExifTag tag) + { + InitializeValues(); + + for (int i = 0; i < this.values.Count; i++) + { + if (this.values[i].Tag == tag) + { + this.values.RemoveAt(i); + return true; + } + } + + return false; + } + + /// + /// Sets the value of the specified tag. + /// + ///The tag of the exif value. + ///The value. + public void SetValue(ExifTag tag, object value) + { + foreach (ExifValue exifValue in Values) + { + if (exifValue.Tag == tag) + { + exifValue.Value = value; + return; + } + } + + ExifValue newExifValue = ExifValue.Create(tag, value); + this.values.Add(newExifValue); + } + + /// + /// Converts this instance to a byte array. + /// + public byte[] ToByteArray() + { + if (this.values == null) + return data; + + if (this.values.Count == 0) + return null; + + ExifWriter writer = new ExifWriter(this.values, Parts, BestPrecision); + return writer.GetData(); + } + + private void InitializeValues() + { + if (this.values != null) + return; + + if (this.data == null) + { + this.values = new Collection(); + return; + } + + ExifReader reader = new ExifReader(); + this.values = reader.Read(this.data); + this.invalidTags = new List(reader.InvalidTags); + this.thumbnailOffset = (int)reader.ThumbnailOffset; + this.thumbnailLength = (int)reader.ThumbnailLength; + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs b/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs new file mode 100644 index 000000000..c37d15388 --- /dev/null +++ b/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs @@ -0,0 +1,426 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Text; + + internal sealed class ExifReader + { + private delegate TDataType ConverterMethod(byte[] data); + + private byte[] data; + private Collection invalidTags = new Collection(); + private uint index; + private bool isLittleEndian; + private uint exifOffset; + private uint gpsOffset; + private uint startIndex; + + public uint ThumbnailLength + { + get; + private set; + } + + public uint ThumbnailOffset + { + get; + private set; + } + + private int RemainingLength + { + get + { + if (this.index >= this.data.Length) + return 0; + + return this.data.Length - (int)this.index; + } + } + + public Collection Read(byte[] data) + { + Collection result = new Collection(); + + this.data = data; + + if (GetString(4) == "Exif") + { + if (GetShort() != 0) + return result; + + this.startIndex = 6; + } + else + { + this.index = 0; + } + + this.isLittleEndian = GetString(2) == "II"; + + if (GetShort() != 0x002A) + return result; + + uint ifdOffset = GetLong(); + AddValues(result, ifdOffset); + + uint thumbnailOffset = GetLong(); + GetThumbnail(thumbnailOffset); + + if (this.exifOffset != 0) + AddValues(result, this.exifOffset); + + if (this.gpsOffset != 0) + AddValues(result, this.gpsOffset); + + return result; + } + + public IEnumerable InvalidTags + { + get + { + return this.invalidTags; + } + } + + private void AddValues(Collection values, uint index) + { + this.index = this.startIndex + index; + ushort count = GetShort(); + + for (ushort i = 0; i < count; i++) + { + ExifValue value = CreateValue(); + if (value == null) + continue; + + bool duplicate = false; + foreach (ExifValue val in values) + { + if (val.Tag == value.Tag) + { + duplicate = true; + break; + } + } + + if (duplicate) + continue; + + if (value.Tag == ExifTag.SubIFDOffset) + { + if (value.DataType == ExifDataType.Long) + this.exifOffset = (uint)value.Value; + } + else if (value.Tag == ExifTag.GPSIFDOffset) + { + if (value.DataType == ExifDataType.Long) + this.gpsOffset = (uint)value.Value; + } + else + values.Add(value); + } + } + + private object ConvertValue(ExifDataType dataType, byte[] data, uint numberOfComponents) + { + if (data == null || data.Length == 0) + return null; + + switch (dataType) + { + case ExifDataType.Unknown: + return null; + case ExifDataType.Ascii: + return ToString(data); + case ExifDataType.Byte: + if (numberOfComponents == 1) + return ToByte(data); + else + return data; + case ExifDataType.DoubleFloat: + if (numberOfComponents == 1) + return ToDouble(data); + else + return ToArray(dataType, data, ToDouble); + case ExifDataType.Long: + if (numberOfComponents == 1) + return ToLong(data); + else + return ToArray(dataType, data, ToLong); + case ExifDataType.Rational: + if (numberOfComponents == 1) + return ToRational(data); + else + return ToArray(dataType, data, ToRational); + case ExifDataType.Short: + if (numberOfComponents == 1) + return ToShort(data); + else + return ToArray(dataType, data, ToShort); + case ExifDataType.SignedByte: + if (numberOfComponents == 1) + return ToSignedByte(data); + else + return ToArray(dataType, data, ToSignedByte); + case ExifDataType.SignedLong: + if (numberOfComponents == 1) + return ToSignedLong(data); + else + return ToArray(dataType, data, ToSignedLong); + case ExifDataType.SignedRational: + if (numberOfComponents == 1) + return ToSignedRational(data); + else + return ToArray(dataType, data, ToSignedRational); + case ExifDataType.SignedShort: + if (numberOfComponents == 1) + return ToSignedShort(data); + else + return ToArray(dataType, data, ToSignedShort); + case ExifDataType.SingleFloat: + if (numberOfComponents == 1) + return ToSingle(data); + else + return ToArray(dataType, data, ToSingle); + case ExifDataType.Undefined: + if (numberOfComponents == 1) + return ToByte(data); + else + return data; + default: + throw new NotImplementedException(); + } + } + + private ExifValue CreateValue() + { + if (RemainingLength < 12) + return null; + + ExifTag tag = ToEnum(GetShort(), ExifTag.Unknown); + ExifDataType dataType = ToEnum(GetShort(), ExifDataType.Unknown); + object value = null; + + if (dataType == ExifDataType.Unknown) + return new ExifValue(tag, dataType, value, false); + + uint numberOfComponents = (uint)GetLong(); + + uint size = numberOfComponents * ExifValue.GetSize(dataType); + byte[] data = GetBytes(4); + + if (size > 4) + { + uint oldIndex = this.index; + this.index = ToLong(data) + this.startIndex; + if (RemainingLength < size) + { + this.invalidTags.Add(tag); + this.index = oldIndex; + return null; + } + value = ConvertValue(dataType, GetBytes(size), numberOfComponents); + this.index = oldIndex; + } + else + { + value = ConvertValue(dataType, data, numberOfComponents); + } + + bool isArray = value != null && numberOfComponents > 1; + return new ExifValue(tag, dataType, value, isArray); + } + + private TEnum ToEnum(int value, TEnum defaultValue) + where TEnum : struct + { + TEnum enumValue = (TEnum)(object)value; + if (Enum.GetValues(typeof(TEnum)).Cast().Any(v => v.Equals(enumValue))) + return enumValue; + + return defaultValue; + } + + private byte[] GetBytes(uint length) + { + if (this.index + length > (uint)this.data.Length) + return null; + + byte[] data = new byte[length]; + Array.Copy(this.data, (int)this.index, data, 0, (int)length); + this.index += length; + + return data; + } + + private uint GetLong() + { + return ToLong(GetBytes(4)); + } + + private ushort GetShort() + { + return ToShort(GetBytes(2)); + } + + private string GetString(uint length) + { + return ToString(GetBytes(length)); + } + + private void GetThumbnail(uint offset) + { + Collection values = new Collection(); + AddValues(values, offset); + + foreach (ExifValue value in values) + { + if (value.Tag == ExifTag.JPEGInterchangeFormat && (value.DataType == ExifDataType.Long)) + ThumbnailOffset = (uint)value.Value + this.startIndex; + else if (value.Tag == ExifTag.JPEGInterchangeFormatLength && value.DataType == ExifDataType.Long) + ThumbnailLength = (uint)value.Value; + } + } + + private static TDataType[] ToArray(ExifDataType dataType, Byte[] data, + ConverterMethod converter) + { + int dataTypeSize = (int)ExifValue.GetSize(dataType); + int length = data.Length / dataTypeSize; + + TDataType[] result = new TDataType[length]; + byte[] buffer = new byte[dataTypeSize]; + + for (int i = 0; i < length; i++) + { + Array.Copy(data, i * dataTypeSize, buffer, 0, dataTypeSize); + + result.SetValue(converter(buffer), i); + } + + return result; + } + + private static byte ToByte(byte[] data) + { + return data[0]; + } + + private double ToDouble(byte[] data) + { + if (!ValidateArray(data, 8)) + return default(double); + + return BitConverter.ToDouble(data, 0); + } + + private uint ToLong(byte[] data) + { + if (!ValidateArray(data, 4)) + return default(uint); + + return BitConverter.ToUInt32(data, 0); + } + + private ushort ToShort(byte[] data) + { + + if (!ValidateArray(data, 2)) + return default(ushort); + + return BitConverter.ToUInt16(data, 0); + } + + private float ToSingle(byte[] data) + { + if (!ValidateArray(data, 4)) + return default(float); + + return BitConverter.ToSingle(data, 0); + } + + private static string ToString(byte[] data) + { + string result = Encoding.UTF8.GetString(data, 0, data.Length); + int nullCharIndex = result.IndexOf('\0'); + if (nullCharIndex != -1) + result = result.Substring(0, nullCharIndex); + + return result; + } + + private double ToRational(byte[] data) + { + if (!ValidateArray(data, 8, 4)) + return default(double); + + uint numerator = BitConverter.ToUInt32(data, 0); + uint denominator = BitConverter.ToUInt32(data, 4); + + return numerator / (double)denominator; + } + + private sbyte ToSignedByte(byte[] data) + { + return unchecked((sbyte)data[0]); + } + + private int ToSignedLong(byte[] data) + { + if (!ValidateArray(data, 4)) + return default(int); + + return BitConverter.ToInt32(data, 0); + } + + private double ToSignedRational(byte[] data) + { + if (!ValidateArray(data, 8, 4)) + return default(double); + + int numerator = BitConverter.ToInt32(data, 0); + int denominator = BitConverter.ToInt32(data, 4); + + return numerator / (double)denominator; + } + + private short ToSignedShort(byte[] data) + { + if (!ValidateArray(data, 2)) + return default(short); + + return BitConverter.ToInt16(data, 0); + } + + private bool ValidateArray(byte[] data, int size) + { + return ValidateArray(data, size, size); + } + + private bool ValidateArray(byte[] data, int size, int stepSize) + { + if (data == null || data.Length < size) + return false; + + if (this.isLittleEndian == BitConverter.IsLittleEndian) + return true; + + for (int i = 0; i < data.Length; i += stepSize) + { + Array.Reverse(data, i, stepSize); + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifTag.cs b/src/ImageProcessorCore/Profiles/Exif/ExifTag.cs new file mode 100644 index 000000000..944a2a15a --- /dev/null +++ b/src/ImageProcessorCore/Profiles/Exif/ExifTag.cs @@ -0,0 +1,927 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + /// + /// All exif tags from the Exif standard 2.2 + /// + public enum ExifTag + { + /// + /// Unknown + /// + Unknown = 0xFFFF, + + /// + /// SubIFDOffset + /// + SubIFDOffset = 0x8769, + + /// + /// GPSIFDOffset + /// + GPSIFDOffset = 0x8825, + + + /// + /// ImageWidth + /// + ImageWidth = 0x0100, + + /// + /// ImageLength + /// + ImageLength = 0x0101, + + /// + /// BitsPerSample + /// + BitsPerSample = 0x0102, + + /// + /// Compression + /// + Compression = 0x0103, + + /// + /// PhotometricInterpretation + /// + PhotometricInterpretation = 0x0106, + + /// + /// Threshholding + /// + Threshholding = 0x0107, + + /// + /// CellWidth + /// + CellWidth = 0x0108, + + /// + /// CellLength + /// + CellLength = 0x0109, + + /// + /// FillOrder + /// + FillOrder = 0x010A, + + /// + /// ImageDescription + /// + ImageDescription = 0x010E, + + /// + /// Make + /// + Make = 0x010F, + + /// + /// Model + /// + Model = 0x0110, + + /// + /// StripOffsets + /// + StripOffsets = 0x0111, + + /// + /// Orientation + /// + Orientation = 0x0112, + + /// + /// SamplesPerPixel + /// + SamplesPerPixel = 0x0115, + + /// + /// RowsPerStrip + /// + RowsPerStrip = 0x0116, + + /// + /// StripByteCounts + /// + StripByteCounts = 0x0117, + + /// + /// MinSampleValue + /// + MinSampleValue = 0x0118, + + /// + /// MaxSampleValue + /// + MaxSampleValue = 0x0119, + + /// + /// XResolution + /// + XResolution = 0x011A, + + /// + /// YResolution + /// + YResolution = 0x011B, + + /// + /// PlanarConfiguration + /// + PlanarConfiguration = 0x011C, + + /// + /// FreeOffsets + /// + FreeOffsets = 0x0120, + + /// + /// FreeByteCounts + /// + FreeByteCounts = 0x0121, + + /// + /// GrayResponseUnit + /// + GrayResponseUnit = 0x0122, + + /// + /// GrayResponseCurve + /// + GrayResponseCurve = 0x0123, + + /// + /// ResolutionUnit + /// + ResolutionUnit = 0x0128, + + /// + /// Software + /// + Software = 0x0131, + + /// + /// DateTime + /// + DateTime = 0x0132, + + /// + /// Artist + /// + Artist = 0x013B, + + /// + /// HostComputer + /// + HostComputer = 0x013C, + + /// + /// ColorMap + /// + ColorMap = 0x0140, + + /// + /// ExtraSamples + /// + ExtraSamples = 0x0152, + + /// + /// Copyright + /// + Copyright = 0x8298, + + + /// + /// DocumentName + /// + DocumentName = 0x010D, + + /// + /// PageName + /// + PageName = 0x011D, + + /// + /// XPosition + /// + XPosition = 0x011E, + + /// + /// YPosition + /// + YPosition = 0x011F, + + /// + /// T4Options + /// + T4Options = 0x0124, + + /// + /// T6Options + /// + T6Options = 0x0125, + + /// + /// PageNumber + /// + PageNumber = 0x0129, + + /// + /// TransferFunction + /// + TransferFunction = 0x012D, + + /// + /// Predictor + /// + Predictor = 0x013D, + + /// + /// WhitePoint + /// + WhitePoint = 0x013E, + + /// + /// PrimaryChromaticities + /// + PrimaryChromaticities = 0x013F, + + /// + /// HalftoneHints + /// + HalftoneHints = 0x0141, + + /// + /// TileWidth + /// + TileWidth = 0x0142, + + /// + /// TileLength + /// + TileLength = 0x0143, + + /// + /// TileOffsets + /// + TileOffsets = 0x0144, + + /// + /// TileByteCounts + /// + TileByteCounts = 0x0145, + + /// + /// BadFaxLines + /// + BadFaxLines = 0x0146, + + /// + /// CleanFaxData + /// + CleanFaxData = 0x0147, + + /// + /// ConsecutiveBadFaxLines + /// + ConsecutiveBadFaxLines = 0x0148, + + /// + /// InkSet + /// + InkSet = 0x014C, + + /// + /// InkNames + /// + InkNames = 0x014D, + + /// + /// NumberOfInks + /// + NumberOfInks = 0x014E, + + /// + /// DotRange + /// + DotRange = 0x0150, + + /// + /// TargetPrinter + /// + TargetPrinter = 0x0151, + + /// + /// SampleFormat + /// + SampleFormat = 0x0153, + + /// + /// SMinSampleValue + /// + SMinSampleValue = 0x0154, + + /// + /// SMaxSampleValue + /// + SMaxSampleValue = 0x0155, + + /// + /// TransferRange + /// + TransferRange = 0x0156, + + /// + /// ClipPath + /// + ClipPath = 0x0157, + + /// + /// XClipPathUnits + /// + XClipPathUnits = 0x0158, + + /// + /// YClipPathUnits + /// + YClipPathUnits = 0x0159, + + /// + /// Indexed + /// + Indexed = 0x015A, + + /// + /// JPEGTables + /// + JPEGTables = 0x015B, + + /// + /// OPIProxy + /// + OPIProxy = 0x015F, + + /// + /// ProfileType + /// + ProfileType = 0x0191, + + /// + /// FaxProfile + /// + FaxProfile = 0x0192, + + /// + /// CodingMethods + /// + CodingMethods = 0x0193, + + /// + /// VersionYear + /// + VersionYear = 0x0194, + + /// + /// ModeNumber + /// + ModeNumber = 0x0195, + + /// + /// Decode + /// + Decode = 0x01B1, + + /// + /// DefaultImageColor + /// + DefaultImageColor = 0x01B2, + + /// + /// JPEGProc + /// + JPEGProc = 0x0200, + + /// + /// JPEGInterchangeFormat + /// + JPEGInterchangeFormat = 0x0201, + + /// + /// JPEGInterchangeFormatLength + /// + JPEGInterchangeFormatLength = 0x0202, + + /// + /// JPEGRestartInterval + /// + JPEGRestartInterval = 0x0203, + + /// + /// JPEGLosslessPredictors + /// + JPEGLosslessPredictors = 0x0205, + + /// + /// JPEGPointTransforms + /// + JPEGPointTransforms = 0x0206, + + /// + /// JPEGQTables + /// + JPEGQTables = 0x0207, + + /// + /// JPEGDCTables + /// + JPEGDCTables = 0x0208, + + /// + /// JPEGACTables + /// + JPEGACTables = 0x0209, + + /// + /// YCbCrCoefficients + /// + YCbCrCoefficients = 0x0211, + + /// + /// YCbCrSubsampling + /// + YCbCrSubsampling = 0x0212, + + /// + /// YCbCrPositioning + /// + YCbCrPositioning = 0x0213, + + /// + /// ReferenceBlackWhite + /// + ReferenceBlackWhite = 0x0214, + + /// + /// StripRowCounts + /// + StripRowCounts = 0x022F, + + /// + /// XMP + /// + XMP = 0x02BC, + + /// + /// ImageID + /// + ImageID = 0x800D, + + /// + /// ImageLayer + /// + ImageLayer = 0x87AC, + + + /// + /// ExposureTime + /// + ExposureTime = 0x829A, + + /// + /// FNumber + /// + FNumber = 0x829D, + + /// + /// ExposureProgram + /// + ExposureProgram = 0x8822, + + /// + /// SpectralSensitivity + /// + SpectralSensitivity = 0x8824, + + /// + /// ISOSpeedRatings + /// + ISOSpeedRatings = 0x8827, + + /// + /// OECF + /// + OECF = 0x8828, + + /// + /// ExifVersion + /// + ExifVersion = 0x9000, + + /// + /// DateTimeOriginal + /// + DateTimeOriginal = 0x9003, + + /// + /// DateTimeDigitized + /// + DateTimeDigitized = 0x9004, + + /// + /// ComponentsConfiguration + /// + ComponentsConfiguration = 0x9101, + + /// + /// CompressedBitsPerPixel + /// + CompressedBitsPerPixel = 0x9102, + + /// + /// ShutterSpeedValue + /// + ShutterSpeedValue = 0x9201, + + /// + /// ApertureValue + /// + ApertureValue = 0x9202, + + /// + /// BrightnessValue + /// + BrightnessValue = 0x9203, + + /// + /// ExposureBiasValue + /// + ExposureBiasValue = 0x9204, + + /// + /// MaxApertureValue + /// + MaxApertureValue = 0x9205, + + /// + /// SubjectDistance + /// + SubjectDistance = 0x9206, + + /// + /// MeteringMode + /// + MeteringMode = 0x9207, + + /// + /// LightSource + /// + LightSource = 0x9208, + + /// + /// Flash + /// + Flash = 0x9209, + + /// + /// FocalLength + /// + FocalLength = 0x920A, + + /// + /// SubjectArea + /// + SubjectArea = 0x9214, + + /// + /// MakerNote + /// + MakerNote = 0x927C, + + /// + /// UserComment + /// + UserComment = 0x9286, + + /// + /// SubsecTime + /// + SubsecTime = 0x9290, + + /// + /// SubsecTimeOriginal + /// + SubsecTimeOriginal = 0x9291, + + /// + /// SubsecTimeDigitized + /// + SubsecTimeDigitized = 0x9292, + + /// + /// FlashpixVersion + /// + FlashpixVersion = 0xA000, + + /// + /// ColorSpace + /// + ColorSpace = 0xA001, + + /// + /// PixelXDimension + /// + PixelXDimension = 0xA002, + + /// + /// PixelYDimension + /// + PixelYDimension = 0xA003, + + /// + /// RelatedSoundFile + /// + RelatedSoundFile = 0xA004, + + /// + /// FlashEnergy + /// + FlashEnergy = 0xA20B, + + /// + /// SpatialFrequencyResponse + /// + SpatialFrequencyResponse = 0xA20C, + + /// + /// FocalPlaneXResolution + /// + FocalPlaneXResolution = 0xA20E, + + /// + /// FocalPlaneYResolution + /// + FocalPlaneYResolution = 0xA20F, + + /// + /// FocalPlaneResolutionUnit + /// + FocalPlaneResolutionUnit = 0xA210, + + /// + /// SubjectLocation + /// + SubjectLocation = 0xA214, + + /// + /// ExposureIndex + /// + ExposureIndex = 0xA215, + + /// + /// SensingMethod + /// + SensingMethod = 0xA217, + + /// + /// FileSource + /// + FileSource = 0xA300, + + /// + /// SceneType + /// + SceneType = 0xA301, + + /// + /// CFAPattern + /// + CFAPattern = 0xA302, + + /// + /// CustomRendered + /// + CustomRendered = 0xA401, + + /// + /// ExposureMode + /// + ExposureMode = 0xA402, + + /// + /// WhiteBalance + /// + WhiteBalance = 0xA403, + + /// + /// DigitalZoomRatio + /// + DigitalZoomRatio = 0xA404, + + /// + /// FocalLengthIn35mmFilm + /// + FocalLengthIn35mmFilm = 0xA405, + + /// + /// SceneCaptureType + /// + SceneCaptureType = 0xA406, + + /// + /// GainControl + /// + GainControl = 0xA407, + + /// + /// Contrast + /// + Contrast = 0xA408, + + /// + /// Saturation + /// + Saturation = 0xA409, + + /// + /// Sharpness + /// + Sharpness = 0xA40A, + + /// + /// DeviceSettingDescription + /// + DeviceSettingDescription = 0xA40B, + + /// + /// SubjectDistanceRange + /// + SubjectDistanceRange = 0xA40C, + + /// + /// ImageUniqueID + /// + ImageUniqueID = 0xA420, + + + /// + /// GPSVersionID + /// + GPSVersionID = 0x0000, + + /// + /// GPSLatitudeRef + /// + GPSLatitudeRef = 0x0001, + + /// + /// GPSLatitude + /// + GPSLatitude = 0x0002, + + /// + /// GPSLongitudeRef + /// + GPSLongitudeRef = 0x0003, + + /// + /// GPSLongitude + /// + GPSLongitude = 0x0004, + + /// + /// GPSAltitudeRef + /// + GPSAltitudeRef = 0x0005, + + /// + /// GPSAltitude + /// + GPSAltitude = 0x0006, + + /// + /// GPSTimestamp + /// + GPSTimestamp = 0x0007, + + /// + /// GPSSatellites + /// + GPSSatellites = 0x0008, + + /// + /// GPSStatus + /// + GPSStatus = 0x0009, + + /// + /// GPSMeasureMode + /// + GPSMeasureMode = 0x000A, + + /// + /// GPSDOP + /// + GPSDOP = 0x000B, + + /// + /// GPSSpeedRef + /// + GPSSpeedRef = 0x000C, + + /// + /// GPSSpeed + /// + GPSSpeed = 0x000D, + + /// + /// GPSTrackRef + /// + GPSTrackRef = 0x000E, + + /// + /// GPSTrack + /// + GPSTrack = 0x000F, + + /// + /// GPSImgDirectionRef + /// + GPSImgDirectionRef = 0x0010, + + /// + /// GPSImgDirection + /// + GPSImgDirection = 0x0011, + + /// + /// GPSMapDatum + /// + GPSMapDatum = 0x0012, + + /// + /// GPSDestLatitudeRef + /// + GPSDestLatitudeRef = 0x0013, + + /// + /// GPSDestLatitude + /// + GPSDestLatitude = 0x0014, + + /// + /// GPSDestLongitudeRef + /// + GPSDestLongitudeRef = 0x0015, + + /// + /// GPSDestLongitude + /// + GPSDestLongitude = 0x0016, + + /// + /// GPSDestBearingRef + /// + GPSDestBearingRef = 0x0017, + + /// + /// GPSDestBearing + /// + GPSDestBearing = 0x0018, + + /// + /// GPSDestDistanceRef + /// + GPSDestDistanceRef = 0x0019, + + /// + /// GPSDestDistance + /// + GPSDestDistance = 0x001A, + + /// + /// GPSProcessingMethod + /// + GPSProcessingMethod = 0x001B, + + /// + /// GPSAreaInformation + /// + GPSAreaInformation = 0x001C, + + /// + /// GPSDateStamp + /// + GPSDateStamp = 0x001D, + + /// + /// GPSDifferential + /// + GPSDifferential = 0x001E + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs b/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs new file mode 100644 index 000000000..860ff30dc --- /dev/null +++ b/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs @@ -0,0 +1,595 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + using System.Globalization; + using System.Text; + + /// + /// A value of the exif profile. + /// + public sealed class ExifValue : IEquatable + { + private object value; + + /// + /// Initializes a new instance of the class + /// by making a copy from another exif value. + /// + /// The other exif value, where the clone should be made from. + /// is null. + public ExifValue(ExifValue other) + { + Guard.NotNull(other, nameof(other)); + + DataType = other.DataType; + IsArray = other.IsArray; + Tag = other.Tag; + + if (!other.IsArray) + { + value = other.value; + } + else + { + Array array = (Array)other.value; + value = array.Clone(); + } + } + + /// + /// The data type of the exif value. + /// + public ExifDataType DataType + { + get; + private set; + } + + /// + /// Returns true if the value is an array. + /// + public bool IsArray + { + get; + private set; + } + + /// + /// The tag of the exif value. + /// + public ExifTag Tag + { + get; + private set; + } + + /// + /// The value. + /// + public object Value + { + get + { + return this.value; + } + set + { + CheckValue(value); + this.value = value; + } + } + + /// + /// Determines whether the specified ExifValue instances are considered equal. + /// + /// The first ExifValue to compare. + /// The second ExifValue to compare. + /// + public static bool operator ==(ExifValue left, ExifValue right) + { + return Equals(left, right); + } + + /// + /// Determines whether the specified ExifValue instances are not considered equal. + /// + /// The first ExifValue to compare. + /// The second ExifValue to compare. + /// + public static bool operator !=(ExifValue left, ExifValue right) + { + return !Equals(left, right); + } + + /// + /// Determines whether the specified object is equal to the current exif value. + /// + ///The object to compare this exif value with. + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + return true; + + return Equals(obj as ExifValue); + } + + /// + /// Determines whether the specified exif value is equal to the current exif value. + /// + ///The exif value to compare this exif value with. + public bool Equals(ExifValue other) + { + if (ReferenceEquals(other, null)) + return false; + + if (ReferenceEquals(this, other)) + return true; + + return + Tag == other.Tag && + DataType == other.DataType && + object.Equals(this.value, other.value); + } + + /// + /// Serves as a hash of this type. + /// + public override int GetHashCode() + { + int hashCode = Tag.GetHashCode() ^ DataType.GetHashCode(); + return this.value != null ? hashCode ^ this.value.GetHashCode() : hashCode; + } + + /// + /// Returns a string that represents the current value. + /// + public override string ToString() + { + + if (this.value == null) + return null; + + if (DataType == ExifDataType.Ascii) + return (string)this.value; + + if (!IsArray) + return ToString(this.value); + + StringBuilder sb = new StringBuilder(); + foreach (object value in (Array)this.value) + { + sb.Append(ToString(value)); + sb.Append(" "); + } + + return sb.ToString(); + } + + internal bool HasValue + { + get + { + if (this.value == null) + return false; + + if (DataType == ExifDataType.Ascii) + return ((string)this.value).Length > 0; + + return true; + } + } + + internal int Length + { + get + { + if (this.value == null) + return 4; + + int size = (int)(GetSize(DataType) * NumberOfComponents); + + return size < 4 ? 4 : size; + } + } + + internal int NumberOfComponents + { + get + { + if (DataType == ExifDataType.Ascii) + return Encoding.UTF8.GetBytes((string)this.value).Length; + + if (IsArray) + return ((Array)this.value).Length; + + return 1; + } + } + + internal ExifValue(ExifTag tag, ExifDataType dataType, bool isArray) + { + Tag = tag; + DataType = dataType; + IsArray = isArray; + + if (dataType == ExifDataType.Ascii) + IsArray = false; + } + + internal ExifValue(ExifTag tag, ExifDataType dataType, object value, bool isArray) + : this(tag, dataType, isArray) + { + this.value = value; + } + + internal static ExifValue Create(ExifTag tag, object value) + { + Guard.IsFalse(tag == ExifTag.Unknown, nameof(tag), "Invalid Tag"); + + ExifValue exifValue = null; + Type type = value != null ? value.GetType() : null; + if (type != null && type.IsArray) + type = type.GetElementType(); + + switch (tag) + { + case ExifTag.ImageDescription: + case ExifTag.Make: + case ExifTag.Model: + case ExifTag.Software: + case ExifTag.DateTime: + case ExifTag.Artist: + case ExifTag.HostComputer: + case ExifTag.Copyright: + case ExifTag.DocumentName: + case ExifTag.PageName: + case ExifTag.InkNames: + case ExifTag.TargetPrinter: + case ExifTag.ImageID: + case ExifTag.SpectralSensitivity: + case ExifTag.DateTimeOriginal: + case ExifTag.DateTimeDigitized: + case ExifTag.SubsecTime: + case ExifTag.SubsecTimeOriginal: + case ExifTag.SubsecTimeDigitized: + case ExifTag.RelatedSoundFile: + case ExifTag.ImageUniqueID: + case ExifTag.GPSLatitudeRef: + case ExifTag.GPSLongitudeRef: + case ExifTag.GPSSatellites: + case ExifTag.GPSStatus: + case ExifTag.GPSMeasureMode: + case ExifTag.GPSSpeedRef: + case ExifTag.GPSTrackRef: + case ExifTag.GPSImgDirectionRef: + case ExifTag.GPSMapDatum: + case ExifTag.GPSDestLatitudeRef: + case ExifTag.GPSDestLongitudeRef: + case ExifTag.GPSDestBearingRef: + case ExifTag.GPSDestDistanceRef: + case ExifTag.GPSDateStamp: + exifValue = new ExifValue(tag, ExifDataType.Ascii, true); + break; + + case ExifTag.ClipPath: + case ExifTag.VersionYear: + case ExifTag.XMP: + case ExifTag.GPSVersionID: + exifValue = new ExifValue(tag, ExifDataType.Byte, true); + break; + case ExifTag.FaxProfile: + case ExifTag.ModeNumber: + case ExifTag.GPSAltitudeRef: + exifValue = new ExifValue(tag, ExifDataType.Byte, false); + break; + + case ExifTag.FreeOffsets: + case ExifTag.FreeByteCounts: + case ExifTag.TileOffsets: + case ExifTag.SMinSampleValue: + case ExifTag.SMaxSampleValue: + case ExifTag.JPEGQTables: + case ExifTag.JPEGDCTables: + case ExifTag.JPEGACTables: + case ExifTag.StripRowCounts: + exifValue = new ExifValue(tag, ExifDataType.Long, true); + break; + case ExifTag.SubIFDOffset: + case ExifTag.GPSIFDOffset: + case ExifTag.T4Options: + case ExifTag.T6Options: + case ExifTag.XClipPathUnits: + case ExifTag.YClipPathUnits: + case ExifTag.ProfileType: + case ExifTag.CodingMethods: + case ExifTag.JPEGInterchangeFormat: + case ExifTag.JPEGInterchangeFormatLength: + exifValue = new ExifValue(tag, ExifDataType.Long, false); + break; + + case ExifTag.WhitePoint: + case ExifTag.PrimaryChromaticities: + case ExifTag.YCbCrCoefficients: + case ExifTag.ReferenceBlackWhite: + case ExifTag.GPSLatitude: + case ExifTag.GPSLongitude: + case ExifTag.GPSTimestamp: + case ExifTag.GPSDestLatitude: + case ExifTag.GPSDestLongitude: + exifValue = new ExifValue(tag, ExifDataType.Rational, true); + break; + case ExifTag.XPosition: + case ExifTag.YPosition: + case ExifTag.XResolution: + case ExifTag.YResolution: + case ExifTag.ExposureTime: + case ExifTag.FNumber: + case ExifTag.CompressedBitsPerPixel: + case ExifTag.ApertureValue: + case ExifTag.MaxApertureValue: + case ExifTag.SubjectDistance: + case ExifTag.FocalLength: + case ExifTag.FlashEnergy: + case ExifTag.FocalPlaneXResolution: + case ExifTag.FocalPlaneYResolution: + case ExifTag.ExposureIndex: + case ExifTag.DigitalZoomRatio: + case ExifTag.GPSAltitude: + case ExifTag.GPSDOP: + case ExifTag.GPSSpeed: + case ExifTag.GPSTrack: + case ExifTag.GPSImgDirection: + case ExifTag.GPSDestBearing: + case ExifTag.GPSDestDistance: + exifValue = new ExifValue(tag, ExifDataType.Rational, false); + break; + + case ExifTag.BitsPerSample: + case ExifTag.MinSampleValue: + case ExifTag.MaxSampleValue: + case ExifTag.GrayResponseCurve: + case ExifTag.ColorMap: + case ExifTag.ExtraSamples: + case ExifTag.PageNumber: + case ExifTag.TransferFunction: + case ExifTag.Predictor: + case ExifTag.HalftoneHints: + case ExifTag.SampleFormat: + case ExifTag.TransferRange: + case ExifTag.DefaultImageColor: + case ExifTag.JPEGLosslessPredictors: + case ExifTag.JPEGPointTransforms: + case ExifTag.YCbCrSubsampling: + case ExifTag.ISOSpeedRatings: + case ExifTag.SubjectArea: + case ExifTag.SubjectLocation: + exifValue = new ExifValue(tag, ExifDataType.Short, true); + break; + case ExifTag.Compression: + case ExifTag.PhotometricInterpretation: + case ExifTag.Threshholding: + case ExifTag.CellWidth: + case ExifTag.CellLength: + case ExifTag.FillOrder: + case ExifTag.Orientation: + case ExifTag.SamplesPerPixel: + case ExifTag.PlanarConfiguration: + case ExifTag.GrayResponseUnit: + case ExifTag.ResolutionUnit: + case ExifTag.CleanFaxData: + case ExifTag.InkSet: + case ExifTag.NumberOfInks: + case ExifTag.DotRange: + case ExifTag.Indexed: + case ExifTag.OPIProxy: + case ExifTag.JPEGProc: + case ExifTag.JPEGRestartInterval: + case ExifTag.YCbCrPositioning: + case ExifTag.ExposureProgram: + case ExifTag.MeteringMode: + case ExifTag.LightSource: + case ExifTag.Flash: + case ExifTag.ColorSpace: + case ExifTag.FocalPlaneResolutionUnit: + case ExifTag.SensingMethod: + case ExifTag.CustomRendered: + case ExifTag.ExposureMode: + case ExifTag.WhiteBalance: + case ExifTag.FocalLengthIn35mmFilm: + case ExifTag.SceneCaptureType: + case ExifTag.GainControl: + case ExifTag.Contrast: + case ExifTag.Saturation: + case ExifTag.Sharpness: + case ExifTag.SubjectDistanceRange: + case ExifTag.GPSDifferential: + exifValue = new ExifValue(tag, ExifDataType.Short, false); + break; + + case ExifTag.Decode: + exifValue = new ExifValue(tag, ExifDataType.SignedRational, true); + break; + case ExifTag.ShutterSpeedValue: + case ExifTag.BrightnessValue: + case ExifTag.ExposureBiasValue: + exifValue = new ExifValue(tag, ExifDataType.SignedRational, false); + break; + + case ExifTag.JPEGTables: + case ExifTag.OECF: + case ExifTag.ExifVersion: + case ExifTag.ComponentsConfiguration: + case ExifTag.MakerNote: + case ExifTag.UserComment: + case ExifTag.FlashpixVersion: + case ExifTag.SpatialFrequencyResponse: + case ExifTag.CFAPattern: + case ExifTag.DeviceSettingDescription: + case ExifTag.GPSProcessingMethod: + case ExifTag.GPSAreaInformation: + exifValue = new ExifValue(tag, ExifDataType.Undefined, true); + break; + case ExifTag.FileSource: + case ExifTag.SceneType: + exifValue = new ExifValue(tag, ExifDataType.Undefined, false); + break; + + case ExifTag.StripOffsets: + case ExifTag.TileByteCounts: + case ExifTag.ImageLayer: + exifValue = CreateNumber(tag, type, true); + break; + case ExifTag.ImageWidth: + case ExifTag.ImageLength: + case ExifTag.TileWidth: + case ExifTag.TileLength: + case ExifTag.BadFaxLines: + case ExifTag.ConsecutiveBadFaxLines: + case ExifTag.PixelXDimension: + case ExifTag.PixelYDimension: + exifValue = CreateNumber(tag, type, false); + break; + + default: + throw new NotImplementedException(); + } + + exifValue.Value = value; + return exifValue; + } + + internal static uint GetSize(ExifDataType dataType) + { + switch (dataType) + { + case ExifDataType.Ascii: + case ExifDataType.Byte: + case ExifDataType.SignedByte: + case ExifDataType.Undefined: + return 1; + case ExifDataType.Short: + case ExifDataType.SignedShort: + return 2; + case ExifDataType.Long: + case ExifDataType.SignedLong: + case ExifDataType.SingleFloat: + return 4; + case ExifDataType.DoubleFloat: + case ExifDataType.Rational: + case ExifDataType.SignedRational: + return 8; + default: + throw new NotImplementedException(dataType.ToString()); + } + } + + private void CheckValue(object value) + { + if (value == null) + return; + + Type type = value.GetType(); + + if (DataType == ExifDataType.Ascii) + { + Guard.IsTrue(type == typeof(string), nameof(value), "Value should be a string."); + return; + } + + if (type.IsArray) + { + Guard.IsTrue(IsArray, nameof(value), "Value should not be an array."); + type = type.GetElementType(); + } + else + { + Guard.IsFalse(IsArray, nameof(value), "Value should not be an array."); + } + + switch (DataType) + { + case ExifDataType.Byte: + Guard.IsTrue(type == typeof(byte), nameof(value), $"Value should be a byte{(IsArray ? " array." : ".")}"); + break; + case ExifDataType.DoubleFloat: + case ExifDataType.Rational: + case ExifDataType.SignedRational: + Guard.IsTrue(type == typeof(double), nameof(value), $"Value should be a double{(IsArray ? " array." : ".")}"); + break; + case ExifDataType.Long: + Guard.IsTrue(type == typeof(uint), nameof(value), $"Value should be an unsigned int{(IsArray ? " array." : ".")}"); + break; + case ExifDataType.Short: + Guard.IsTrue(type == typeof(ushort), nameof(value), $"Value should be an unsigned short{(IsArray ? " array." : ".")}"); + break; + case ExifDataType.SignedByte: + Guard.IsTrue(type == typeof(sbyte), nameof(value), $"Value should be a signed byte{(IsArray ? " array." : ".")}"); + break; + case ExifDataType.SignedLong: + Guard.IsTrue(type == typeof(int), nameof(value), $"Value should be an int{(IsArray ? " array." : ".")}"); + break; + case ExifDataType.SignedShort: + Guard.IsTrue(type == typeof(short), nameof(value), $"Value should be a short{(IsArray ? " array." : ".")}"); + break; + case ExifDataType.SingleFloat: + Guard.IsTrue(type == typeof(float), nameof(value), $"Value should be a float{(IsArray ? " array." : ".")}"); + break; + case ExifDataType.Undefined: + Guard.IsTrue(type == typeof(byte), nameof(value), "Value should be a byte array."); + break; + default: + throw new NotImplementedException(); + } + } + + private static ExifValue CreateNumber(ExifTag tag, Type type, bool isArray) + { + if (type == null || type == typeof(ushort)) + return new ExifValue(tag, ExifDataType.Short, isArray); + else if (type == typeof(short)) + return new ExifValue(tag, ExifDataType.SignedShort, isArray); + else if (type == typeof(uint)) + return new ExifValue(tag, ExifDataType.Long, isArray); + else + return new ExifValue(tag, ExifDataType.SignedLong, isArray); + } + + private string ToString(object value) + { + switch (DataType) + { + case ExifDataType.Ascii: + return (string)value; + case ExifDataType.Byte: + return ((byte)value).ToString("X2", CultureInfo.InvariantCulture); + case ExifDataType.DoubleFloat: + return ((double)value).ToString(CultureInfo.InvariantCulture); + case ExifDataType.Long: + return ((uint)value).ToString(CultureInfo.InvariantCulture); + case ExifDataType.Rational: + return ((double)value).ToString(CultureInfo.InvariantCulture); + case ExifDataType.Short: + return ((ushort)value).ToString(CultureInfo.InvariantCulture); + case ExifDataType.SignedByte: + return ((sbyte)value).ToString("X2", CultureInfo.InvariantCulture); + case ExifDataType.SignedLong: + return ((int)value).ToString(CultureInfo.InvariantCulture); + case ExifDataType.SignedRational: + return ((double)value).ToString(CultureInfo.InvariantCulture); + case ExifDataType.SignedShort: + return ((short)value).ToString(CultureInfo.InvariantCulture); + case ExifDataType.SingleFloat: + return ((float)value).ToString(CultureInfo.InvariantCulture); + case ExifDataType.Undefined: + return ((byte)value).ToString("X2", CultureInfo.InvariantCulture); + default: + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs b/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs new file mode 100644 index 000000000..cee38cda5 --- /dev/null +++ b/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs @@ -0,0 +1,407 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Text; + + internal sealed class ExifWriter + { + private static readonly ExifTag[] IfdTags = new ExifTag[93] + { + ExifTag.ImageWidth, ExifTag.ImageLength, ExifTag.BitsPerSample, ExifTag.Compression, + ExifTag.PhotometricInterpretation, ExifTag.Threshholding, ExifTag.CellWidth, + ExifTag.CellLength, ExifTag.FillOrder,ExifTag.ImageDescription, ExifTag.Make, + ExifTag.Model, ExifTag.StripOffsets, ExifTag.Orientation, ExifTag.SamplesPerPixel, + ExifTag.RowsPerStrip, ExifTag.StripByteCounts, ExifTag.MinSampleValue, + ExifTag.MaxSampleValue, ExifTag.XResolution, ExifTag.YResolution, + ExifTag.PlanarConfiguration, ExifTag.FreeOffsets, ExifTag.FreeByteCounts, + ExifTag.GrayResponseUnit, ExifTag.GrayResponseCurve, ExifTag.ResolutionUnit, + ExifTag.Software, ExifTag.DateTime, ExifTag.Artist, ExifTag.HostComputer, + ExifTag.ColorMap, ExifTag.ExtraSamples, ExifTag.Copyright, ExifTag.DocumentName, + ExifTag.PageName, ExifTag.XPosition, ExifTag.YPosition, ExifTag.T4Options, + ExifTag.T6Options, ExifTag.PageNumber, ExifTag.TransferFunction, ExifTag.Predictor, + ExifTag.WhitePoint, ExifTag.PrimaryChromaticities, ExifTag.HalftoneHints, + ExifTag.TileWidth, ExifTag.TileLength, ExifTag.TileOffsets, ExifTag.TileByteCounts, + ExifTag.BadFaxLines, ExifTag.CleanFaxData, ExifTag.ConsecutiveBadFaxLines, + ExifTag.InkSet, ExifTag.InkNames, ExifTag.NumberOfInks, ExifTag.DotRange, + ExifTag.TargetPrinter, ExifTag.SampleFormat, ExifTag.SMinSampleValue, + ExifTag.SMaxSampleValue, ExifTag.TransferRange, ExifTag.ClipPath, + ExifTag.XClipPathUnits, ExifTag.YClipPathUnits, ExifTag.Indexed, ExifTag.JPEGTables, + ExifTag.OPIProxy, ExifTag.ProfileType, ExifTag.FaxProfile, ExifTag.CodingMethods, + ExifTag.VersionYear, ExifTag.ModeNumber, ExifTag.Decode, ExifTag.DefaultImageColor, + ExifTag.JPEGProc, ExifTag.JPEGInterchangeFormat, ExifTag.JPEGInterchangeFormatLength, + ExifTag.JPEGRestartInterval, ExifTag.JPEGLosslessPredictors, + ExifTag.JPEGPointTransforms, ExifTag.JPEGQTables, ExifTag.JPEGDCTables, + ExifTag.JPEGACTables, ExifTag.YCbCrCoefficients, ExifTag.YCbCrSubsampling, + ExifTag.YCbCrSubsampling, ExifTag.YCbCrPositioning, ExifTag.ReferenceBlackWhite, + ExifTag.StripRowCounts, ExifTag.XMP, ExifTag.ImageID, ExifTag.ImageLayer + }; + + private static readonly ExifTag[] ExifTags = new ExifTag[56] + { + ExifTag.ExposureTime, ExifTag.FNumber, ExifTag.ExposureProgram, + ExifTag.SpectralSensitivity, ExifTag.ISOSpeedRatings, ExifTag.OECF, + ExifTag.ExifVersion, ExifTag.DateTimeOriginal, ExifTag.DateTimeDigitized, + ExifTag.ComponentsConfiguration, ExifTag.CompressedBitsPerPixel, + ExifTag.ShutterSpeedValue, ExifTag.ApertureValue, ExifTag.BrightnessValue, + ExifTag.ExposureBiasValue, ExifTag.MaxApertureValue, ExifTag.SubjectDistance, + ExifTag.MeteringMode, ExifTag.LightSource, ExifTag.Flash, ExifTag.FocalLength, + ExifTag.SubjectArea, ExifTag.MakerNote, ExifTag.UserComment, ExifTag.SubsecTime, + ExifTag.SubsecTimeOriginal, ExifTag.SubsecTimeDigitized, ExifTag.FlashpixVersion, + ExifTag.ColorSpace, ExifTag.PixelXDimension, ExifTag.PixelYDimension, + ExifTag.RelatedSoundFile, ExifTag.FlashEnergy, ExifTag.SpatialFrequencyResponse, + ExifTag.FocalPlaneXResolution, ExifTag.FocalPlaneYResolution, + ExifTag.FocalPlaneResolutionUnit, ExifTag.SubjectLocation, ExifTag.ExposureIndex, + ExifTag.SensingMethod, ExifTag.FileSource, ExifTag.SceneType, ExifTag.CFAPattern, + ExifTag.CustomRendered, ExifTag.ExposureMode, ExifTag.WhiteBalance, + ExifTag.DigitalZoomRatio, ExifTag.FocalLengthIn35mmFilm, ExifTag.SceneCaptureType, + ExifTag.GainControl, ExifTag.Contrast, ExifTag.Saturation, ExifTag.Sharpness, + ExifTag.DeviceSettingDescription, ExifTag.SubjectDistanceRange, ExifTag.ImageUniqueID + }; + + private static readonly ExifTag[] GPSTags = new ExifTag[31] + { + ExifTag.GPSVersionID, ExifTag.GPSLatitudeRef, ExifTag.GPSLatitude, + ExifTag.GPSLongitudeRef, ExifTag.GPSLongitude, ExifTag.GPSAltitudeRef, + ExifTag.GPSAltitude, ExifTag.GPSTimestamp, ExifTag.GPSSatellites, ExifTag.GPSStatus, + ExifTag.GPSMeasureMode, ExifTag.GPSDOP, ExifTag.GPSSpeedRef, ExifTag.GPSSpeed, + ExifTag.GPSTrackRef, ExifTag.GPSTrack, ExifTag.GPSImgDirectionRef, + ExifTag.GPSImgDirection, ExifTag.GPSMapDatum, ExifTag.GPSDestLatitudeRef, + ExifTag.GPSDestLatitude, ExifTag.GPSDestLongitudeRef, ExifTag.GPSDestLongitude, + ExifTag.GPSDestBearingRef, ExifTag.GPSDestBearing, ExifTag.GPSDestDistanceRef, + ExifTag.GPSDestDistance, ExifTag.GPSProcessingMethod, ExifTag.GPSAreaInformation, + ExifTag.GPSDateStamp, ExifTag.GPSDifferential + }; + + private const int StartIndex = 6; + + private ExifParts allowedParts; + private bool bestPrecision; + private Collection values; + private Collection dataOffsets; + private Collection ifdIndexes; + private Collection exifIndexes; + private Collection gpsIndexes; + + public ExifWriter(Collection values, ExifParts allowedParts, bool bestPrecision) + { + this.values = values; + this.allowedParts = allowedParts; + this.bestPrecision = bestPrecision; + + this.ifdIndexes = GetIndexes(ExifParts.IfdTags, IfdTags); + this.exifIndexes = GetIndexes(ExifParts.ExifTags, ExifTags); + this.gpsIndexes = GetIndexes(ExifParts.GPSTags, GPSTags); + } + + public byte[] GetData() + { + uint length = 0; + int exifIndex = -1; + int gpsIndex = -1; + + if (this.exifIndexes.Count > 0) + exifIndex = (int)GetIndex(this.ifdIndexes, ExifTag.SubIFDOffset); + + if (this.gpsIndexes.Count > 0) + gpsIndex = (int)GetIndex(this.ifdIndexes, ExifTag.GPSIFDOffset); + + uint ifdLength = 2 + GetLength(this.ifdIndexes) + 4; + uint exifLength = GetLength(this.exifIndexes); + uint gpsLength = GetLength(this.gpsIndexes); + + if (exifLength > 0) + exifLength += 2; + + if (gpsLength > 0) + gpsLength += 2; + + length = ifdLength + exifLength + gpsLength; + + if (length == 6) + return null; + + length += 10 + 4 + 2; + + byte[] result = new byte[length]; + result[0] = (byte)'E'; + result[1] = (byte)'x'; + result[2] = (byte)'i'; + result[3] = (byte)'f'; + result[4] = 0x00; + result[5] = 0x00; + result[6] = (byte)'I'; + result[7] = (byte)'I'; + result[8] = 0x2A; + result[9] = 0x00; + + int i = 10; + uint ifdOffset = ((uint)i - StartIndex) + 4; + uint thumbnailOffset = ifdOffset + ifdLength + exifLength + gpsLength; + + if (exifLength > 0) + this.values[exifIndex].Value = (ifdOffset + ifdLength); + + if (gpsLength > 0) + this.values[gpsIndex].Value = (ifdOffset + ifdLength + exifLength); + + i = Write(BitConverter.GetBytes(ifdOffset), result, i); + i = WriteHeaders(this.ifdIndexes, result, i); + i = Write(BitConverter.GetBytes(thumbnailOffset), result, i); + i = WriteData(this.ifdIndexes, result, i); + + if (exifLength > 0) + { + i = WriteHeaders(this.exifIndexes, result, i); + i = WriteData(this.exifIndexes, result, i); + } + + if (gpsLength > 0) + { + i = WriteHeaders(this.gpsIndexes, result, i); + i = WriteData(this.gpsIndexes, result, i); + } + + Write(BitConverter.GetBytes((ushort)0), result, i); + + return result; + } + + private int GetIndex(Collection indexes, ExifTag tag) + { + foreach (int index in indexes) + { + if (this.values[index].Tag == tag) + return index; + } + + int newIndex = this.values.Count; + indexes.Add(newIndex); + this.values.Add(ExifValue.Create(tag, null)); + return newIndex; + } + + private Collection GetIndexes(ExifParts part, ExifTag[] tags) + { + if (((int)this.allowedParts & (int)part) == 0) + return new Collection(); + + Collection result = new Collection(); + for (int i = 0; i < this.values.Count; i++) + { + ExifValue value = this.values[i]; + + if (!value.HasValue) + continue; + + int index = Array.IndexOf(tags, value.Tag); + if (index > -1) + result.Add(i); + } + + return result; + } + + private uint GetLength(IEnumerable indexes) + { + uint length = 0; + + foreach (int index in indexes) + { + uint valueLength = (uint)this.values[index].Length; + + if (valueLength > 4) + length += 12 + valueLength; + else + length += 12; + } + + return length; + } + + private static int Write(byte[] source, byte[] destination, int offset) + { + Buffer.BlockCopy(source, 0, destination, offset, source.Length); + + return offset + source.Length; + } + + private int WriteArray(ExifValue value, byte[] destination, int offset) + { + if (value.DataType == ExifDataType.Ascii) + return WriteValue(ExifDataType.Ascii, value.Value, destination, offset); + + int newOffset = offset; + foreach (object obj in (Array)value.Value) + newOffset = WriteValue(value.DataType, obj, destination, newOffset); + + return newOffset; + } + + private int WriteData(Collection indexes, byte[] destination, int offset) + { + if (this.dataOffsets.Count == 0) + return offset; + + int newOffset = offset; + + int i = 0; + foreach (int index in indexes) + { + ExifValue value = this.values[index]; + if (value.Length > 4) + { + Write(BitConverter.GetBytes(newOffset - StartIndex), destination, this.dataOffsets[i++]); + newOffset = WriteValue(value, destination, newOffset); + } + } + + return newOffset; + } + + private int WriteHeaders(Collection indexes, byte[] destination, int offset) + { + this.dataOffsets = new Collection(); + + int newOffset = Write(BitConverter.GetBytes((ushort)indexes.Count), destination, offset); + + if (indexes.Count == 0) + return newOffset; + + foreach (int index in indexes) + { + ExifValue value = this.values[index]; + newOffset = Write(BitConverter.GetBytes((ushort)value.Tag), destination, newOffset); + newOffset = Write(BitConverter.GetBytes((ushort)value.DataType), destination, newOffset); + newOffset = Write(BitConverter.GetBytes((uint)value.NumberOfComponents), destination, newOffset); + + if (value.Length > 4) + this.dataOffsets.Add(newOffset); + else + WriteValue(value, destination, newOffset); + + newOffset += 4; + } + + return newOffset; + } + + private int WriteRational(double value, byte[] destination, int offset) + { + uint numerator = 1; + uint denominator = 1; + + if (double.IsPositiveInfinity(value)) + denominator = 0; + else if (double.IsNegativeInfinity(value)) + denominator = 0; + else + { + double val = Math.Abs(value); + double df = numerator / denominator; + double epsilon = this.bestPrecision ? double.Epsilon : .000001; + + while (Math.Abs(df - val) > epsilon) + { + if (df < val) + numerator++; + else + { + denominator++; + numerator = (uint)(val * denominator); + } + + df = numerator / (double)denominator; + } + } + + Write(BitConverter.GetBytes(numerator), destination, offset); + Write(BitConverter.GetBytes(denominator), destination, offset + 4); + + return offset + 8; + } + + private int WriteSignedRational(double value, byte[] destination, int offset) + { + int numerator = 1; + int denominator = 1; + + if (double.IsPositiveInfinity(value)) + denominator = 0; + else if (double.IsNegativeInfinity(value)) + denominator = 0; + else + { + double val = Math.Abs(value); + double df = numerator / denominator; + double epsilon = this.bestPrecision ? double.Epsilon : .000001; + + while (Math.Abs(df - val) > epsilon) + { + if (df < val) + numerator++; + else + { + denominator++; + numerator = (int)(val * denominator); + } + + df = numerator / (double)denominator; + } + } + + Write(BitConverter.GetBytes(numerator * (value < 0.0 ? -1 : 1)), destination, offset); + Write(BitConverter.GetBytes(denominator), destination, offset + 4); + + return offset + 8; + } + + private int WriteValue(ExifDataType dataType, object value, byte[] destination, int offset) + { + switch (dataType) + { + case ExifDataType.Ascii: + return Write(Encoding.UTF8.GetBytes((string)value), destination, offset); + case ExifDataType.Byte: + case ExifDataType.Undefined: + destination[offset] = (byte)value; + return offset + 1; + case ExifDataType.DoubleFloat: + return Write(BitConverter.GetBytes((double)value), destination, offset); + case ExifDataType.Short: + return Write(BitConverter.GetBytes((ushort)value), destination, offset); + case ExifDataType.Long: + return Write(BitConverter.GetBytes((uint)value), destination, offset); + case ExifDataType.Rational: + return WriteRational((double)value, destination, offset); + case ExifDataType.SignedByte: + destination[offset] = unchecked((byte)((sbyte)value)); + return offset + 1; + case ExifDataType.SignedLong: + return Write(BitConverter.GetBytes((int)value), destination, offset); + case ExifDataType.SignedShort: + return Write(BitConverter.GetBytes((short)value), destination, offset); + case ExifDataType.SignedRational: + return WriteSignedRational((double)value, destination, offset); + case ExifDataType.SingleFloat: + return Write(BitConverter.GetBytes((float)value), destination, offset); + default: + throw new NotImplementedException(); + } + } + + private int WriteValue(ExifValue value, byte[] destination, int offset) + { + if (value.IsArray && value.DataType != ExifDataType.Ascii) + return WriteArray(value, destination, offset); + else + return WriteValue(value.DataType, value.Value, destination, offset); + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Profiles/Exif/README.md b/src/ImageProcessorCore/Profiles/Exif/README.md new file mode 100644 index 000000000..b6e27b70c --- /dev/null +++ b/src/ImageProcessorCore/Profiles/Exif/README.md @@ -0,0 +1,3 @@ +Adapted from Magick.NET: + +https://github.com/dlemstra/Magick.NET/tree/784e23b1f5c824fc03d4b95d3387b3efe1ed510b/Magick.NET/Core/Profiles/Exif \ No newline at end of file From ecb67ee76304d51d68c7d9dd4fc24bd0023d5e16 Mon Sep 17 00:00:00 2001 From: dirk Date: Sat, 13 Aug 2016 11:06:46 +0200 Subject: [PATCH 4/4] The JPEG encoder and decoder will now read and write the Exif profile and added unit tests to verify this. Former-commit-id: 5ea83609c02c71e457f1e083501b30dfb86af88c Former-commit-id: a594b52cfb4b1fb96d39fa1241c1db1db42ee401 Former-commit-id: 7276cfdbaa100a6ba0d705c311e6b439b3d11cfd --- .../Jpg/JpegDecoderCore.cs.REMOVED.git-id | 2 +- .../Formats/Jpg/JpegEncoderCore.cs | 39 ++- .../Profiles/Exif/ExifProfileTests.cs | 306 ++++++++++++++++++ .../Profiles/Exif/ExifValueTests.cs | 50 +++ 4 files changed, 393 insertions(+), 4 deletions(-) create mode 100644 tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs create mode 100644 tests/ImageProcessorCore.Tests/Profiles/Exif/ExifValueTests.cs diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id b/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id index d5d55bcd2..ae3ffc69d 100644 --- a/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id +++ b/src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id @@ -1 +1 @@ -25b2ab2a0acfb874ca8ac8ae979fc6b1bc9bf466 \ No newline at end of file +09f4618eaade2900f7174b9ab400deb0a25bd813 \ No newline at end of file diff --git a/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs b/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs index 48c65caad..2368a498d 100644 --- a/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs +++ b/src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs @@ -488,10 +488,9 @@ namespace ImageProcessorCore.Formats int componentCount = 3; // Write the Start Of Image marker. - double densityX = image.HorizontalResolution; - double densityY = image.VerticalResolution; + WriteApplicationHeader((short)image.HorizontalResolution, (short)image.VerticalResolution); - WriteApplicationHeader((short)densityX, (short)densityY); + WriteProfiles(image); // Write the quantization tables. this.WriteDQT(); @@ -571,6 +570,40 @@ namespace ImageProcessorCore.Formats this.outputStream.Write(this.buffer, 0, 4); } + private void WriteProfiles(Image image) + where T : IPackedVector + where TP : struct + { + WriteProfile(image.ExifProfile); + } + + private void WriteProfile(ExifProfile exifProfile) + { + if (exifProfile == null) + { + return; + } + + byte[] data = exifProfile.ToByteArray(); + if (data == null || data.Length == 0) + { + return; + } + + if (data.Length > 65533) + { + throw new ImageFormatException("Exif profile size exceeds limit."); + } + + this.buffer[0] = JpegConstants.Markers.XFF; + this.buffer[1] = JpegConstants.Markers.APP1; // Application Marker + this.buffer[2] = (byte)((data.Length >> 8) & 0xFF); + this.buffer[3] = (byte)(data.Length & 0xFF); + + this.outputStream.Write(this.buffer, 0, 4); + this.outputStream.Write(data, 0, data.Length); + } + /// /// Writes the Define Quantization Marker and tables. /// diff --git a/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs b/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs new file mode 100644 index 000000000..908d49b59 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs @@ -0,0 +1,306 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + using System; + using System.Collections; + using System.IO; + using System.Linq; + using System.Text; + using Xunit; + + public class ExifProfileTests + { + [Fact] + public void Constructor() + { + using (FileStream stream = File.OpenRead(TestImages.Jpg.Calliphora)) + { + Image image = new Image(stream); + + Assert.Null(image.ExifProfile); + + image.ExifProfile = new ExifProfile(); + image.ExifProfile.SetValue(ExifTag.Copyright, "Dirk Lemstra"); + + image = WriteAndRead(image); + + Assert.NotNull(image.ExifProfile); + Assert.Equal(1, image.ExifProfile.Values.Count()); + + ExifValue value = image.ExifProfile.Values.FirstOrDefault(val => val.Tag == ExifTag.Copyright); + TestValue(value, "Dirk Lemstra"); + } + } + + [Fact] + public void ConstructorEmpty() + { + new ExifProfile((byte[])null); + new ExifProfile(new byte[] { }); + } + + [Fact] + public void ConstructorCopy() + { + Assert.Throws(() => { new ExifProfile((ExifProfile)null); }); + + ExifProfile profile = GetExifProfile(); + + ExifProfile clone = new ExifProfile(profile); + TestProfile(clone); + + profile.SetValue(ExifTag.ColorSpace, (ushort)2); + + clone = new ExifProfile(profile); + TestProfile(clone); + } + + [Fact] + public void WriteFraction() + { + using (MemoryStream memStream = new MemoryStream()) + { + double exposureTime = 1.0 / 1600; + + ExifProfile profile = GetExifProfile(); + + profile.SetValue(ExifTag.ExposureTime, exposureTime); + + Image image = new Image(1, 1); + image.ExifProfile = profile; + + image.SaveAsJpeg(memStream); + + memStream.Position = 0; + image = new Image(memStream); + + profile = image.ExifProfile; + Assert.NotNull(profile); + + ExifValue value = profile.GetValue(ExifTag.ExposureTime); + Assert.NotNull(value); + Assert.NotEqual(exposureTime, value.Value); + + memStream.Position = 0; + profile = GetExifProfile(); + + profile.SetValue(ExifTag.ExposureTime, exposureTime); + profile.BestPrecision = true; + image.ExifProfile = profile; + + image.SaveAsJpeg(memStream); + + memStream.Position = 0; + image = new Image(memStream); + + profile = image.ExifProfile; + Assert.NotNull(profile); + + value = profile.GetValue(ExifTag.ExposureTime); + TestValue(value, exposureTime); + } + } + + [Fact] + public void ReadWriteInfinity() + { + using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan)) + { + Image image = new Image(stream); + image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, double.PositiveInfinity); + + image = WriteAndRead(image); + ExifValue value = image.ExifProfile.GetValue(ExifTag.ExposureBiasValue); + Assert.NotNull(value); + Assert.Equal(double.PositiveInfinity, value.Value); + + image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, double.NegativeInfinity); + + image = WriteAndRead(image); + value = image.ExifProfile.GetValue(ExifTag.ExposureBiasValue); + Assert.NotNull(value); + Assert.Equal(double.NegativeInfinity, value.Value); + + image.ExifProfile.SetValue(ExifTag.FlashEnergy, double.NegativeInfinity); + + image = WriteAndRead(image); + value = image.ExifProfile.GetValue(ExifTag.FlashEnergy); + Assert.NotNull(value); + Assert.Equal(double.PositiveInfinity, value.Value); + } + } + + [Fact] + public void SetValue() + { + double[] latitude = new double[] { 12.3, 4.56, 789.0 }; + + using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan)) + { + Image image = new Image(stream); + image.ExifProfile.SetValue(ExifTag.Software, "ImageProcessorCore"); + + ExifValue value = image.ExifProfile.GetValue(ExifTag.Software); + TestValue(value, "ImageProcessorCore"); + + Assert.Throws(() => { value.Value = 15; }); + + image.ExifProfile.SetValue(ExifTag.ShutterSpeedValue, 75.55); + + value = image.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); + TestValue(value, 75.55); + + Assert.Throws(() => { value.Value = 75; }); + + image.ExifProfile.SetValue(ExifTag.XResolution, 150.0); + + value = image.ExifProfile.GetValue(ExifTag.XResolution); + TestValue(value, 150.0); + + Assert.Throws(() => { value.Value = "ImageProcessorCore"; }); + + image.ExifProfile.SetValue(ExifTag.ReferenceBlackWhite, null); + + value = image.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite); + TestValue(value, (string)null); + + image.ExifProfile.SetValue(ExifTag.GPSLatitude, latitude); + + value = image.ExifProfile.GetValue(ExifTag.GPSLatitude); + TestValue(value, latitude); + + image = WriteAndRead(image); + + Assert.NotNull(image.ExifProfile); + Assert.Equal(17, image.ExifProfile.Values.Count()); + + value = image.ExifProfile.GetValue(ExifTag.Software); + TestValue(value, "ImageProcessorCore"); + + value = image.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); + TestValue(value, 75.55); + + value = image.ExifProfile.GetValue(ExifTag.XResolution); + TestValue(value, 150.0); + + value = image.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite); + Assert.Null(value); + + value = image.ExifProfile.GetValue(ExifTag.GPSLatitude); + TestValue(value, latitude); + + image.ExifProfile.Parts = ExifParts.ExifTags; + + image = WriteAndRead(image); + + Assert.NotNull(image.ExifProfile); + Assert.Equal(8, image.ExifProfile.Values.Count()); + + Assert.NotNull(image.ExifProfile.GetValue(ExifTag.ColorSpace)); + Assert.True(image.ExifProfile.RemoveValue(ExifTag.ColorSpace)); + Assert.False(image.ExifProfile.RemoveValue(ExifTag.ColorSpace)); + Assert.Null(image.ExifProfile.GetValue(ExifTag.ColorSpace)); + + Assert.Equal(7, image.ExifProfile.Values.Count()); + } + } + + [Fact] + public void Values() + { + ExifProfile profile = GetExifProfile(); + + TestProfile(profile); + + var thumbnail = profile.CreateThumbnail(); + Assert.NotNull(thumbnail); + Assert.Equal(256, thumbnail.Width); + Assert.Equal(170, thumbnail.Height); + } + + [Fact] + public void WriteTooLargeProfile() + { + StringBuilder junk = new StringBuilder(); + for (int i = 0; i < 65500; i++) + junk.Append("I"); + + Image image = new Image(100, 100); + image.ExifProfile = new ExifProfile(); + image.ExifProfile.SetValue(ExifTag.ImageDescription, junk.ToString()); + + using (MemoryStream memStream = new MemoryStream()) + { + Assert.Throws(() => image.SaveAsJpeg(memStream)); + } + } + + private static ExifProfile GetExifProfile() + { + using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan)) + { + Image image = new Image(stream); + + ExifProfile profile = image.ExifProfile; + Assert.NotNull(profile); + + return profile; + } + } + + private static Image WriteAndRead(Image image) + { + using (MemoryStream memStream = new MemoryStream()) + { + image.SaveAsJpeg(memStream); + + memStream.Position = 0; + return new Image(memStream); + } + } + + private static void TestProfile(ExifProfile profile) + { + Assert.NotNull(profile); + + Assert.Equal(16, profile.Values.Count()); + + foreach (ExifValue value in profile.Values) + { + Assert.NotNull(value.Value); + + if (value.Tag == ExifTag.Software) + Assert.Equal("Windows Photo Editor 10.0.10011.16384", value.ToString()); + + if (value.Tag == ExifTag.XResolution) + Assert.Equal(300.0, value.Value); + + if (value.Tag == ExifTag.PixelXDimension) + Assert.Equal(2338U, value.Value); + } + } + + private static void TestValue(ExifValue value, string expected) + { + Assert.NotNull(value); + Assert.Equal(expected, value.Value); + } + + private static void TestValue(ExifValue value, double expected) + { + Assert.NotNull(value); + Assert.Equal(expected, value.Value); + } + + private static void TestValue(ExifValue value, double[] expected) + { + Assert.NotNull(value); + + Assert.Equal(expected, (ICollection)value.Value); + } + } +} diff --git a/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifValueTests.cs b/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifValueTests.cs new file mode 100644 index 000000000..3ee09c23d --- /dev/null +++ b/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifValueTests.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + using System.IO; + using System.Linq; + using Xunit; + + public class ExifValueTests + { + private static ExifValue GetExifValue() + { + using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan)) + { + Image image = new Image(stream); + + ExifProfile profile = image.ExifProfile; + Assert.NotNull(profile); + + return profile.Values.First(); + } + } + + [Fact] + public void IEquatable() + { + ExifValue first = GetExifValue(); + ExifValue second = GetExifValue(); + + Assert.True(first == second); + Assert.True(first.Equals(second)); + Assert.True(first.Equals((object)second)); + } + + [Fact] + public void Properties() + { + ExifValue value = GetExifValue(); + + Assert.Equal(ExifDataType.Ascii, value.DataType); + Assert.Equal(ExifTag.GPSDOP, value.Tag); + Assert.Equal(false, value.IsArray); + Assert.Equal("Windows Photo Editor 10.0.10011.16384", value.ToString()); + Assert.Equal("Windows Photo Editor 10.0.10011.16384", value.Value); + } + } +}