Skip to content

Commit da4d4d7

Browse files
Merge pull request #2535 from JeffP134/main
Fix for issue 2504 - IPTC and ICC profile information being lost during TIFF file save
2 parents 54b7e04 + bf261f0 commit da4d4d7

File tree

3 files changed

+151
-11
lines changed

3 files changed

+151
-11
lines changed

src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
157157
long ifdMarker = WriteHeader(writer, buffer);
158158

159159
Image<TPixel> metadataImage = image;
160+
160161
foreach (ImageFrame<TPixel> frame in image.Frames)
161162
{
162163
cancellationToken.ThrowIfCancellationRequested();
@@ -235,9 +236,13 @@ private long WriteFrame<TPixel>(
235236

236237
if (image != null)
237238
{
239+
// Write the metadata for the root image
238240
entriesCollector.ProcessMetadata(image, this.skipMetadata);
239241
}
240242

243+
// Write the metadata for the frame
244+
entriesCollector.ProcessMetadata(frame, this.skipMetadata);
245+
241246
entriesCollector.ProcessFrameInfo(frame, imageMetadata);
242247
entriesCollector.ProcessImageFormat(this);
243248

src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using SixLabors.ImageSharp.Formats.Tiff.Constants;
77
using SixLabors.ImageSharp.Metadata;
88
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
9+
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
10+
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
911
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
1012

1113
namespace SixLabors.ImageSharp.Formats.Tiff;
@@ -19,6 +21,9 @@ internal class TiffEncoderEntriesCollector
1921
public void ProcessMetadata(Image image, bool skipMetadata)
2022
=> new MetadataProcessor(this).Process(image, skipMetadata);
2123

24+
public void ProcessMetadata(ImageFrame frame, bool skipMetadata)
25+
=> new MetadataProcessor(this).Process(frame, skipMetadata);
26+
2227
public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata)
2328
=> new FrameInfoProcessor(this).Process(frame, imageMetadata);
2429

@@ -56,15 +61,29 @@ public MetadataProcessor(TiffEncoderEntriesCollector collector)
5661

5762
public void Process(Image image, bool skipMetadata)
5863
{
59-
ImageFrame rootFrame = image.Frames.RootFrame;
60-
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile;
61-
XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile;
64+
this.ProcessProfiles(image.Metadata, skipMetadata);
6265

63-
this.ProcessProfiles(image.Metadata, skipMetadata, rootFrameExifProfile, rootFrameXmpProfile);
66+
if (!skipMetadata)
67+
{
68+
this.ProcessMetadata(image.Metadata.ExifProfile ?? new ExifProfile());
69+
}
70+
71+
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software))
72+
{
73+
this.Collector.Add(new ExifString(ExifTagValue.Software)
74+
{
75+
Value = SoftwareValue
76+
});
77+
}
78+
}
79+
80+
public void Process(ImageFrame frame, bool skipMetadata)
81+
{
82+
this.ProcessProfiles(frame.Metadata, skipMetadata);
6483

6584
if (!skipMetadata)
6685
{
67-
this.ProcessMetadata(rootFrameExifProfile ?? new ExifProfile());
86+
this.ProcessMetadata(frame.Metadata.ExifProfile ?? new ExifProfile());
6887
}
6988

7089
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software))
@@ -150,7 +169,23 @@ private void ProcessMetadata(ExifProfile exifProfile)
150169
}
151170
}
152171

