From fa68e1b8439b204f989ca27749ae0caee1edecf8 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 9 May 2021 19:10:56 +0200 Subject: [PATCH] Read and write Exif Profile --- .../Tiff/TiffDecoderMetadataCreator.cs | 10 ++- .../Tiff/TiffEncoderEntriesCollector.cs | 22 ++++- .../Metadata/Profiles/Exif/ExifParts.cs | 4 +- .../Formats/Tiff/TiffDecoderTests.cs | 6 +- .../Formats/Tiff/TiffMetadataTests.cs | 3 +- .../Profiles/Exif/ExifProfileTests.cs | 80 +++++++++++++++---- 6 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index 7f97303ed..9a495ad31 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; @@ -57,9 +56,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } + if (coreMetadata.ExifProfile == null) + { + coreMetadata.ExifProfile = frame?.ExifProfile.DeepClone(); + } + if (coreMetadata.IptcProfile == null) { - if (TryGetIptc(frame.ExifProfile.Values, out var iptcBytes)) + if (TryGetIptc(frame.ExifProfile.Values, out byte[] iptcBytes)) { coreMetadata.IptcProfile = new IptcProfile(iptcBytes); } @@ -95,7 +99,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff // Some Encoders write the data type of IPTC as long. if (iptc.DataType == ExifDataType.Long) { - var iptcValues = (uint[])iptc.GetValue(); + uint[] iptcValues = (uint[])iptc.GetValue(); iptcBytes = new byte[iptcValues.Length * 4]; Buffer.BlockCopy(iptcValues, 0, iptcBytes, 0, iptcValues.Length * 4); if (iptcBytes[0] == 0x1c) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index c0ad474b2..4916a9804 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -66,12 +66,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.collector.Add(width); this.collector.Add(height); - this.collector.Add(software); this.ProcessResolution(image.Metadata, frameMetadata); - this.ProcessProfiles(image.Metadata, frameMetadata); this.ProcessMetadata(frameMetadata); + + if (!this.collector.Entries.Exists(t => t.Tag == ExifTag.Software)) + { + this.collector.Add(software); + } } private static bool IsPureMetadata(ExifTag tag) @@ -174,9 +177,20 @@ namespace SixLabors.ImageSharp.Formats.Tiff private void ProcessProfiles(ImageMetadata imageMetadata, TiffFrameMetadata tiffFrameMetadata) { - if (imageMetadata.ExifProfile != null) + if (imageMetadata.ExifProfile != null && imageMetadata.ExifProfile.Parts != ExifParts.None) { - // todo: implement processing exif profile + imageMetadata.SyncProfiles(); + foreach (IExifValue entry in imageMetadata.ExifProfile.Values) + { + if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null) + { + ExifParts entryPart = ExifTags.GetPart(entry.Tag); + if (entryPart != ExifParts.None && imageMetadata.ExifProfile.Parts.HasFlag(entryPart)) + { + this.collector.AddOrReplace(entry.DeepClone()); + } + } + } } else { diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs index dc12f3819..0a9c879ce 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifParts.cs @@ -24,12 +24,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// /// ExifTags /// - ExifTags = 4, + ExifTags = 2, /// /// GPSTags /// - GpsTags = 8, + GpsTags = 4, /// /// All diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ae1de1734..bffb60302 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -42,9 +42,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder)); [Theory] - [InlineData(TestImages.Tiff.RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)] - [InlineData(TestImages.Tiff.SmallRgbDeflate, 24, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)] - [InlineData(TestImages.Tiff.Calliphora_GrayscaleUncompressed, 8, 804, 1198, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)] + [InlineData(SmallRgbDeflate, 24, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(Calliphora_GrayscaleUncompressed, 8, 804, 1198, 96, 96, PixelResolutionUnit.PixelsPerInch)] public void Identify(string imagePath, int expectedPixelSize, int expectedWidth, int expectedHeight, double expectedHResolution, double expectedVResolution, PixelResolutionUnit expectedResolutionUnit) { var testFile = TestFile.Create(imagePath); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index 5763b0e8a..f501299fd 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -292,8 +292,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(frameMeta.HorizontalResolution, frameMetaOut.HorizontalResolution); Assert.Equal(frameMeta.VerticalResolution, frameMetaOut.VerticalResolution); - Assert.Equal("ImageSharp", frameMetaOut.ExifProfile.GetValue(ExifTag.Software).Value); - if (preserveMetadata) { Assert.Equal(tiffMeta.XmpProfile, tiffMetaOut.XmpProfile); @@ -311,6 +309,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { Assert.Null(tiffMetaOut.XmpProfile); + Assert.Equal("ImageSharp", frameMetaOut.ExifProfile.GetValue(ExifTag.Software).Value); Assert.Null(frameMeta.ExifProfile.GetValue(ExifTag.Software)?.Value); Assert.Null(frameMeta.ExifProfile.GetValue(ExifTag.ImageDescription)?.Value); Assert.Null(frameMeta.ExifProfile.GetValue(ExifTag.Make)?.Value); diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 10f6ff9bf..1f4bbaea9 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; - +using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -28,7 +28,12 @@ namespace SixLabors.ImageSharp.Tests /// /// Writes a png file. /// - Png + Png, + + /// + /// Writes a tiff file. + /// + Tiff, } private static readonly Dictionary TestProfileValues = new Dictionary @@ -69,7 +74,7 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void ConstructorEmpty() { - new ExifProfile((byte[])null); + new ExifProfile(null); new ExifProfile(new byte[] { }); } @@ -92,6 +97,7 @@ namespace SixLabors.ImageSharp.Tests [Theory] [InlineData(TestImageWriteFormat.Jpeg)] [InlineData(TestImageWriteFormat.Png)] + [InlineData(TestImageWriteFormat.Tiff)] public void WriteFraction(TestImageWriteFormat imageFormat) { using (var memStream = new MemoryStream()) @@ -135,6 +141,7 @@ namespace SixLabors.ImageSharp.Tests [Theory] [InlineData(TestImageWriteFormat.Jpeg)] [InlineData(TestImageWriteFormat.Png)] + [InlineData(TestImageWriteFormat.Tiff)] public void ReadWriteInfinity(TestImageWriteFormat imageFormat) { Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); @@ -161,9 +168,17 @@ namespace SixLabors.ImageSharp.Tests } [Theory] - [InlineData(TestImageWriteFormat.Jpeg)] - [InlineData(TestImageWriteFormat.Png)] - public void SetValue(TestImageWriteFormat imageFormat) + /* The original exif profile has 19 values, the written profile 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 */ + [InlineData(TestImageWriteFormat.Jpeg, 16)] + [InlineData(TestImageWriteFormat.Png, 16)] + /* Note: The tiff format has 24 expected profile values, because some tiff specific exif + values will be written in addition to the original profile. */ + [InlineData(TestImageWriteFormat.Tiff, 24)] + public void SetValue(TestImageWriteFormat imageFormat, int expectedProfileValueCount) { Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); image.Metadata.ExifProfile.SetValue(ExifTag.Software, "ImageSharp"); @@ -206,18 +221,12 @@ namespace SixLabors.ImageSharp.Tests // todo: duplicate tags Assert.Equal(2, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932)); - int profileCount = image.Metadata.ExifProfile.Values.Count; image = WriteAndRead(image, imageFormat); Assert.NotNull(image.Metadata.ExifProfile); Assert.Equal(0, image.Metadata.ExifProfile.Values.Count(v => (ushort)v.Tag == 59932)); - // 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); + Assert.Equal(expectedProfileValueCount, image.Metadata.ExifProfile.Values.Count); software = image.Metadata.ExifProfile.GetValue(ExifTag.Software); Assert.Equal("15", software.Value); @@ -233,20 +242,42 @@ namespace SixLabors.ImageSharp.Tests latitude = image.Metadata.ExifProfile.GetValue(ExifTag.GPSLatitude); Assert.Equal(expectedLatitude, latitude.Value); + } + [Theory] + [InlineData(TestImageWriteFormat.Jpeg)] + [InlineData(TestImageWriteFormat.Png)] + public void WriteOnlyExifTags_Works(TestImageWriteFormat imageFormat) + { + // Arrange + Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); image.Metadata.ExifProfile.Parts = ExifParts.ExifTags; + // Act image = WriteAndRead(image, imageFormat); + // Assert Assert.NotNull(image.Metadata.ExifProfile); - Assert.Equal(8, image.Metadata.ExifProfile.Values.Count); + Assert.Equal(7, image.Metadata.ExifProfile.Values.Count); + foreach (IExifValue exifProfileValue in image.Metadata.ExifProfile.Values) + { + Assert.True(ExifTags.GetPart(exifProfileValue.Tag) == ExifParts.ExifTags); + } + } + + [Fact] + public void RemoveEntry_Works() + { + // Arrange + Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); + int profileCount = image.Metadata.ExifProfile.Values.Count; + // Assert 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(profileCount - 1, image.Metadata.ExifProfile.Values.Count); } [Fact] @@ -382,6 +413,7 @@ namespace SixLabors.ImageSharp.Tests [Theory] [InlineData(TestImageWriteFormat.Jpeg)] [InlineData(TestImageWriteFormat.Png)] + [InlineData(TestImageWriteFormat.Tiff)] public void WritingImagePreservesExifProfile(TestImageWriteFormat imageFormat) { // Arrange @@ -456,8 +488,10 @@ namespace SixLabors.ImageSharp.Tests return WriteAndReadJpeg(image); case TestImageWriteFormat.Png: return WriteAndReadPng(image); + case TestImageWriteFormat.Tiff: + return WriteAndReadTiff(image); default: - throw new ArgumentException("Unexpected test image format, only Jpeg and Png are allowed"); + throw new ArgumentException("Unexpected test image format, only Jpeg, Png and Tiff are allowed"); } } @@ -485,6 +519,18 @@ namespace SixLabors.ImageSharp.Tests } } + private static Image WriteAndReadTiff(Image image) + { + using (var memStream = new MemoryStream()) + { + image.SaveAsTiff(memStream, new TiffEncoder()); + image.Dispose(); + + memStream.Position = 0; + return Image.Load(memStream, new TiffDecoder()); + } + } + private static void TestProfile(ExifProfile profile) { Assert.NotNull(profile);