📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

528 lines
20 KiB

// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using Xunit;
namespace SixLabors.ImageSharp.Tests
{
public class ExifProfileTests
{
public enum TestImageWriteFormat
{
Jpeg,
Png
}
private static readonly Dictionary<ExifTag, object> TestProfileValues = new Dictionary<ExifTag, object>
{
{ ExifTag.Software, "Software" },
{ ExifTag.Copyright, "Copyright" },
{ ExifTag.Orientation, (ushort)5 },
{ ExifTag.ShutterSpeedValue, new SignedRational(75.55) },
{ ExifTag.ImageDescription, "ImageDescription" },
{ ExifTag.ExposureTime, new Rational(1.0 / 1600.0) },
{ ExifTag.Model, "Model" },
};
[Theory]
[InlineData(TestImageWriteFormat.Jpeg)]
[InlineData(TestImageWriteFormat.Png)]
public void Constructor(TestImageWriteFormat imageFormat)
{
Image<Rgba32> image = TestFile.Create(TestImages.Jpeg.Baseline.Calliphora).CreateRgba32Image();
Assert.Null(image.Metadata.ExifProfile);
const string expected = "Dirk Lemstra";
image.Metadata.ExifProfile = new ExifProfile();
image.Metadata.ExifProfile.SetValue(ExifTag.Copyright, expected);
image = WriteAndRead(image, imageFormat);
Assert.NotNull(image.Metadata.ExifProfile);
Assert.Equal(1, image.Metadata.ExifProfile.Values.Count);
IExifValue<string> value = image.Metadata.ExifProfile.GetValue(ExifTag.Copyright);
Assert.NotNull(value);
Assert.Equal(expected, value.Value);
}
[Fact]
public void ConstructorEmpty()
{
new ExifProfile((byte[])null);
new ExifProfile(new byte[] { });
}
[Fact]
public void ConstructorCopy()
{
Assert.Throws<NullReferenceException>(() => ((ExifProfile)null).DeepClone());
ExifProfile profile = GetExifProfile();
ExifProfile clone = profile.DeepClone();
TestProfile(clone);
profile.SetValue(ExifTag.ColorSpace, (ushort)2);
clone = profile.DeepClone();
TestProfile(clone);
}
[Theory]
[InlineData(TestImageWriteFormat.Jpeg)]
[InlineData(TestImageWriteFormat.Png)]
public void WriteFraction(TestImageWriteFormat imageFormat)
{
using (var memStream = new MemoryStream())
{
double exposureTime = 1.0 / 1600;
ExifProfile profile = GetExifProfile();
profile.SetValue(ExifTag.ExposureTime, new Rational(exposureTime));
var image = new Image<Rgba32>(1, 1);
image.Metadata.ExifProfile = profile;
image = WriteAndRead(image, imageFormat);
profile = image.Metadata.ExifProfile;
Assert.NotNull(profile);
IExifValue<Rational> value = profile.GetValue(ExifTag.ExposureTime);
Assert.NotNull(value);
Assert.NotEqual(exposureTime, value.Value.ToDouble());
memStream.Position = 0;
profile = GetExifProfile();
profile.SetValue(ExifTag.ExposureTime, new Rational(exposureTime, true));
image.Metadata.ExifProfile = profile;
image = WriteAndRead(image, imageFormat);
profile = image.Metadata.ExifProfile;
Assert.NotNull(profile);
value = profile.GetValue(ExifTag.ExposureTime);
Assert.Equal(exposureTime, value.Value.ToDouble());
image.Dispose();
}
}
[Theory]
[InlineData(TestImageWriteFormat.Jpeg)]
[InlineData(TestImageWriteFormat.Png)]
public void ReadWriteInfinity(TestImageWriteFormat imageFormat)
{
Image<Rgba32> image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image();
image.Metadata.ExifProfile.SetValue(ExifTag.ExposureBiasValue, new SignedRational(double.PositiveInfinity));
image = WriteAndReadJpeg(image);
IExifValue<SignedRational> value = image.Metadata.ExifProfile.GetValue(ExifTag.ExposureBiasValue);
Assert.NotNull(value);
Assert.Equal(new SignedRational(double.PositiveInfinity), value.Value);
image.Metadata.ExifProfile.SetValue(ExifTag.ExposureBiasValue, new SignedRational(double.NegativeInfinity));
image = WriteAndRead(image, imageFormat);
value = image.Metadata.ExifProfile.GetValue(ExifTag.ExposureBiasValue);
Assert.NotNull(value);
Assert.Equal(new SignedRational(double.NegativeInfinity), value.Value);
image.Metadata.ExifProfile.SetValue(ExifTag.FlashEnergy, new Rational(double.NegativeInfinity));
image = WriteAndRead(image, imageFormat);
IExifValue<Rational> value2 = image.Metadata.ExifProfile.GetValue(ExifTag.FlashEnergy);
Assert.NotNull(value);
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<Rgba32> 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");
// Assert.Throws<ArgumentException>(() => { value.SetValue(15); });
// image.Metadata.ExifProfile.SetValue(ExifTag.ShutterSpeedValue, new SignedRational(75.55));
// value = image.Metadata.ExifProfile.GetValue(ExifTag.ShutterSpeedValue);
// TestValue(value, new SignedRational(7555, 100));
// Assert.Throws<ArgumentException>(() => { value.WithValue(75); });
// 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;
// value = image.Metadata.ExifProfile.GetValue(ExifTag.XResolution);
// TestValue(value, new Rational(150, 1));
// Assert.Throws<ArgumentException>(() => { value.WithValue("ImageSharp"); });
// image.Metadata.ExifProfile.SetValue(ExifTag.ReferenceBlackWhite, null);
// value = image.Metadata.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite);
// TestValue(value, (string)null);
// image.Metadata.ExifProfile.SetValue(ExifTag.GPSLatitude, latitude);
// value = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude);
// TestValue(value, latitude);
// image = WriteAndRead(image, imageFormat);
// Assert.NotNull(image.Metadata.ExifProfile);
// Assert.Equal(17, image.Metadata.ExifProfile.Values.Count());
// value = image.Metadata.ExifProfile.GetValue(ExifTag.Software);
// TestValue(value, "ImageSharp");
// value = image.Metadata.ExifProfile.GetValue(ExifTag.ShutterSpeedValue);
// TestValue(value, new SignedRational(75.55));
// value = image.Metadata.ExifProfile.GetValue(ExifTag.XResolution);
// TestValue(value, new Rational(150.0));
// value = image.Metadata.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite);
// Assert.Null(value);
// value = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude);
// TestValue(value, latitude);
// image.Metadata.ExifProfile.Parts = ExifParts.ExifTags;
// image = WriteAndRead(image, imageFormat);
// 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.Equal(7, image.Metadata.ExifProfile.Values.Count());
//}
[Fact]
public void Syncs()
{
var exifProfile = new ExifProfile();
exifProfile.SetValue(ExifTag.XResolution, new Rational(200));
exifProfile.SetValue(ExifTag.YResolution, new Rational(300));
var metaData = new ImageMetadata
{
ExifProfile = exifProfile,
HorizontalResolution = 200,
VerticalResolution = 300
};
metaData.HorizontalResolution = 100;
Assert.Equal(200, metaData.ExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble());
Assert.Equal(300, metaData.ExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble());
exifProfile.Sync(metaData);
Assert.Equal(100, (metaData.ExifProfile.GetValue(ExifTag.XResolution).Value).ToDouble());
Assert.Equal(300, (metaData.ExifProfile.GetValue(ExifTag.YResolution).Value).ToDouble());
metaData.VerticalResolution = 150;
Assert.Equal(100, (metaData.ExifProfile.GetValue(ExifTag.XResolution).Value).ToDouble());
Assert.Equal(300, (metaData.ExifProfile.GetValue(ExifTag.YResolution).Value).ToDouble());
exifProfile.Sync(metaData);
Assert.Equal(100, (metaData.ExifProfile.GetValue(ExifTag.XResolution).Value).ToDouble());
Assert.Equal(150, (metaData.ExifProfile.GetValue(ExifTag.YResolution).Value).ToDouble());
}
[Fact]
public void Values()
{
ExifProfile profile = GetExifProfile();
TestProfile(profile);
Image<Rgba32> thumbnail = profile.CreateThumbnail<Rgba32>();
Assert.NotNull(thumbnail);
Assert.Equal(256, thumbnail.Width);
Assert.Equal(170, thumbnail.Height);
}
[Fact]
public void ReadWriteLargeProfileJpg()
{
ExifTag<string>[] tags = new[] { ExifTag.Software, ExifTag.Copyright, ExifTag.Model, ExifTag.ImageDescription };
foreach (ExifTag<string> tag in tags)
{
// Arrange
var junk = new StringBuilder();
for (int i = 0; i < 65600; i++)
{
junk.Append("a");
}
var image = new Image<Rgba32>(100, 100);
ExifProfile expectedProfile = CreateExifProfile();
var expectedProfileTags = expectedProfile.Values.Select(x => x.Tag).ToList();
expectedProfile.SetValue(tag, junk.ToString());
image.Metadata.ExifProfile = expectedProfile;
// Act
Image<Rgba32> reloadedImage = WriteAndRead(image, TestImageWriteFormat.Jpeg);
// Assert
ExifProfile actualProfile = reloadedImage.Metadata.ExifProfile;
Assert.NotNull(actualProfile);
foreach (ExifTag expectedProfileTag in expectedProfileTags)
{
IExifValue actualProfileValue = actualProfile.Values.First(x => x.Tag == expectedProfileTag);
IExifValue expectedProfileValue = expectedProfile.Values.First(x => x.Tag == expectedProfileTag);
Assert.Equal(expectedProfileValue.GetValue(), actualProfileValue.GetValue());
}
IExifValue<string> expected = expectedProfile.GetValue(tag);
IExifValue<string> actual = actualProfile.GetValue(tag);
Assert.Equal(expected, actual);
}
}
[Fact]
public void ExifTypeUndefined()
{
// 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<Rgba32> image = TestFile.Create(TestImages.Jpeg.Progressive.Bad.ExifUndefType).CreateRgba32Image();
Assert.NotNull(image);
ExifProfile profile = image.Metadata.ExifProfile;
Assert.NotNull(profile);
foreach (ExifValue value in profile.Values)
{
if (value.DataType == ExifDataType.Undefined)
{
Assert.True(value.IsArray);
Assert.Equal(4U, 4 * ExifDataTypes.GetSize(value.DataType));
}
}
}
[Fact]
public void TestArrayValueWithUnspecifiedSize()
{
// This images contains array in the exif profile that has zero components.
Image<Rgba32> image = TestFile.Create(TestImages.Jpeg.Issues.InvalidCast520).CreateRgba32Image();
ExifProfile profile = image.Metadata.ExifProfile;
Assert.NotNull(profile);
// Force parsing of the profile.
Assert.Equal(25, profile.Values.Count);
byte[] bytes = profile.ToByteArray();
Assert.Equal(525, bytes.Length);
}
//[Theory]
//[InlineData(TestImageWriteFormat.Jpeg)]
//[InlineData(TestImageWriteFormat.Png)]
//public void WritingImagePreservesExifProfile(TestImageWriteFormat imageFormat)
//{
// // arrange
// var image = new Image<Rgba32>(1, 1);
// ExifProfile expected = CreateExifProfile();
// image.Metadata.ExifProfile = expected;
// // act
// Image<Rgba32> reloadedImage = WriteAndRead(image, imageFormat);
// // assert
// ExifProfile actual = reloadedImage.Metadata.ExifProfile;
// Assert.NotNull(actual);
// foreach (KeyValuePair<ExifTag, object> 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);
// }
//}
private static ExifProfile CreateExifProfile()
{
var profile = new ExifProfile();
foreach (KeyValuePair<ExifTag, object> 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 });
}
return profile;
}
internal static ExifProfile GetExifProfile()
{
Image<Rgba32> image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image();
ExifProfile profile = image.Metadata.ExifProfile;
Assert.NotNull(profile);
return profile;
}
private static Image<Rgba32> WriteAndRead(Image<Rgba32> image, TestImageWriteFormat imageFormat)
{
return imageFormat switch
{
TestImageWriteFormat.Jpeg => WriteAndReadJpeg(image),
TestImageWriteFormat.Png => WriteAndReadPng(image),
_ => throw new ArgumentException("Unexpected test image format, only Jpeg and Png are allowed"),
};
}
private static Image<Rgba32> WriteAndReadJpeg(Image<Rgba32> image)
{
using (var memStream = new MemoryStream())
{
image.SaveAsJpeg(memStream);
image.Dispose();
memStream.Position = 0;
return Image.Load<Rgba32>(memStream);
}
}
private static Image<Rgba32> WriteAndReadPng(Image<Rgba32> image)
{
using (var memStream = new MemoryStream())
{
image.SaveAsPng(memStream);
image.Dispose();
memStream.Position = 0;
return Image.Load<Rgba32>(memStream);
}
}
private static void TestProfile(ExifProfile profile)
{
Assert.NotNull(profile);
Assert.Equal(16, profile.Values.Count);
foreach (IExifValue value in profile.Values)
{
Assert.NotNull(value.GetValue());
}
IExifValue<string> software = profile.GetValue(ExifTag.Software);
Assert.Equal("Windows Photo Editor 10.0.10011.16384", software.Value);
IExifValue<Rational> xResolution = profile.GetValue(ExifTag.XResolution);
Assert.Equal(new Rational(300.0), xResolution.Value);
IExifValue<Number> 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());
}
}
}