153-
private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, ExifProfile exifProfile, XmpProfile xmpProfile)
172+
private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata)
173+
{
174+
this.ProcessExifProfile(skipMetadata, imageMetadata.ExifProfile);
175+
this.ProcessIptcProfile(skipMetadata, imageMetadata.IptcProfile, imageMetadata.ExifProfile);
176+
this.ProcessIccProfile(imageMetadata.IccProfile, imageMetadata.ExifProfile);
177+
this.ProcessXmpProfile(skipMetadata, imageMetadata.XmpProfile, imageMetadata.ExifProfile);
178+
}
179+
180+
private void ProcessProfiles(ImageFrameMetadata frameMetadata, bool skipMetadata)
181+
{
182+
this.ProcessExifProfile(skipMetadata, frameMetadata.ExifProfile);
183+
this.ProcessIptcProfile(skipMetadata, frameMetadata.IptcProfile, frameMetadata.ExifProfile);
184+
this.ProcessIccProfile(frameMetadata.IccProfile, frameMetadata.ExifProfile);
185+
this.ProcessXmpProfile(skipMetadata, frameMetadata.XmpProfile, frameMetadata.ExifProfile);
186+
}
187+
188+
private void ProcessExifProfile(bool skipMetadata, ExifProfile exifProfile)
154189
{
155190
if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None))
156191
{
@@ -170,13 +205,16 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
170205
{
171206
exifProfile?.RemoveValue(ExifTag.SubIFDOffset);
172207
}
208+
}
173209

174-
if (!skipMetadata && imageMetadata.IptcProfile != null)
210+
private void ProcessIptcProfile(bool skipMetadata, IptcProfile iptcProfile, ExifProfile exifProfile)
211+
{
212+
if (!skipMetadata && iptcProfile != null)
175213
{
176-
imageMetadata.IptcProfile.UpdateData();
214+
iptcProfile.UpdateData();
177215
ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte)
178216
{
179-
Value = imageMetadata.IptcProfile.Data
217+
Value = iptcProfile.Data
180218
};
181219

182220
this.Collector.AddOrReplace(iptc);
@@ -185,12 +223,15 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
185223
{
186224
exifProfile?.RemoveValue(ExifTag.IPTC);
187225
}
226+
}
188227

189-
if (imageMetadata.IccProfile != null)
228+
private void ProcessIccProfile(IccProfile iccProfile, ExifProfile exifProfile)
229+
{
230+
if (iccProfile != null)
190231
{
191232
ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined)
192233
{
193-
Value = imageMetadata.IccProfile.ToByteArray()
234+
Value = iccProfile.ToByteArray()
194235
};
195236

196237
this.Collector.AddOrReplace(icc);
@@ -199,7 +240,10 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
199240
{
200241
exifProfile?.RemoveValue(ExifTag.IccProfile);
201242
}
243+
}
202244

