diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 637373e804..ff3d5c7b0d 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -11,7 +11,7 @@ on:
branches:
- main
- release/*
- types: [ labeled, opened, synchronize, reopened ]
+ types: [ opened, synchronize, reopened ]
jobs:
# Prime a single LFS cache and expose the exact key for the matrix
diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs
index bb6c2a2825..d4d80fc123 100644
--- a/src/ImageSharp/Formats/DecoderOptions.cs
+++ b/src/ImageSharp/Formats/DecoderOptions.cs
@@ -78,12 +78,12 @@ public sealed class DecoderOptions
return false;
}
- if (profile.IsCanonicalSrgbMatrixTrc())
+ if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
{
return false;
}
- if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
+ if (profile.IsCanonicalSrgbMatrixTrc())
{
return false;
}
@@ -99,11 +99,11 @@ public sealed class DecoderOptions
return false;
}
- if (this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc())
+ if (this.ColorProfileHandling == ColorProfileHandling.Convert)
{
return true;
}
- return this.ColorProfileHandling == ColorProfileHandling.Convert;
+ return this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc();
}
}
diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs
index c18fc663b5..2a5d44a355 100644
--- a/src/ImageSharp/Formats/ImageDecoder.cs
+++ b/src/ImageSharp/Formats/ImageDecoder.cs
@@ -328,6 +328,14 @@ public abstract class ImageDecoder : IImageDecoder
{
image.Metadata.IccProfile = null;
}
+
+ foreach (ImageFrame frame in image.Frames)
+ {
+ if (options.CanRemoveIccProfile(frame.Metadata.IccProfile))
+ {
+ frame.Metadata.IccProfile = null;
+ }
+ }
}
private static void HandleIccProfile(DecoderOptions options, ImageInfo image)
@@ -336,5 +344,13 @@ public abstract class ImageDecoder : IImageDecoder
{
image.Metadata.IccProfile = null;
}
+
+ foreach (ImageFrameMetadata frame in image.FrameMetadataCollection)
+ {
+ if (options.CanRemoveIccProfile(frame.IccProfile))
+ {
+ frame.IccProfile = null;
+ }
+ }
}
}
diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs
index da50a1abec..f6b1a92a03 100644
--- a/src/ImageSharp/Formats/ImageDecoderCore.cs
+++ b/src/ImageSharp/Formats/ImageDecoderCore.cs
@@ -5,6 +5,7 @@ using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
@@ -164,4 +165,56 @@ internal abstract class ImageDecoderCore
converter.Convert(image);
return true;
}
+
+ ///
+ /// Converts the ICC color profile of the specified image frame to the compact sRGB v4 profile if a source profile is
+ /// available.
+ ///
+ ///
+ /// This method should only be used by decoders that gurantee that the encoded image data is in a color space
+ /// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB).
+ ///
+ /// If the image does not have a valid ICC profile for color conversion, no changes are made.
+ /// This operation may affect the color appearance of the image to ensure consistency with the sRGB color
+ /// space.
+ ///
+ /// The pixel format.
+ /// The image frame whose ICC profile will be converted to the compact sRGB v4 profile.
+ ///
+ /// if the conversion was performed; otherwise, .
+ ///
+ protected bool TryConvertIccProfile(ImageFrame frame)
+ where TPixel : unmanaged, IPixel
+ {
+ if (!this.Options.TryGetIccProfileForColorConversion(frame.Metadata.IccProfile, out IccProfile? profile))
+ {
+ return false;
+ }
+
+ ColorConversionOptions options = new()
+ {
+ SourceIccProfile = profile,
+ TargetIccProfile = CompactSrgbV4Profile.Profile,
+ MemoryAllocator = frame.Configuration.MemoryAllocator,
+ };
+
+ ColorProfileConverter converter = new(options);
+
+ ImageMetadata metadata = new()
+ {
+ IccProfile = frame.Metadata.IccProfile
+ };
+
+ IMemoryGroup m = frame.PixelBuffer.MemoryGroup;
+
+ // Safe: ToArray only materializes the Memory segment list, not the underlying pixel buffers,
+ // and Wrap(Memory[]) creates a Consumed MemoryGroup that does not own the buffers (Dispose just
+ // invalidates the view). This means no pixel data is cloned and disposing the temporary image will
+ // not dispose or leak the frame's pixel buffer.
+ MemoryGroup memorySource = MemoryGroup.Wrap(m.ToArray());
+
+ using Image image = new(frame.Configuration, memorySource, frame.Width, frame.Height, metadata);
+ converter.Convert(image);
+ return true;
+ }
}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs
new file mode 100644
index 0000000000..b422e65adc
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs
@@ -0,0 +1,144 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.ColorProfiles;
+using SixLabors.ImageSharp.ColorProfiles.Icc;
+using SixLabors.ImageSharp.Formats.Tiff.Utils;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
+
+///
+/// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration.
+/// Each channel is represented with 16 bits.
+///
+/// The type of pixel format.
+internal class CieLab16PlanarTiffColor : TiffBasePlanarColorDecoder
+ where TPixel : unmanaged, IPixel
+{
+ private readonly ColorProfileConverter colorProfileConverter;
+ private readonly Configuration configuration;
+ private readonly bool isBigEndian;
+
+ // libtiff encodes 16-bit Lab as:
+ // L* : unsigned [0, 65535] mapping to [0, 100]
+ // a*, b* : signed [-32768, 32767], values are 256x the 1976 a*, b* values.
+ private const float Inv65535 = 1f / 65535f;
+ private const float Inv256 = 1f / 256f;
+
+ public CieLab16PlanarTiffColor(
+ Configuration configuration,
+ DecoderOptions decoderOptions,
+ ImageFrameMetadata metadata,
+ MemoryAllocator allocator,
+ bool isBigEndian)
+ {
+ this.isBigEndian = isBigEndian;
+ this.configuration = configuration;
+
+ if (decoderOptions.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile))
+ {
+ ColorConversionOptions options = new()
+ {
+ SourceIccProfile = iccProfile,
+ TargetIccProfile = CompactSrgbV4Profile.Profile,
+ MemoryAllocator = allocator
+ };
+
+ this.colorProfileConverter = new ColorProfileConverter(options);
+ }
+ else
+ {
+ ColorConversionOptions options = new()
+ {
+ MemoryAllocator = allocator
+ };
+
+ this.colorProfileConverter = new ColorProfileConverter(options);
+ }
+ }
+
+ ///
+ public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height)
+ {
+ Span lPlane = data[0].GetSpan();
+ Span aPlane = data[1].GetSpan();
+ Span bPlane = data[2].GetSpan();
+
+ // Allocate temporary buffers to hold the LAB -> RGB conversion.
+ // This should be the maximum width of a row.
+ using IMemoryOwner rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width);
+ using IMemoryOwner vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width);
+
+ Span rgbRow = rgbBuffer.Memory.Span;
+ Span vectorRow = vectorBuffer.Memory.Span;
+
+ // Reuse the rgbRow span for lab data since both are 3-float structs, avoiding an extra allocation.
+ Span cieLabRow = MemoryMarshal.Cast(rgbRow);
+
+ int stride = width * 2;
+
+ if (this.isBigEndian)
+ {
+ for (int y = 0; y < height; y++)
+ {
+ int rowBase = y * stride;
+ Span pixelRow = pixels.DangerousGetRowSpan(top + y).Slice(left, width);
+
+ for (int x = 0; x < width; x++)
+ {
+ int i = rowBase + (x * 2);
+
+ ushort lRaw = TiffUtilities.ConvertToUShortBigEndian(lPlane.Slice(i, 2));
+ short aRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(aPlane.Slice(i, 2)));
+ short bRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(bPlane.Slice(i, 2)));
+
+ float l = lRaw * 100f * Inv65535;
+ float a = aRaw * Inv256;
+ float b = bRaw * Inv256;
+
+ cieLabRow[x] = new CieLab(l, a, b);
+ }
+
+ // Convert CIE Lab -> Rgb -> Vector4 -> TPixel
+ this.colorProfileConverter.Convert(cieLabRow, rgbRow);
+ Rgb.ToScaledVector4(rgbRow, vectorRow);
+ PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
+ }
+
+ return;
+ }
+
+ for (int y = 0; y < height; y++)
+ {
+ int rowBase = y * stride;
+ Span pixelRow = pixels.DangerousGetRowSpan(top + y).Slice(left, width);
+
+ for (int x = 0; x < width; x++)
+ {
+ int i = rowBase + (x * 2);
+
+ ushort lRaw = TiffUtilities.ConvertToUShortLittleEndian(lPlane.Slice(i, 2));
+ short aRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(aPlane.Slice(i, 2)));
+ short bRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(bPlane.Slice(i, 2)));
+
+ float l = lRaw * 100f * Inv65535;
+ float a = aRaw * Inv256;
+ float b = bRaw * Inv256;
+
+ cieLabRow[x] = new CieLab(l, a, b);
+ }
+
+ // Convert CIE Lab -> Rgb -> Vector4 -> TPixel
+ this.colorProfileConverter.Convert(cieLabRow, rgbRow);
+ Rgb.ToScaledVector4(rgbRow, vectorRow);
+ PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs
new file mode 100644
index 0000000000..4455753bf3
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.ColorProfiles;
+using SixLabors.ImageSharp.ColorProfiles.Icc;
+using SixLabors.ImageSharp.Formats.Tiff.Utils;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
+
+///
+/// Implements decoding pixel data with photometric interpretation of type 'CieLab'.
+/// Each channel is represented with 16 bits.
+///
+/// The type of pixel format.
+internal class CieLab16TiffColor : TiffBaseColorDecoder
+ where TPixel : unmanaged, IPixel
+{
+ private readonly ColorProfileConverter colorProfileConverter;
+ private readonly Configuration configuration;
+ private readonly bool isBigEndian;
+
+ // libtiff encodes 16-bit Lab as:
+ // L* : unsigned [0, 65535] mapping to [0, 100]
+ // a*, b* : signed [-32768, 32767], values are 256x the 1976 a*, b* values.
+ private const float Inv65535 = 1f / 65535f;
+ private const float Inv256 = 1f / 256f;
+
+ public CieLab16TiffColor(
+ Configuration configuration,
+ DecoderOptions decoderOptions,
+ ImageFrameMetadata metadata,
+ MemoryAllocator allocator,
+ bool isBigEndian)
+ {
+ this.isBigEndian = isBigEndian;
+ this.configuration = configuration;
+
+ if (decoderOptions.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile))
+ {
+ ColorConversionOptions options = new()
+ {
+ SourceIccProfile = iccProfile,
+ TargetIccProfile = CompactSrgbV4Profile.Profile,
+ MemoryAllocator = allocator
+ };
+
+ this.colorProfileConverter = new ColorProfileConverter(options);
+ }
+ else
+ {
+ ColorConversionOptions options = new()
+ {
+ MemoryAllocator = allocator
+ };
+
+ this.colorProfileConverter = new ColorProfileConverter(options);
+ }
+ }
+
+ ///
+ public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height)
+ {
+ int offset = 0;
+
+ // Allocate temporary buffers to hold the LAB -> RGB conversion.
+ // This should be the maximum width of a row.
+ using IMemoryOwner rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width);
+ using IMemoryOwner vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width);
+
+ Span rgbRow = rgbBuffer.Memory.Span;
+ Span vectorRow = vectorBuffer.Memory.Span;
+
+ // Reuse the rgbRow span for lab data since both are 3-float structs, avoiding an extra allocation.
+ Span cieLabRow = MemoryMarshal.Cast(rgbRow);
+
+ if (this.isBigEndian)
+ {
+ for (int y = top; y < top + height; y++)
+ {
+ Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
+
+ for (int x = 0; x < pixelRow.Length; x++)
+ {
+ ushort lRaw = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2));
+ offset += 2;
+ short aRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2)));
+ offset += 2;
+ short bRaw = unchecked((short)TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2)));
+ offset += 2;
+
+ float l = lRaw * 100f * Inv65535;
+ float a = aRaw * Inv256;
+ float b = bRaw * Inv256;
+
+ cieLabRow[x] = new CieLab(l, a, b);
+ }
+
+ // Convert CIE Lab -> Rgb -> Vector4 -> TPixel
+ this.colorProfileConverter.Convert(cieLabRow, rgbRow);
+ Rgb.ToScaledVector4(rgbRow, vectorRow);
+ PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
+ }
+
+ return;
+ }
+
+ for (int y = top; y < top + height; y++)
+ {
+ Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
+
+ for (int x = 0; x < pixelRow.Length; x++)
+ {
+ ushort lRaw = TiffUtilities.ConvertToUShortLittleEndian(data.Slice(offset, 2));
+ offset += 2;
+ short aRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(data.Slice(offset, 2)));
+ offset += 2;
+ short bRaw = unchecked((short)TiffUtilities.ConvertToUShortLittleEndian(data.Slice(offset, 2)));
+ offset += 2;
+
+ float l = lRaw * 100f * Inv65535;
+ float a = aRaw * Inv256;
+ float b = bRaw * Inv256;
+
+ cieLabRow[x] = new CieLab(l, a, b);
+ }
+
+ // Convert CIE Lab -> Rgb -> Vector4 -> TPixel
+ this.colorProfileConverter.Convert(cieLabRow, rgbRow);
+ Rgb.ToScaledVector4(rgbRow, vectorRow);
+ PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs
similarity index 94%
rename from src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs
rename to src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs
index d23d1e2909..1252f1d3d2 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs
@@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
/// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration.
///
/// The type of pixel format.
-internal class CieLabPlanarTiffColor : TiffBasePlanarColorDecoder
+internal class CieLab8PlanarTiffColor : TiffBasePlanarColorDecoder
where TPixel : unmanaged, IPixel
{
private static readonly ColorProfileConverter ColorProfileConverter = new();
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs
similarity index 83%
rename from src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs
rename to src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs
index b10d27ccda..ce5bed53c4 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs
@@ -10,14 +10,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
///
/// Implements decoding pixel data with photometric interpretation of type 'CieLab'.
+/// Each channel is represented with 8 bits.
///
/// The type of pixel format.
-internal class CieLabTiffColor : TiffBaseColorDecoder
+internal class CieLab8TiffColor : TiffBaseColorDecoder
where TPixel : unmanaged, IPixel
{
private static readonly ColorProfileConverter ColorProfileConverter = new();
private const float Inv255 = 1f / 255f;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CieLab8TiffColor()
+ {
+ }
+
///
public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height)
{
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs
index 2e22fcde03..509056267b 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs
@@ -1,10 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Buffers;
using System.Numerics;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.ColorProfiles;
+using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
@@ -12,18 +17,48 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
internal class CmykTiffColor : TiffBaseColorDecoder
where TPixel : unmanaged, IPixel
{
- private static readonly ColorProfileConverter ColorProfileConverter = new();
+ private readonly ColorProfileConverter colorProfileConverter;
+ private readonly Configuration configuration;
private const float Inv255 = 1f / 255f;
private readonly TiffDecoderCompressionType compression;
- public CmykTiffColor(TiffDecoderCompressionType compression) => this.compression = compression;
+ public CmykTiffColor(
+ TiffDecoderCompressionType compression,
+ Configuration configuration,
+ DecoderOptions decoderOptions,
+ ImageFrameMetadata metadata,
+ MemoryAllocator allocator)
+ {
+ this.compression = compression;
+ this.configuration = configuration;
+
+ if (decoderOptions.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile))
+ {
+ ColorConversionOptions options = new()
+ {
+ SourceIccProfile = iccProfile,
+ TargetIccProfile = CompactSrgbV4Profile.Profile,
+ MemoryAllocator = allocator
+ };
+
+ this.colorProfileConverter = new ColorProfileConverter(options);
+ }
+ else
+ {
+ ColorConversionOptions options = new()
+ {
+ MemoryAllocator = allocator
+ };
+
+ this.colorProfileConverter = new ColorProfileConverter(options);
+ }
+ }
///
public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height)
{
int offset = 0;
-
if (this.compression == TiffDecoderCompressionType.Jpeg)
{
for (int y = top; y < top + height; y++)
@@ -40,17 +75,31 @@ internal class CmykTiffColor : TiffBaseColorDecoder
return;
}
+ // Allocate temporary buffers to hold the CMYK -> RGB conversion.
+ // This should be the maximum width of a row.
+ using IMemoryOwner rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width);
+ using IMemoryOwner vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate(width);
+
+ Span rgbRow = rgbBuffer.Memory.Span;
+ Span vectorRow = vectorBuffer.Memory.Span;
+
+ // Reuse the Vector4 buffer as CMYK storage since both are 4-float structs, avoiding an extra allocation.
+ Span cmykRow = MemoryMarshal.Cast(vectorRow);
+
for (int y = top; y < top + height; y++)
{
Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
- for (int x = 0; x < pixelRow.Length; x++)
- {
- Cmyk cmyk = new(data[offset] * Inv255, data[offset + 1] * Inv255, data[offset + 2] * Inv255, data[offset + 3] * Inv255);
- Rgb rgb = ColorProfileConverter.Convert(in cmyk);
- pixelRow[x] = TPixel.FromScaledVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f));
- offset += 4;
- }
+ // Collect CMYK pixels.
+ // ByteToNormalizedFloat efficiently converts packed 4-byte component data
+ // to normalized 0-1 floats using SIMD.
+ SimdUtils.ByteToNormalizedFloat(data.Slice(offset, width * 4), MemoryMarshal.Cast(cmykRow));
+ offset += width * 4;
+
+ // Convert CMYK -> RGB -> Vector4 -> TPixel
+ this.colorProfileConverter.Convert(cmykRow, rgbRow);
+ Rgb.ToScaledVector4(rgbRow, vectorRow);
+ PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
}
}
}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
index e2eb82e3b4..270c7f356c 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
@@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
@@ -11,6 +12,8 @@ internal static class TiffColorDecoderFactory
where TPixel : unmanaged, IPixel
{
public static TiffBaseColorDecoder Create(
+ ImageFrameMetadata metadata,
+ DecoderOptions options,
Configuration configuration,
MemoryAllocator memoryAllocator,
TiffColorType colorType,
@@ -396,13 +399,20 @@ internal static class TiffColorDecoderFactory
return new YCbCrTiffColor(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
case TiffColorType.CieLab:
- DebugGuard.IsTrue(
- bitsPerSample.Channels == 3
- && bitsPerSample.Channel2 == 8
- && bitsPerSample.Channel1 == 8
- && bitsPerSample.Channel0 == 8,
- "bitsPerSample");
- return new CieLabTiffColor();
+
+ DebugGuard.IsTrue(bitsPerSample.Channels == 3, "bitsPerSample");
+
+ if (bitsPerSample.Channel0 == 8)
+ {
+ return new CieLab8TiffColor();
+ }
+
+ return new CieLab16TiffColor(
+ configuration,
+ options,
+ metadata,
+ memoryAllocator,
+ byteOrder == ByteOrder.BigEndian);
case TiffColorType.Cmyk:
DebugGuard.IsTrue(
@@ -412,7 +422,7 @@ internal static class TiffColorDecoderFactory
&& bitsPerSample.Channel1 == 8
&& bitsPerSample.Channel0 == 8,
"bitsPerSample");
- return new CmykTiffColor(compression);
+ return new CmykTiffColor(compression, configuration, options, metadata, memoryAllocator);
default:
throw TiffThrowHelper.InvalidColorType(colorType.ToString());
@@ -420,6 +430,10 @@ internal static class TiffColorDecoderFactory
}
public static TiffBasePlanarColorDecoder CreatePlanar(
+ ImageFrameMetadata metadata,
+ DecoderOptions options,
+ Configuration configuration,
+ MemoryAllocator allocator,
TiffColorType colorType,
TiffBitsPerSample bitsPerSample,
TiffExtraSampleType? extraSampleType,
@@ -443,7 +457,14 @@ internal static class TiffColorDecoderFactory
return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
case TiffColorType.CieLabPlanar:
- return new CieLabPlanarTiffColor();
+ return bitsPerSample.Channel0 == 8
+ ? new CieLab8PlanarTiffColor()
+ : new CieLab16PlanarTiffColor(
+ configuration,
+ options,
+ metadata,
+ allocator,
+ byteOrder == ByteOrder.BigEndian);
case TiffColorType.Rgb161616Planar:
DebugGuard.IsTrue(colorMap == null, "colorMap");
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs
index 744cba35fd..9e5e3dba94 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs
@@ -107,7 +107,7 @@ internal class YCbCrConverter
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Rgba32 Convert(float y, float cb, float cr)
{
- Rgba32 pixel = default(Rgba32);
+ Rgba32 pixel = default;
pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y);
pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb));
pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y);
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 8a4a27946f..e3a51aa8d3 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -52,11 +52,6 @@ internal class TiffDecoderCore : ImageDecoderCore
///
private ByteOrder byteOrder;
- ///
- /// Indicating whether is BigTiff format.
- ///
- private bool isBigTiff;
-
///
/// Initializes a new instance of the class.
///
@@ -167,7 +162,6 @@ internal class TiffDecoderCore : ImageDecoderCore
IList directories = reader.Read();
this.byteOrder = reader.ByteOrder;
- this.isBigTiff = reader.IsBigTiff;
Size? size = null;
uint frameCount = 0;
@@ -273,6 +267,15 @@ internal class TiffDecoderCore : ImageDecoderCore
this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken);
}
+ // Only RGB-compatible color types can be converted here because the TPixel-based ICC profile conversion
+ // expects RGB-like pixel data; other photometric interpretations (YCbCr, CMYK, Lab, etc.) would require
+ // dedicated transforms. We do this once at the frame level to avoid duplicating conversion logic
+ // across all color decoders and to keep their decode paths focused on raw pixel unpacking.
+ if (this.ColorType is >= TiffColorType.PaletteColor and <= TiffColorType.Rgba32323232Planar)
+ {
+ _ = this.TryConvertIccProfile(frame);
+ }
+
return frame;
}
@@ -453,7 +456,7 @@ internal class TiffDecoderCore : ImageDecoderCore
}
using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata);
- TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder();
+ TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(frame.Metadata);
for (int i = 0; i < stripsPerPlane; i++)
{
@@ -518,7 +521,7 @@ internal class TiffDecoderCore : ImageDecoderCore
int bitsPerPixel = this.BitsPerPixel;
using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata);
- TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder();
+ TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(frame.Metadata);
Buffer2D pixels = frame.PixelBuffer;
// There exists in this world TIFF files with uncompressed strips larger than Int32.MaxValue.
@@ -661,7 +664,7 @@ internal class TiffDecoderCore : ImageDecoderCore
}
using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel, frame.Metadata);
- TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder();
+ TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(frame.Metadata);
int tileIndex = 0;
int remainingPixelsInColumn = height;
@@ -762,7 +765,7 @@ internal class TiffDecoderCore : ImageDecoderCore
Span tileBufferSpan = tileBuffer.GetSpan();
using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel, frame.Metadata, true, tileWidth, tileLength);
- TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder();
+ TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(frame.Metadata);
int tileIndex = 0;
for (int tileY = 0; tileY < tilesDown; tileY++)
@@ -803,9 +806,11 @@ internal class TiffDecoderCore : ImageDecoderCore
}
}
- private TiffBaseColorDecoder CreateChunkyColorDecoder()
+ private TiffBaseColorDecoder CreateChunkyColorDecoder(ImageFrameMetadata metadata)
where TPixel : unmanaged, IPixel =>
TiffColorDecoderFactory.Create(
+ metadata,
+ this.Options,
this.configuration,
this.memoryAllocator,
this.ColorType,
@@ -818,9 +823,13 @@ internal class TiffDecoderCore : ImageDecoderCore
this.CompressionType,
this.byteOrder);
- private TiffBasePlanarColorDecoder CreatePlanarColorDecoder()
+ private TiffBasePlanarColorDecoder CreatePlanarColorDecoder(ImageFrameMetadata metadata)
where TPixel : unmanaged, IPixel =>
TiffColorDecoderFactory.CreatePlanar(
+ metadata,
+ this.Options,
+ this.configuration,
+ this.memoryAllocator,
this.ColorType,
this.BitsPerSample,
this.ExtraSamplesType,
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
index 7519871b74..9445f468cb 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
@@ -452,13 +452,9 @@ internal static class TiffDecoderOptionsParser
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for CieLab images.");
}
- ushort bitsPerChannel = options.BitsPerSample.Channel0;
- if (bitsPerChannel != 8)
- {
- TiffThrowHelper.ThrowNotSupported("Only 8 bits per channel is supported for CieLab images.");
- }
-
- options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.CieLab : TiffColorType.CieLabPlanar;
+ options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky
+ ? TiffColorType.CieLab
+ : TiffColorType.CieLabPlanar;
break;
}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index 5096d93bd7..e432a7251b 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -352,6 +352,21 @@ public class TiffDecoderTests : TiffDecoderBaseTester
image.CompareToReferenceOutput(ImageComparer.Exact, provider);
}
+ [Theory]
+ [WithFile(Icc.PerceptualCmyk, PixelTypes.Rgba32)]
+ [WithFile(Icc.PerceptualCieLab, PixelTypes.Rgba32)]
+ [WithFile(Icc.PerceptualRgb8, PixelTypes.Rgba32)]
+ [WithFile(Icc.PerceptualRgb16, PixelTypes.Rgba32)]
+ public void Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(TiffDecoder.Instance, new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert });
+
+ image.DebugSave(provider);
+ image.CompareToReferenceOutput(provider);
+ Assert.Null(image.Metadata.IccProfile);
+ }
+
[Theory]
[WithFile(Issues2454_A, PixelTypes.Rgba32)]
[WithFile(Issues2454_B, PixelTypes.Rgba32)]
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index dc3275999f..764954cae0 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -1194,6 +1194,14 @@ public static class TestImages
];
public static readonly string[] Metadata = [SampleMetadata];
+
+ public static class Icc
+ {
+ public const string PerceptualCmyk = "Tiff/icc-profiles/Perceptual_CMYK.tiff";
+ public const string PerceptualCieLab = "Tiff/icc-profiles/Perceptual_CIELAB.tiff";
+ public const string PerceptualRgb8 = "Tiff/icc-profiles/Perceptual_RGB8.tiff";
+ public const string PerceptualRgb16 = "Tiff/icc-profiles/Perceptual_RGB16.tiff";
+ }
}
public static class BigTiff
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_CIELAB.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_CIELAB.png
new file mode 100644
index 0000000000..3863e7202b
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_CIELAB.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8dd7d54f362b33c2d2d4b4b3cb3bbece23c14138073403234db7b53012939101
+size 383
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_CMYK.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_CMYK.png
new file mode 100644
index 0000000000..bc97709c19
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_CMYK.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:370af73b800622a671c0718b2c137ead8401adf20c39b45f153d2c9bb09b40ed
+size 385
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB16.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB16.png
new file mode 100644
index 0000000000..3fcea773ac
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB16.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de9cfa53a82b169c533999908c7ace43aa35a8b456d5f0378b539669c7857d1c
+size 386
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB8.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB8.png
new file mode 100644
index 0000000000..3fcea773ac
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_RGB8.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de9cfa53a82b169c533999908c7ace43aa35a8b456d5f0378b539669c7857d1c
+size 386
diff --git a/tests/Images/Input/Tiff/icc-profiles/Perceptual_CIELAB.tiff b/tests/Images/Input/Tiff/icc-profiles/Perceptual_CIELAB.tiff
new file mode 100644
index 0000000000..ab2aff8207
--- /dev/null
+++ b/tests/Images/Input/Tiff/icc-profiles/Perceptual_CIELAB.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9494eea65b2c5b6ef033fb89b4fee2ef97d07773ae4b3988da972e1ba152b890
+size 64418
diff --git a/tests/Images/Input/Tiff/icc-profiles/Perceptual_CMYK.tiff b/tests/Images/Input/Tiff/icc-profiles/Perceptual_CMYK.tiff
new file mode 100644
index 0000000000..7dd685fe37
--- /dev/null
+++ b/tests/Images/Input/Tiff/icc-profiles/Perceptual_CMYK.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cfb7fe9e93362fa121d97fa05f61242733d79fa9fa06bb7153e1773e827567dd
+size 601124
diff --git a/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB16.tiff b/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB16.tiff
new file mode 100644
index 0000000000..3692848191
--- /dev/null
+++ b/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB16.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b0e89ac971c23c12c9edff6f9e9dd066fe99f36e28e89972343d50562dd7dbd
+size 65260
diff --git a/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB8.tiff b/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB8.tiff
new file mode 100644
index 0000000000..e2c1ff54c0
--- /dev/null
+++ b/tests/Images/Input/Tiff/icc-profiles/Perceptual_RGB8.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:18b0e5ee58c9f53bf5414fc8244e98d65aa0bc6c3397e201399baff0cd378b23
+size 35236