Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for issue 2504 - IPTC and ICC profile information being lost during TIFF file save #2535

Merged
merged 2 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
}