245+
private void ProcessXmpProfile(bool skipMetadata, XmpProfile xmpProfile, ExifProfile exifProfile)
246+
{
203247
if (!skipMetadata && xmpProfile != null)
204248
{
205249
ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte)

tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using SixLabors.ImageSharp.Formats.Tiff.Constants;
88
using SixLabors.ImageSharp.Metadata;
99
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
10+
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
1011
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
1112
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
1213
using SixLabors.ImageSharp.PixelFormats;
@@ -318,4 +319,94 @@ public void Encode_PreservesMetadata<TPixel>(TestImageProvider<TPixel> provider)
318319
Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value);
319320
Assert.Equal(exifProfileInput.Values.Count, encodedImageExifProfile.Values.Count);
320321
}
322+
323+
[Theory]
324+
[WithFile(SampleMetadata, PixelTypes.Rgba32)]
325+
public void Encode_PreservesMetadata_IptcAndIcc<TPixel>(TestImageProvider<TPixel> provider)
326+
where TPixel : unmanaged, IPixel<TPixel>
327+
{
328+
// Load Tiff image
329+
DecoderOptions options = new() { SkipMetadata = false };
330+
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance, options);
331+
332+
ImageMetadata inputMetaData = image.Metadata;
333+
ImageFrame<TPixel> rootFrameInput = image.Frames.RootFrame;
334+
335+
IptcProfile iptcProfile = new();
336+
iptcProfile.SetValue(IptcTag.Name, "Test name");
337+
rootFrameInput.Metadata.IptcProfile = iptcProfile;
338+
339+
IccProfileHeader iccProfileHeader = new() { Class = IccProfileClass.ColorSpace };
340+
IccProfile iccProfile = new();
341+
rootFrameInput.Metadata.IccProfile = iccProfile;
342+
343+
TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata();
344+
XmpProfile xmpProfileInput = rootFrameInput.Metadata.XmpProfile;
345+
ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile;
346+
IptcProfile iptcProfileInput = rootFrameInput.Metadata.IptcProfile;
347+
IccProfile iccProfileInput = rootFrameInput.Metadata.IccProfile;
348+
349+
Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression);
350+
Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaInput.BitsPerPixel);
351+
352+
// Save to Tiff
353+
TiffEncoder tiffEncoder = new() { PhotometricInterpretation = TiffPhotometricInterpretation.Rgb };
354+
using MemoryStream ms = new();
355+
image.Save(ms, tiffEncoder);
356+
357+
// Assert
358+
ms.Position = 0;
359+
using Image<Rgba32> encodedImage = Image.Load<Rgba32>(ms);
360+
361+
ImageMetadata encodedImageMetaData = encodedImage.Metadata;
362+
ImageFrame<Rgba32> rootFrameEncodedImage = encodedImage.Frames.RootFrame;
363+
TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata();
364+
ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile;
365+
XmpProfile encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile;
366+
IptcProfile encodedImageIptcProfile = rootFrameEncodedImage.Metadata.IptcProfile;
367+
IccProfile encodedImageIccProfile = rootFrameEncodedImage.Metadata.IccProfile;
368+
369+
Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel);
370+
Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression);
371+
372+
Assert.Equal(inputMetaData.HorizontalResolution, encodedImageMetaData.HorizontalResolution);
373+
Assert.Equal(inputMetaData.VerticalResolution, encodedImageMetaData.VerticalResolution);
374+
Assert.Equal(inputMetaData.ResolutionUnits, encodedImageMetaData.ResolutionUnits);
375+
376+
Assert.Equal(rootFrameInput.Width, rootFrameEncodedImage.Width);
377+
Assert.Equal(rootFrameInput.Height, rootFrameEncodedImage.Height);
378+
379+
PixelResolutionUnit resolutionUnitInput = UnitConverter.ExifProfileToResolutionUnit(exifProfileInput);
380+
PixelResolutionUnit resolutionUnitEncoded = UnitConverter.ExifProfileToResolutionUnit(encodedImageExifProfile);
381+
Assert.Equal(resolutionUnitInput, resolutionUnitEncoded);
382+
Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble());
383+
Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble());
384+
385+
Assert.NotNull(xmpProfileInput);
386+
Assert.NotNull(encodedImageXmpProfile);
387+
Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data);
388+
389+
Assert.NotNull(iptcProfileInput);
390+
Assert.NotNull(encodedImageIptcProfile);
391+
Assert.Equal(iptcProfileInput.Data, encodedImageIptcProfile.Data);
392+
Assert.Equal(iptcProfileInput.GetValues(IptcTag.Name)[0].Value, encodedImageIptcProfile.GetValues(IptcTag.Name)[0].Value);
393+
394+
Assert.NotNull(iccProfileInput);
395+
Assert.NotNull(encodedImageIccProfile);
396+
Assert.Equal(iccProfileInput.Entries.Length, encodedImageIccProfile.Entries.Length);
397+
Assert.Equal(iccProfileInput.Header.Class, encodedImageIccProfile.Header.Class);
398+
399+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Software).Value, encodedImageExifProfile.GetValue(ExifTag.Software).Value);
400+
Assert.Equal(exifProfileInput.GetValue(ExifTag.ImageDescription).Value, encodedImageExifProfile.GetValue(ExifTag.ImageDescription).Value);
401+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Make).Value, encodedImageExifProfile.GetValue(ExifTag.Make).Value);
402+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Copyright).Value, encodedImageExifProfile.GetValue(ExifTag.Copyright).Value);
403+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Artist).Value, encodedImageExifProfile.GetValue(ExifTag.Artist).Value);
404+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Orientation).Value, encodedImageExifProfile.GetValue(ExifTag.Orientation).Value);
405+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Model).Value, encodedImageExifProfile.GetValue(ExifTag.Model).Value);
406+
407+
Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value);
408+
409+
// Adding the IPTC and ICC profiles dynamically increments the number of values in the original EXIF profile by 2
410+
Assert.Equal(exifProfileInput.Values.Count + 2, encodedImageExifProfile.Values.Count);
411+
}
321412
}

0 commit comments

Comments
 (0)