diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index d7243c6964..b7338ac20a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -157,6 +157,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals long ifdMarker = WriteHeader(writer, buffer); Image metadataImage = image; + foreach (ImageFrame frame in image.Frames) { cancellationToken.ThrowIfCancellationRequested(); @@ -235,9 +236,13 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals if (image != null) { + // Write the metadata for the root image entriesCollector.ProcessMetadata(image, this.skipMetadata); } + // Write the metadata for the frame + entriesCollector.ProcessMetadata(frame, this.skipMetadata); + entriesCollector.ProcessFrameInfo(frame, imageMetadata); entriesCollector.ProcessImageFormat(this); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index cf9b4ae213..e7ee0c65bc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -19,6 +20,9 @@ internal class TiffEncoderEntriesCollector public void ProcessMetadata(Image image, bool skipMetadata) => new MetadataProcessor(this).Process(image, skipMetadata); + public void ProcessMetadata(ImageFrame frame, bool skipMetadata) + => new MetadataProcessor(this).Process(frame, skipMetadata); + public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) => new FrameInfoProcessor(this).Process(frame, imageMetadata); @@ -56,15 +60,30 @@ internal class TiffEncoderEntriesCollector public void Process(Image image, bool skipMetadata) { - ImageFrame rootFrame = image.Frames.RootFrame; - ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile; - XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile; + this.ProcessProfiles(image.Metadata, skipMetadata); - this.ProcessProfiles(image.Metadata, skipMetadata, rootFrameExifProfile, rootFrameXmpProfile); + if (!skipMetadata) + { + this.ProcessMetadata(image.Metadata.ExifProfile ?? new ExifProfile()); + } + + if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) + { + this.Collector.Add(new ExifString(ExifTagValue.Software) + { + Value = SoftwareValue + }); + } + } + + + public void Process(ImageFrame frame, bool skipMetadata) + { + this.ProcessProfiles(frame.Metadata, skipMetadata); if (!skipMetadata) { - this.ProcessMetadata(rootFrameExifProfile ?? new ExifProfile()); + this.ProcessMetadata(frame.Metadata.ExifProfile ?? new ExifProfile()); } if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) @@ -150,16 +169,16 @@ internal class TiffEncoderEntriesCollector } } - private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, ExifProfile exifProfile, XmpProfile xmpProfile) + private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata) { - if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None)) + if (!skipMetadata && (imageMetadata.ExifProfile != null && imageMetadata.ExifProfile.Parts != ExifParts.None)) { - foreach (IExifValue entry in exifProfile.Values) + 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 && exifProfile.Parts.HasFlag(entryPart)) + if (entryPart != ExifParts.None && imageMetadata.ExifProfile.Parts.HasFlag(entryPart)) { this.Collector.AddOrReplace(entry.DeepClone()); } @@ -168,7 +187,7 @@ internal class TiffEncoderEntriesCollector } else { - exifProfile?.RemoveValue(ExifTag.SubIFDOffset); + imageMetadata.ExifProfile?.RemoveValue(ExifTag.SubIFDOffset); } if (!skipMetadata && imageMetadata.IptcProfile != null) @@ -183,7 +202,7 @@ internal class TiffEncoderEntriesCollector } else { - exifProfile?.RemoveValue(ExifTag.IPTC); + imageMetadata.ExifProfile?.RemoveValue(ExifTag.IPTC); } if (imageMetadata.IccProfile != null) @@ -197,21 +216,86 @@ internal class TiffEncoderEntriesCollector } else { - exifProfile?.RemoveValue(ExifTag.IccProfile); + imageMetadata.ExifProfile?.RemoveValue(ExifTag.IccProfile); + } + + if (!skipMetadata && imageMetadata.XmpProfile != null) + { + ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte) + { + Value = imageMetadata.XmpProfile.Data + }; + + this.Collector.AddOrReplace(xmp); + } + else + { + imageMetadata.ExifProfile?.RemoveValue(ExifTag.XMP); + } + } + + private void ProcessProfiles(ImageFrameMetadata frameMetadata, bool skipMetadata) + { + if (!skipMetadata && (frameMetadata.ExifProfile != null && frameMetadata.ExifProfile.Parts != ExifParts.None)) + { + foreach (IExifValue entry in frameMetadata.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 && frameMetadata.ExifProfile.Parts.HasFlag(entryPart)) + { + this.Collector.AddOrReplace(entry.DeepClone()); + } + } + } + } + else + { + frameMetadata.ExifProfile?.RemoveValue(ExifTag.SubIFDOffset); + } + + if (!skipMetadata && frameMetadata.IptcProfile != null) + { + frameMetadata.IptcProfile.UpdateData(); + ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte) + { + Value = frameMetadata.IptcProfile.Data + }; + + this.Collector.AddOrReplace(iptc); + } + else + { + frameMetadata.ExifProfile?.RemoveValue(ExifTag.IPTC); + } + + if (frameMetadata.IccProfile != null) + { + ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined) + { + Value = frameMetadata.IccProfile.ToByteArray() + }; + + this.Collector.AddOrReplace(icc); + } + else + { + frameMetadata.ExifProfile?.RemoveValue(ExifTag.IccProfile); } - if (!skipMetadata && xmpProfile != null) + if (!skipMetadata && frameMetadata.XmpProfile != null) { ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte) { - Value = xmpProfile.Data + Value = frameMetadata.XmpProfile.Data }; this.Collector.AddOrReplace(xmp); } else { - exifProfile?.RemoveValue(ExifTag.XMP); + frameMetadata.ExifProfile?.RemoveValue(ExifTag.XMP); } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index b671addf95..1225bbc8cb 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -318,4 +319,94 @@ public class TiffMetadataTests Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value); Assert.Equal(exifProfileInput.Values.Count, encodedImageExifProfile.Values.Count); } + + [Theory] + [WithFile(SampleMetadata, PixelTypes.Rgba32)] + public void Encode_PreservesMetadata_IptcAndIcc(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Load Tiff image + DecoderOptions options = new() { SkipMetadata = false }; + using Image image = provider.GetImage(TiffDecoder.Instance, options); + + ImageMetadata inputMetaData = image.Metadata; + ImageFrame rootFrameInput = image.Frames.RootFrame; + + IptcProfile iptcProfile = new(); + iptcProfile.SetValue(IptcTag.Name, "Test name"); + rootFrameInput.Metadata.IptcProfile = iptcProfile; + + IccProfileHeader iccProfileHeader = new IccProfileHeader(); + iccProfileHeader.Class = IccProfileClass.ColorSpace; + IccProfile iccProfile = new(); + rootFrameInput.Metadata.IccProfile = iccProfile; + + TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata(); + XmpProfile xmpProfileInput = rootFrameInput.Metadata.XmpProfile; + ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile; + IptcProfile iptcProfileInput = rootFrameInput.Metadata.IptcProfile; + IccProfile iccProfileInput = rootFrameInput.Metadata.IccProfile; + + Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression); + Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaInput.BitsPerPixel); + + // Save to Tiff + TiffEncoder tiffEncoder = new() { PhotometricInterpretation = TiffPhotometricInterpretation.Rgb }; + using MemoryStream ms = new(); + image.Save(ms, tiffEncoder); + + // Assert + ms.Position = 0; + using Image encodedImage = Image.Load(ms); + + ImageMetadata encodedImageMetaData = encodedImage.Metadata; + ImageFrame rootFrameEncodedImage = encodedImage.Frames.RootFrame; + TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata(); + ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile; + XmpProfile encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile; + IptcProfile encodedImageIptcProfile = rootFrameEncodedImage.Metadata.IptcProfile; + IccProfile encodedImageIccProfile = rootFrameEncodedImage.Metadata.IccProfile; + + Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel); + Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression); + + Assert.Equal(inputMetaData.HorizontalResolution, encodedImageMetaData.HorizontalResolution); + Assert.Equal(inputMetaData.VerticalResolution, encodedImageMetaData.VerticalResolution); + Assert.Equal(inputMetaData.ResolutionUnits, encodedImageMetaData.ResolutionUnits); + + Assert.Equal(rootFrameInput.Width, rootFrameEncodedImage.Width); + Assert.Equal(rootFrameInput.Height, rootFrameEncodedImage.Height); + + PixelResolutionUnit resolutionUnitInput = UnitConverter.ExifProfileToResolutionUnit(exifProfileInput); + PixelResolutionUnit resolutionUnitEncoded = UnitConverter.ExifProfileToResolutionUnit(encodedImageExifProfile); + Assert.Equal(resolutionUnitInput, resolutionUnitEncoded); + Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); + Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); + + Assert.NotNull(xmpProfileInput); + Assert.NotNull(encodedImageXmpProfile); + Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data); + + Assert.NotNull(iptcProfileInput); + Assert.NotNull(encodedImageIptcProfile); + Assert.Equal(iptcProfileInput.Data, encodedImageIptcProfile.Data); + Assert.Equal(iptcProfileInput.GetValues(IptcTag.Name)[0].Value, encodedImageIptcProfile.GetValues(IptcTag.Name)[0].Value); + + Assert.NotNull(iccProfileInput); + Assert.NotNull(encodedImageIccProfile); + Assert.Equal(iccProfileInput.Entries.Length, encodedImageIccProfile.Entries.Length); + Assert.Equal(iccProfileInput.Header.Class, encodedImageIccProfile.Header.Class); + + Assert.Equal(exifProfileInput.GetValue(ExifTag.Software).Value, encodedImageExifProfile.GetValue(ExifTag.Software).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.ImageDescription).Value, encodedImageExifProfile.GetValue(ExifTag.ImageDescription).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Make).Value, encodedImageExifProfile.GetValue(ExifTag.Make).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Copyright).Value, encodedImageExifProfile.GetValue(ExifTag.Copyright).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Artist).Value, encodedImageExifProfile.GetValue(ExifTag.Artist).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Orientation).Value, encodedImageExifProfile.GetValue(ExifTag.Orientation).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Model).Value, encodedImageExifProfile.GetValue(ExifTag.Model).Value); + + Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value); + // Adding the IPTC and ICC profiles dynamically increments the number of values in the original EXIF profile by 2 + Assert.Equal(exifProfileInput.Values.Count + 2, encodedImageExifProfile.Values.Count); + } }