Browse Source

Merge pull request #3159 from SixLabors/fix/3145-preserve-tiff-icc-profile

Clone TIFF profiles into image metadata
pull/3160/head
James Jackson-South 3 weeks ago
committed by GitHub
parent
commit
776cb8bee3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  2. 7
      src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
  3. 15
      tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs
  4. 16
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  5. 13
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

18
src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs

@ -22,8 +22,6 @@ internal static class TiffDecoderMetadataCreator
TiffThrowHelper.ThrowImageFormatException("Expected at least one frame.");
}
ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0]);
if (!ignoreMetadata)
{
for (int i = 0; i < frames.Count; i++)
@ -43,12 +41,24 @@ internal static class TiffDecoderMetadataCreator
}
}
return imageMetaData;
// IPTC and XMP are materialized from IFD tags above, so image-level metadata
// must be created after all frame profiles are available.
return Create(byteOrder, isBigTiff, frames[0]);
}
private static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ImageFrameMetadata rootFrameMetadata)
{
ImageMetadata imageMetaData = new();
// TIFF stores metadata per IFD, while ImageMetadata is the source used by single-frame encoders.
// Mirror the root IFD profiles at image scope so cross-format encoding preserves them. Each profile
// is cloned because image-level and frame-level metadata are independently mutable public APIs.
ImageMetadata imageMetaData = new()
{
ExifProfile = rootFrameMetadata.ExifProfile?.DeepClone(),
IccProfile = rootFrameMetadata.IccProfile?.DeepClone(),
IptcProfile = rootFrameMetadata.IptcProfile?.DeepClone(),
XmpProfile = rootFrameMetadata.XmpProfile?.DeepClone()
};
SetResolution(imageMetaData, rootFrameMetadata.ExifProfile);
TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata();

7
src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs

@ -403,6 +403,13 @@ internal sealed class ExifWriter
return WriteUInt32((uint)longNumber, destination, offset);
}
// ExifLong8Array retains ulong storage but reports Long when every value fits
// in 32 bits, allowing BigTIFF offsets to be serialized by classic EXIF writers.
if (value is ulong long8Value)
{
return WriteUInt32((uint)long8Value, destination, offset);
}
return WriteUInt32((uint)value, destination, offset);
case ExifDataType.Long8:
return WriteUInt64((ulong)value, destination, offset);

15
tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs

@ -88,6 +88,21 @@ public class BigTiffMetadataTests
Assert.Equal(ExifDataType.Long8, long8.DataType);
}
[Fact]
public void ExifLong8Array_CanWriteValuesAsLong()
{
ExifLong8Array long8 = new(ExifTagValue.StripOffsets);
Assert.True(long8.TrySetValue(new long[] { 1, uint.MaxValue }));
Assert.Equal(ExifDataType.Long, long8.DataType);
byte[] buffer = new byte[8];
int written = ExifWriter.WriteValue(long8, buffer, 0);
Assert.Equal(buffer.Length, written);
Assert.Equal(1U, BinaryPrimitives.ReadUInt32LittleEndian(buffer));
Assert.Equal(uint.MaxValue, BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(4)));
}
[Fact]
public void ExifSignedLong8Array()
{

16
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -370,6 +370,22 @@ public class TiffDecoderTests : TiffDecoderBaseTester
Assert.Null(image.Metadata.IccProfile);
}
[Theory]
[WithFile(Icc.PerceptualRgb8, PixelTypes.Rgba32)]
[WithFile(Icc.PerceptualRgb16, PixelTypes.Rgba32)]
public void Decode_WhenColorProfileHandlingIsPreserve_PreservesIccProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance, options);
Assert.NotNull(image.Metadata.IccProfile);
Assert.NotSame(image.Frames.RootFrame.Metadata.IccProfile, image.Metadata.IccProfile);
Assert.Equal(
image.Frames.RootFrame.Metadata.IccProfile.ToByteArray(),
image.Metadata.IccProfile.ToByteArray());
}
[Theory]
[WithFile(Issues2454_A, PixelTypes.Rgba32)]
[WithFile(Issues2454_B, PixelTypes.Rgba32)]

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

@ -150,13 +150,22 @@ public class TiffMetadataTests
Assert.NotNull(meta);
if (ignoreMetadata)
{
Assert.Null(image.Metadata.XmpProfile);
Assert.Null(image.Metadata.ExifProfile);
Assert.Null(rootFrameMetaData.XmpProfile);
Assert.Null(rootFrameMetaData.ExifProfile);
}
else
{
Assert.NotNull(image.Metadata.XmpProfile);
Assert.NotNull(image.Metadata.ExifProfile);
Assert.NotNull(rootFrameMetaData.XmpProfile);
Assert.NotNull(rootFrameMetaData.ExifProfile);
Assert.NotSame(rootFrameMetaData.XmpProfile, image.Metadata.XmpProfile);
Assert.NotSame(rootFrameMetaData.ExifProfile, image.Metadata.ExifProfile);
Assert.Equal(rootFrameMetaData.XmpProfile.Data, image.Metadata.XmpProfile.Data);
Assert.Equal(rootFrameMetaData.ExifProfile.ToByteArray(), image.Metadata.ExifProfile.ToByteArray());
Assert.Equal(2596, rootFrameMetaData.XmpProfile.Data.Length); // padding bytes are trimmed
Assert.Equal(25, rootFrameMetaData.ExifProfile.Values.Count);
}
@ -171,6 +180,10 @@ public class TiffMetadataTests
IptcProfile iptcProfile = image.Frames.RootFrame.Metadata.IptcProfile;
Assert.NotNull(iptcProfile);
Assert.NotNull(image.Metadata.IptcProfile);
Assert.NotSame(iptcProfile, image.Metadata.IptcProfile);
Assert.Equal(iptcProfile.Data, image.Metadata.IptcProfile.Data);
IptcValue byline = iptcProfile.Values.FirstOrDefault(data => data.Tag == IptcTag.Byline);
Assert.NotNull(byline);
Assert.Equal("Studio Mantyniemi", byline.Value);

Loading…
Cancel
Save