diff --git a/src/ImageSharp/Metadata/Profiles/Exif/DC-008-Translation-2019-E.pdf b/src/ImageSharp/Metadata/Profiles/Exif/DC-008-Translation-2019-E.pdf new file mode 100644 index 000000000..9be0c8402 Binary files /dev/null and b/src/ImageSharp/Metadata/Profiles/Exif/DC-008-Translation-2019-E.pdf differ diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index 630ea6e0d..f98a1f3c7 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -143,15 +143,8 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// The data type of the tag. public IExifValue GetValue(ExifTag tag) { - foreach (IExifValue exifValue in this.Values) - { - if (exifValue.Tag == tag) - { - return (IExifValue)exifValue; - } - } - - return null; + IExifValue value = this.GetValueInternal(tag); + return value is null ? null : (IExifValue)value; } /// @@ -184,25 +177,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// The value. /// The data type of the tag. public void SetValue(ExifTag tag, TValueType value) - { - foreach (IExifValue exifValue in this.Values) - { - if (exifValue.Tag == tag) - { - exifValue.TrySetValue(value); - return; - } - } - - ExifValue newExifValue = ExifValues.Create(tag); - if (newExifValue is null) - { - throw new NotSupportedException(); - } - - newExifValue.TrySetValue(value); - this.values.Add(newExifValue); - } + => this.SetValueInternal(tag, value); /// /// Converts this instance to a byte array. @@ -227,6 +202,50 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public ExifProfile DeepClone() => new ExifProfile(this); + /// + /// Returns the value with the specified tag. + /// + /// The tag of the exif value. + /// The value with the specified tag. + internal IExifValue GetValueInternal(ExifTag tag) + { + foreach (IExifValue exifValue in this.Values) + { + if (exifValue.Tag == tag) + { + return exifValue; + } + } + + return null; + } + + /// + /// Sets the value of the specified tag. + /// + /// The tag of the exif value. + /// The value. + internal void SetValueInternal(ExifTag tag, object value) + { + foreach (IExifValue exifValue in this.Values) + { + if (exifValue.Tag == tag) + { + exifValue.TrySetValue(value); + return; + } + } + + ExifValue newExifValue = ExifValues.Create(tag); + if (newExifValue is null) + { + throw new NotSupportedException(); + } + + newExifValue.TrySetValue(value); + this.values.Add(newExifValue); + } + /// /// Synchronizes the profiles with the specified metadata. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/README.md b/src/ImageSharp/Metadata/Profiles/Exif/README.md index b6e27b70c..7901527e1 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/README.md +++ b/src/ImageSharp/Metadata/Profiles/Exif/README.md @@ -1,3 +1,3 @@ -Adapted from Magick.NET: +Adapted from Magick.NET: -https://github.com/dlemstra/Magick.NET/tree/784e23b1f5c824fc03d4b95d3387b3efe1ed510b/Magick.NET/Core/Profiles/Exif \ No newline at end of file +https://github.com/dlemstra/Magick.NET diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 8bf44d764..fbd705957 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -154,83 +154,90 @@ namespace SixLabors.ImageSharp.Tests Assert.Equal(new Rational(double.PositiveInfinity), value2.Value); } - //[Theory] - //[InlineData(TestImageWriteFormat.Jpeg)] - //[InlineData(TestImageWriteFormat.Png)] - //public void SetValue(TestImageWriteFormat imageFormat) - //{ - // var latitude = new Rational[] { new Rational(12.3), new Rational(4.56), new Rational(789.0) }; - - // Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); - // image.Metadata.ExifProfile.SetValue(ExifTag.Software, "ImageSharp"); + [Theory] + [InlineData(TestImageWriteFormat.Jpeg)] + [InlineData(TestImageWriteFormat.Png)] + public void SetValue(TestImageWriteFormat imageFormat) + { + Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); + image.Metadata.ExifProfile.SetValue(ExifTag.Software, "ImageSharp"); - // IExifValue value = image.Metadata.ExifProfile.GetValue(ExifTag.Software); - // TestValue(value, "ImageSharp"); + IExifValue software = image.Metadata.ExifProfile.GetValue(ExifTag.Software); + Assert.Equal("ImageSharp", software.Value); - // Assert.Throws(() => { value.SetValue(15); }); + // ExifString can set integer values. + Assert.True(software.TrySetValue(15)); + Assert.False(software.TrySetValue(15F)); - // image.Metadata.ExifProfile.SetValue(ExifTag.ShutterSpeedValue, new SignedRational(75.55)); + image.Metadata.ExifProfile.SetValue(ExifTag.ShutterSpeedValue, new SignedRational(75.55)); - // value = image.Metadata.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); + IExifValue shutterSpeed = image.Metadata.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); - // TestValue(value, new SignedRational(7555, 100)); + Assert.Equal(new SignedRational(7555, 100), shutterSpeed.Value); + Assert.False(shutterSpeed.TrySetValue(75)); - // Assert.Throws(() => { value.WithValue(75); }); + image.Metadata.ExifProfile.SetValue(ExifTag.XResolution, new Rational(150.0)); - // image.Metadata.ExifProfile.SetValue(ExifTag.XResolution, new Rational(150.0)); + // We also need to change this value because this overrides XResolution when the image is written. + image.Metadata.HorizontalResolution = 150.0; - // // We also need to change this value because this overrides XResolution when the image is written. - // image.Metadata.HorizontalResolution = 150.0; + IExifValue xResolution = image.Metadata.ExifProfile.GetValue(ExifTag.XResolution); + Assert.Equal(new Rational(150, 1), xResolution.Value); - // value = image.Metadata.ExifProfile.GetValue(ExifTag.XResolution); - // TestValue(value, new Rational(150, 1)); + Assert.False(xResolution.TrySetValue("ImageSharp")); - // Assert.Throws(() => { value.WithValue("ImageSharp"); }); + image.Metadata.ExifProfile.SetValue(ExifTag.ReferenceBlackWhite, null); - // image.Metadata.ExifProfile.SetValue(ExifTag.ReferenceBlackWhite, null); + IExifValue referenceBlackWhite = image.Metadata.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite); + Assert.Null(referenceBlackWhite.Value); - // value = image.Metadata.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite); - // TestValue(value, (string)null); + var expectedLatitude = new Rational[] { new Rational(12.3), new Rational(4.56), new Rational(789.0) }; + image.Metadata.ExifProfile.SetValue(ExifTag.GPSLatitude, expectedLatitude); - // image.Metadata.ExifProfile.SetValue(ExifTag.GPSLatitude, latitude); + IExifValue latitude = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude); + Assert.Equal(expectedLatitude, latitude.Value); - // value = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude); - // TestValue(value, latitude); + int profileCount = image.Metadata.ExifProfile.Values.Count; + image = WriteAndRead(image, imageFormat); - // image = WriteAndRead(image, imageFormat); + Assert.NotNull(image.Metadata.ExifProfile); - // Assert.NotNull(image.Metadata.ExifProfile); - // Assert.Equal(17, image.Metadata.ExifProfile.Values.Count()); + // Should be 3 less. + // 1 x due to setting of null "ReferenceBlackWhite" value. + // 2 x due to use of non-standard padding tag 0xEA1C listed in EXIF Tool. We can read those values but adhere + // strictly to the 2.3.1 specification when writing. (TODO: Support 2.3.2) + // https://exiftool.org/TagNames/EXIF.html + Assert.Equal(profileCount - 3, image.Metadata.ExifProfile.Values.Count); - // value = image.Metadata.ExifProfile.GetValue(ExifTag.Software); - // TestValue(value, "ImageSharp"); + software = image.Metadata.ExifProfile.GetValue(ExifTag.Software); + Assert.Equal("15", software.Value); - // value = image.Metadata.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); - // TestValue(value, new SignedRational(75.55)); + shutterSpeed = image.Metadata.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); + Assert.Equal(new SignedRational(75.55), shutterSpeed.Value); - // value = image.Metadata.ExifProfile.GetValue(ExifTag.XResolution); - // TestValue(value, new Rational(150.0)); + xResolution = image.Metadata.ExifProfile.GetValue(ExifTag.XResolution); + Assert.Equal(new Rational(150.0), xResolution.Value); - // value = image.Metadata.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite); - // Assert.Null(value); + referenceBlackWhite = image.Metadata.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite); + Assert.Null(referenceBlackWhite); - // value = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude); - // TestValue(value, latitude); + latitude = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude); + Assert.Equal(expectedLatitude, latitude.Value); - // image.Metadata.ExifProfile.Parts = ExifParts.ExifTags; + image.Metadata.ExifProfile.Parts = ExifParts.ExifTags; - // image = WriteAndRead(image, imageFormat); + image = WriteAndRead(image, imageFormat); - // Assert.NotNull(image.Metadata.ExifProfile); - // Assert.Equal(8, image.Metadata.ExifProfile.Values.Count()); + Assert.NotNull(image.Metadata.ExifProfile); + Assert.Equal(8, image.Metadata.ExifProfile.Values.Count); - // Assert.NotNull(image.Metadata.ExifProfile.GetValue(ExifTag.ColorSpace)); - // Assert.True(image.Metadata.ExifProfile.RemoveValue(ExifTag.ColorSpace)); - // Assert.False(image.Metadata.ExifProfile.RemoveValue(ExifTag.ColorSpace)); - // Assert.Null(image.Metadata.ExifProfile.GetValue(ExifTag.ColorSpace)); + Assert.NotNull(image.Metadata.ExifProfile.GetValue(ExifTag.ColorSpace)); + Assert.True(image.Metadata.ExifProfile.RemoveValue(ExifTag.ColorSpace)); + Assert.False(image.Metadata.ExifProfile.RemoveValue(ExifTag.ColorSpace)); + Assert.Null(image.Metadata.ExifProfile.GetValue(ExifTag.ColorSpace)); - // Assert.Equal(7, image.Metadata.ExifProfile.Values.Count()); - //} + Assert.Equal(7, image.Metadata.ExifProfile.Values.Count); + } [Fact] public void Syncs() @@ -308,8 +315,8 @@ namespace SixLabors.ImageSharp.Tests foreach (ExifTag expectedProfileTag in expectedProfileTags) { - IExifValue actualProfileValue = actualProfile.Values.First(x => x.Tag == expectedProfileTag); - IExifValue expectedProfileValue = expectedProfile.Values.First(x => x.Tag == expectedProfileTag); + IExifValue actualProfileValue = actualProfile.GetValueInternal(expectedProfileTag); + IExifValue expectedProfileValue = expectedProfile.GetValueInternal(expectedProfileTag); Assert.Equal(expectedProfileValue.GetValue(), actualProfileValue.GetValue()); } @@ -324,7 +331,6 @@ namespace SixLabors.ImageSharp.Tests { // This image contains an 802 byte EXIF profile // It has a tag with an index offset of 18,481,152 bytes (overrunning the data) - Image image = TestFile.Create(TestImages.Jpeg.Progressive.Bad.ExifUndefType).CreateRgba32Image(); Assert.NotNull(image); @@ -357,53 +363,52 @@ namespace SixLabors.ImageSharp.Tests Assert.Equal(525, bytes.Length); } - //[Theory] - //[InlineData(TestImageWriteFormat.Jpeg)] - //[InlineData(TestImageWriteFormat.Png)] - //public void WritingImagePreservesExifProfile(TestImageWriteFormat imageFormat) - //{ - // // arrange - // var image = new Image(1, 1); - // ExifProfile expected = CreateExifProfile(); - // image.Metadata.ExifProfile = expected; - - // // act - // Image reloadedImage = WriteAndRead(image, imageFormat); - - // // assert - // ExifProfile actual = reloadedImage.Metadata.ExifProfile; - // Assert.NotNull(actual); - // foreach (KeyValuePair expectedProfileValue in TestProfileValues) - // { - // ExifValue actualProfileValue = actual.GetValue(expectedProfileValue.Key); - // Assert.NotNull(actualProfileValue); - // Assert.Equal(expectedProfileValue.Value, actualProfileValue.Value); - // } - //} - - //[Fact] - //public void ProfileToByteArray() - //{ - // // arrange - // byte[] exifBytesWithoutExifCode = ExifConstants.LittleEndianByteOrderMarker; - // ExifProfile expectedProfile = CreateExifProfile(); - // var expectedProfileTags = expectedProfile.Values.Select(x => x.Tag).ToList(); - - // // act - // byte[] actualBytes = expectedProfile.ToByteArray(); - // var actualProfile = new ExifProfile(actualBytes); - - // // assert - // Assert.NotNull(actualBytes); - // Assert.NotEmpty(actualBytes); - // Assert.Equal(exifBytesWithoutExifCode, actualBytes.Take(exifBytesWithoutExifCode.Length).ToArray()); - // foreach (ExifTag expectedProfileTag in expectedProfileTags) - // { - // ExifValue actualProfileValue = actualProfile.GetValue(expectedProfileTag); - // ExifValue expectedProfileValue = expectedProfile.GetValue(expectedProfileTag); - // Assert.Equal(expectedProfileValue.Value, actualProfileValue.Value); - // } - //} + [Theory] + [InlineData(TestImageWriteFormat.Jpeg)] + [InlineData(TestImageWriteFormat.Png)] + public void WritingImagePreservesExifProfile(TestImageWriteFormat imageFormat) + { + // Arrange + var image = new Image(1, 1); + image.Metadata.ExifProfile = CreateExifProfile(); + + // Act + Image reloadedImage = WriteAndRead(image, imageFormat); + + // Assert + ExifProfile actual = reloadedImage.Metadata.ExifProfile; + Assert.NotNull(actual); + foreach (KeyValuePair expectedProfileValue in TestProfileValues) + { + IExifValue actualProfileValue = actual.GetValueInternal(expectedProfileValue.Key); + Assert.NotNull(actualProfileValue); + Assert.Equal(expectedProfileValue.Value, actualProfileValue.GetValue()); + } + } + + [Fact] + public void ProfileToByteArray() + { + // Arrange + byte[] exifBytesWithoutExifCode = ExifConstants.LittleEndianByteOrderMarker; + ExifProfile expectedProfile = CreateExifProfile(); + var expectedProfileTags = expectedProfile.Values.Select(x => x.Tag).ToList(); + + // Act + byte[] actualBytes = expectedProfile.ToByteArray(); + var actualProfile = new ExifProfile(actualBytes); + + // Assert + Assert.NotNull(actualBytes); + Assert.NotEmpty(actualBytes); + Assert.Equal(exifBytesWithoutExifCode, actualBytes.Take(exifBytesWithoutExifCode.Length).ToArray()); + foreach (ExifTag expectedProfileTag in expectedProfileTags) + { + IExifValue actualProfileValue = actualProfile.GetValueInternal(expectedProfileTag); + IExifValue expectedProfileValue = expectedProfile.GetValueInternal(expectedProfileTag); + Assert.Equal(expectedProfileValue.GetValue(), actualProfileValue.GetValue()); + } + } private static ExifProfile CreateExifProfile() { @@ -411,12 +416,7 @@ namespace SixLabors.ImageSharp.Tests foreach (KeyValuePair exifProfileValue in TestProfileValues) { - // Manky - Answers on a postcard for a nicer way to do this. - MethodInfo method = typeof(ExifProfile) - .GetMethod("SetValue") - .MakeGenericMethod(new[] { exifProfileValue.Key.GetType().GenericTypeArguments[0] }); - - method.Invoke(profile, new[] { exifProfileValue.Key, exifProfileValue.Value }); + profile.SetValueInternal(exifProfileValue.Key, exifProfileValue.Value); } return profile; @@ -434,12 +434,15 @@ namespace SixLabors.ImageSharp.Tests private static Image WriteAndRead(Image image, TestImageWriteFormat imageFormat) { - return imageFormat switch + switch (imageFormat) { - TestImageWriteFormat.Jpeg => WriteAndReadJpeg(image), - TestImageWriteFormat.Png => WriteAndReadPng(image), - _ => throw new ArgumentException("Unexpected test image format, only Jpeg and Png are allowed"), - }; + case TestImageWriteFormat.Jpeg: + return WriteAndReadJpeg(image); + case TestImageWriteFormat.Png: + return WriteAndReadPng(image); + default: + throw new ArgumentException("Unexpected test image format, only Jpeg and Png are allowed"); + } } private static Image WriteAndReadJpeg(Image image) @@ -486,43 +489,5 @@ namespace SixLabors.ImageSharp.Tests IExifValue xDimension = profile.GetValue(ExifTag.PixelXDimension); Assert.Equal(2338U, xDimension.Value); } - - private static void TestValue(IExifValue value, string expected) - { - Assert.NotNull(value); - Assert.Equal(expected, value.GetValue()); - } - - private static void TestValue(IExifValue value, Rational expected) - { - Assert.NotNull(value); - Assert.Equal(expected, value.GetValue()); - } - - private static void TestValue(IExifValue value, SignedRational expected) - { - Assert.NotNull(value); - Assert.Equal(expected, value.GetValue()); - } - - private static void TestValue(IExifValue value, Rational[] expected) - { - Assert.NotNull(value); - - Assert.Equal(expected, (ICollection)value.GetValue()); - } - - private static void TestValue(ExifValue value, double expected) - { - Assert.NotNull(value); - Assert.Equal(expected, value.GetValue()); - } - - private static void TestValue(ExifValue value, double[] expected) - { - Assert.NotNull(value); - - Assert.Equal(expected, (ICollection)value.GetValue()); - } } }