Skip to content

Commit 1e53adf

Browse files
committed
Fix for issue 2504 - ensuring image and individual frame metadata are written out correctly
1 parent 54b7e04 commit 1e53adf

File tree

3 files changed

+195
-15
lines changed

3 files changed

+195
-15
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: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using SixLabors.ImageSharp.Metadata;
88
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
99
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
10+
using SixLabors.ImageSharp.PixelFormats;
1011

1112
namespace SixLabors.ImageSharp.Formats.Tiff;
1213

@@ -19,6 +20,9 @@ internal class TiffEncoderEntriesCollector
1920
public void ProcessMetadata(Image image, bool skipMetadata)
2021
=> new MetadataProcessor(this).Process(image, skipMetadata);
2122

23+
public void ProcessMetadata(ImageFrame frame, bool skipMetadata)
24+
=> new MetadataProcessor(this).Process(frame, skipMetadata);
25+
2226
public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata)
2327
=> new FrameInfoProcessor(this).Process(frame, imageMetadata);
2428

@@ -56,15 +60,30 @@ public MetadataProcessor(TiffEncoderEntriesCollector collector)
5660

5761
public void Process(Image image, bool skipMetadata)
5862
{
59-
ImageFrame rootFrame = image.Frames.RootFrame;
60-
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile;
61-
XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile;
63+
this.ProcessProfiles(image.Metadata, skipMetadata);
6264

63-
this.ProcessProfiles(image.Metadata, skipMetadata, rootFrameExifProfile, rootFrameXmpProfile);
65+
if (!skipMetadata)
66+
{
67+
this.ProcessMetadata(image.Metadata.ExifProfile ?? new ExifProfile());
68+
}
69+
70+
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software))
71+
{
72+
this.Collector.Add(new ExifString(ExifTagValue.Software)
73+
{
74+
Value = SoftwareValue
75+
});
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,16 +169,16 @@ 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)
154173
{
155-
if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None))
174+
if (!skipMetadata && (imageMetadata.ExifProfile != null && imageMetadata.ExifProfile.Parts != ExifParts.None))
156175
{
157-
foreach (IExifValue entry in exifProfile.Values)
176+
foreach (IExifValue entry in imageMetadata.ExifProfile.Values)
158177
{
159178
if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null)
160179
{
161180
ExifParts entryPart = ExifTags.GetPart(entry.Tag);
162-
if (entryPart != ExifParts.None && exifProfile.Parts.HasFlag(entryPart))
181+
if (entryPart != ExifParts.None && imageMetadata.ExifProfile.Parts.HasFlag(entryPart))
163182
{
164183
this.Collector.AddOrReplace(entry.DeepClone());
165184
}
@@ -168,7 +187,7 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
168187
}
169188
else
170189
{
171-
exifProfile?.RemoveValue(ExifTag.SubIFDOffset);
190+
imageMetadata.ExifProfile?.RemoveValue(ExifTag.SubIFDOffset);
172191
}
173192

174193
if (!skipMetadata && imageMetadata.IptcProfile != null)
@@ -183,7 +202,7 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
183202
}
184203
else
185204
{
186-
exifProfile?.RemoveValue(ExifTag.IPTC);
205+
imageMetadata.ExifProfile?.RemoveValue(ExifTag.IPTC);
187206
}
188207

189208
if (imageMetadata.IccProfile != null)
@@ -197,21 +216,86 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
197216
}
198217
else
199218
{
200-
exifProfile?.RemoveValue(ExifTag.IccProfile);
219+
imageMetadata.ExifProfile?.RemoveValue(ExifTag.IccProfile);
220+
}
221+
222+
if (!skipMetadata && imageMetadata.XmpProfile != null)
223+
{
224+
ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte)
225+
{
226+
Value = imageMetadata.XmpProfile.Data
227+
};
228+
229+
this.Collector.AddOrReplace(xmp);
230+
}
231+
else
232+
{
233+
imageMetadata.ExifProfile?.RemoveValue(ExifTag.XMP);
234+
}
235+
}
236+
237+
private void ProcessProfiles(ImageFrameMetadata frameMetadata, bool skipMetadata)
238+
{
239+
if (!skipMetadata && (frameMetadata.ExifProfile != null && frameMetadata.ExifProfile.Parts != ExifParts.None))
240+
{
241+
foreach (IExifValue entry in frameMetadata.ExifProfile.Values)
242+
{
243+
if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null)
244+
{
245+
ExifParts entryPart = ExifTags.GetPart(entry.Tag);
246+
if (entryPart != ExifParts.None && frameMetadata.ExifProfile.Parts.HasFlag(entryPart))
247+
{
248+
this.Collector.AddOrReplace(entry.DeepClone());
249+
}
250+
}
251+
}
252+
}
253+
else
254+
{
255+
frameMetadata.ExifProfile?.RemoveValue(ExifTag.SubIFDOffset);
256+
}
257+
258+
if (!skipMetadata && frameMetadata.IptcProfile != null)
259+
{
260+
frameMetadata.IptcProfile.UpdateData();
261+
ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte)
262+
{
263+
Value = frameMetadata.IptcProfile.Data
264+
};
265+
266+
this.Collector.AddOrReplace(iptc);
267+
}
268+
else
269+
{
270+
frameMetadata.ExifProfile?.RemoveValue(ExifTag.IPTC);
271+
}
272+
273+
if (frameMetadata.IccProfile != null)
274+
{
275+
ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined)
276+
{
277+
Value = frameMetadata.IccProfile.ToByteArray()
278+
};
279+
280+
this.Collector.AddOrReplace(icc);
281+
}
282+
else
283+
{
284+
frameMetadata.ExifProfile?.RemoveValue(ExifTag.IccProfile);
201285
}
202286

