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 000000000..b422e65ad --- /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 000000000..4455753bf --- /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 d23d1e290..1252f1d3d 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 b10d27ccd..ce5bed53c 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 2e22fcde0..509056267 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 e2eb82e3b..363c16445 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,14 +422,19 @@ 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()); } } +#pragma warning disable IDE0060 // Remove unused parameter public static TiffBasePlanarColorDecoder CreatePlanar( + ImageFrameMetadata metadata, + DecoderOptions options, + Configuration configuration, + MemoryAllocator allocator, TiffColorType colorType, TiffBitsPerSample bitsPerSample, TiffExtraSampleType? extraSampleType, @@ -443,7 +458,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 744cba35f..9e5e3dba9 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 8a4a27946..30fa277fc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -453,7 +453,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 +518,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 +661,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 +762,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 +803,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 +820,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 7519871b7..9445f468c 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 5096d93bd..c186d4f01 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -23,7 +23,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester public static readonly string[] MultiframeTestImages = Multiframes; [Theory] - [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] + // [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] public void ThrowsNotSupported(TestImageProvider provider) where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder.Instance)); @@ -352,6 +352,19 @@ public class TiffDecoderTests : TiffDecoderBaseTester image.CompareToReferenceOutput(ImageComparer.Exact, provider); } + [Theory] + [WithFile(Icc.PerceptualCmyk, PixelTypes.Rgba32)] + [WithFile(Icc.PerceptualCieLab, 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 dc3275999..ee0766107 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1194,6 +1194,12 @@ 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 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 000000000..3863e7202 --- /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 000000000..bc97709c1 --- /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/Input/Tiff/icc-profiles/Perceptual_CIELAB.tiff b/tests/Images/Input/Tiff/icc-profiles/Perceptual_CIELAB.tiff new file mode 100644 index 000000000..ab2aff820 --- /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 000000000..7dd685fe3 --- /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