Skip to content

Commit

Permalink
Merge pull request #2535 from JeffP134/main
Browse files Browse the repository at this point in the history
Fix for issue 2504 - IPTC and ICC profile information being lost during TIFF file save
  • Loading branch information
JimBobSquarePants committed Sep 15, 2023
2 parents 54b7e04 + bf261f0 commit da4d4d7
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 11 deletions.
5 changes: 5 additions & 0 deletions src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
long ifdMarker = WriteHeader(writer, buffer);

Image<TPixel> metadataImage = image;

foreach (ImageFrame<TPixel> frame in image.Frames)
{
cancellationToken.ThrowIfCancellationRequested();
Expand Down Expand Up @@ -235,9 +236,13 @@ public static long WriteHeader(TiffStreamWriter writer, Span<byte> buffer)

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);

Expand Down
66 changes: 55 additions & 11 deletions src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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;

namespace SixLabors.ImageSharp.Formats.Tiff;
Expand All @@ -19,6 +21,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);

Expand Down Expand Up @@ -56,15 +61,29 @@ public MetadataProcessor(TiffEncoderEntriesCollector collector)

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))
Expand Down Expand Up @@ -150,7 +169,23 @@ private void ProcessMetadata(ExifProfile exifProfile)
}
}

private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, ExifProfile exifProfile, XmpProfile xmpProfile)
private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata)
{
this.ProcessExifProfile(skipMetadata, imageMetadata.ExifProfile);
this.ProcessIptcProfile(skipMetadata, imageMetadata.IptcProfile, imageMetadata.ExifProfile);
this.ProcessIccProfile(imageMetadata.IccProfile, imageMetadata.ExifProfile);
this.ProcessXmpProfile(skipMetadata, imageMetadata.XmpProfile, imageMetadata.ExifProfile);
}

private void ProcessProfiles(ImageFrameMetadata frameMetadata, bool skipMetadata)
{
this.ProcessExifProfile(skipMetadata, frameMetadata.ExifProfile);
this.ProcessIptcProfile(skipMetadata, frameMetadata.IptcProfile, frameMetadata.ExifProfile);
this.ProcessIccProfile(frameMetadata.IccProfile, frameMetadata.ExifProfile);
this.ProcessXmpProfile(skipMetadata, frameMetadata.XmpProfile, frameMetadata.ExifProfile);
}

private void ProcessExifProfile(bool skipMetadata, ExifProfile exifProfile)
{
if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None))
{
Expand All @@ -170,13 +205,16 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
{
exifProfile?.RemoveValue(ExifTag.SubIFDOffset);
}
}

if (!skipMetadata && imageMetadata.IptcProfile != null)
private void ProcessIptcProfile(bool skipMetadata, IptcProfile iptcProfile, ExifProfile exifProfile)
{
if (!skipMetadata && iptcProfile != null)
{
imageMetadata.IptcProfile.UpdateData();
iptcProfile.UpdateData();
ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte)
{
Value = imageMetadata.IptcProfile.Data
Value = iptcProfile.Data
};

this.Collector.AddOrReplace(iptc);
Expand All @@ -185,12 +223,15 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
{
exifProfile?.RemoveValue(ExifTag.IPTC);
}
}

if (imageMetadata.IccProfile != null)
private void ProcessIccProfile(IccProfile iccProfile, ExifProfile exifProfile)
{
if (iccProfile != null)
{
ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined)
{
Value = imageMetadata.IccProfile.ToByteArray()
Value = iccProfile.ToByteArray()
};

this.Collector.AddOrReplace(icc);
Expand All @@ -199,7 +240,10 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
{
exifProfile?.RemoveValue(ExifTag.IccProfile);
}
}

private void ProcessXmpProfile(bool skipMetadata, XmpProfile xmpProfile, ExifProfile exifProfile)
{
if (!skipMetadata && xmpProfile != null)
{
ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte)
Expand Down
91 changes: 91 additions & 0 deletions tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
Expand Down Expand Up @@ -318,4 +319,94 @@ public void Encode_PreservesMetadata<TPixel>(TestImageProvider<TPixel> provider)
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
// Load Tiff image
DecoderOptions options = new() { SkipMetadata = false };
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance, options);

ImageMetadata inputMetaData = image.Metadata;
ImageFrame<TPixel> rootFrameInput = image.Frames.RootFrame;

IptcProfile iptcProfile = new();
iptcProfile.SetValue(IptcTag.Name, "Test name");
rootFrameInput.Metadata.IptcProfile = iptcProfile;

IccProfileHeader iccProfileHeader = new() { 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<Rgba32> encodedImage = Image.Load<Rgba32>(ms);

ImageMetadata encodedImageMetaData = encodedImage.Metadata;
ImageFrame<Rgba32> 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);
}
}

0 comments on commit da4d4d7

Please sign in to comment.