203-
if (!skipMetadata && xmpProfile != null)
287+
if (!skipMetadata && frameMetadata.XmpProfile != null)
204288
{
205289
ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte)
206290
{
207-
Value = xmpProfile.Data
291+
Value = frameMetadata.XmpProfile.Data
208292
};
209293

210294
this.Collector.AddOrReplace(xmp);
211295
}
212296
else
213297
{
214-
exifProfile?.RemoveValue(ExifTag.XMP);
298+
frameMetadata.ExifProfile?.RemoveValue(ExifTag.XMP);
215299
}
216300
}
217301
}

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 IccProfileHeader();
340+
iccProfileHeader.Class = IccProfileClass.ColorSpace;
341+
IccProfile iccProfile = new();
342+
rootFrameInput.Metadata.IccProfile = iccProfile;
343+
344+
TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata();
345+
XmpProfile xmpProfileInput = rootFrameInput.Metadata.XmpProfile;
346+
ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile;
347+
IptcProfile iptcProfileInput = rootFrameInput.Metadata.IptcProfile;
348+
IccProfile iccProfileInput = rootFrameInput.Metadata.IccProfile;
349+
350+
Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression);
351+
Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaInput.BitsPerPixel);
352+
353+
// Save to Tiff
354+
TiffEncoder tiffEncoder = new() { PhotometricInterpretation = TiffPhotometricInterpretation.Rgb };
355+
using MemoryStream ms = new();
356+
image.Save(ms, tiffEncoder);
357+
358+
// Assert
359+
ms.Position = 0;
360+
using Image<Rgba32> encodedImage = Image.Load<Rgba32>(ms);
361+
362+
ImageMetadata encodedImageMetaData = encodedImage.Metadata;
363+
ImageFrame<Rgba32> rootFrameEncodedImage = encodedImage.Frames.RootFrame;
364+
TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata();
365+
ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile;
366+
XmpProfile encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile;
367+
IptcProfile encodedImageIptcProfile = rootFrameEncodedImage.Metadata.IptcProfile;
368+
IccProfile encodedImageIccProfile = rootFrameEncodedImage.Metadata.IccProfile;
369+
370+
Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel);
371+
Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression);
372+
373+
Assert.Equal(inputMetaData.HorizontalResolution, encodedImageMetaData.HorizontalResolution);
374+
Assert.Equal(inputMetaData.VerticalResolution, encodedImageMetaData.VerticalResolution);
375+
Assert.Equal(inputMetaData.ResolutionUnits, encodedImageMetaData.ResolutionUnits);
376+
377+
Assert.Equal(rootFrameInput.Width, rootFrameEncodedImage.Width);
378+
Assert.Equal(rootFrameInput.Height, rootFrameEncodedImage.Height);
379+
380+
PixelResolutionUnit resolutionUnitInput = UnitConverter.ExifProfileToResolutionUnit(exifProfileInput);
381+
PixelResolutionUnit resolutionUnitEncoded = UnitConverter.ExifProfileToResolutionUnit(encodedImageExifProfile);
382+
Assert.Equal(resolutionUnitInput, resolutionUnitEncoded);
383+
Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble());
384+
Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble());
385+
386+
Assert.NotNull(xmpProfileInput);
387+
Assert.NotNull(encodedImageXmpProfile);
388+
Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data);
389+
390+
Assert.NotNull(iptcProfileInput);
391+
Assert.NotNull(encodedImageIptcProfile);
392+
Assert.Equal(iptcProfileInput.Data, encodedImageIptcProfile.Data);
393+
Assert.Equal(iptcProfileInput.GetValues(IptcTag.Name)[0].Value, encodedImageIptcProfile.GetValues(IptcTag.Name)[0].Value);
394+
395+
Assert.NotNull(iccProfileInput);
396+
Assert.NotNull(encodedImageIccProfile);
397+
Assert.Equal(iccProfileInput.Entries.Length, encodedImageIccProfile.Entries.Length);
398+
Assert.Equal(iccProfileInput.Header.Class, encodedImageIccProfile.Header.Class);
399+
400+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Software).Value, encodedImageExifProfile.GetValue(ExifTag.Software).Value);
401+
Assert.Equal(exifProfileInput.GetValue(ExifTag.ImageDescription).Value, encodedImageExifProfile.GetValue(ExifTag.ImageDescription).Value);
402+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Make).Value, encodedImageExifProfile.GetValue(ExifTag.Make).Value);
403+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Copyright).Value, encodedImageExifProfile.GetValue(ExifTag.Copyright).Value);
404+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Artist).Value, encodedImageExifProfile.GetValue(ExifTag.Artist).Value);
405+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Orientation).Value, encodedImageExifProfile.GetValue(ExifTag.Orientation).Value);
406+
Assert.Equal(exifProfileInput.GetValue(ExifTag.Model).Value, encodedImageExifProfile.GetValue(ExifTag.Model).Value);
407+
408+
Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value);
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)