Browse Source

Allow conversion for CIE Lab and CMYK

pull/3054/head
James Jackson-South 1 week ago
parent
commit
7f402b8bb8
  1. 144
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16PlanarTiffColor{TPixel}.cs
  2. 140
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab16TiffColor{TPixel}.cs
  3. 2
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8PlanarTiffColor{TPixel}.cs
  4. 10
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs
  5. 69
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs
  6. 40
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
  7. 2
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs
  8. 18
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  9. 10
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  10. 15
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  11. 6
      tests/ImageSharp.Tests/TestImages.cs
  12. 3
      tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_CIELAB.png
  13. 3
      tests/Images/External/ReferenceOutput/TiffDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual_CMYK.png
  14. 3
      tests/Images/Input/Tiff/icc-profiles/Perceptual_CIELAB.tiff
  15. 3
      tests/Images/Input/Tiff/icc-profiles/Perceptual_CMYK.tiff

144
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;
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration.
/// Each channel is represented with 16 bits.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
internal class CieLab16PlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
}
/// <inheritdoc/>
public override void Decode(IMemoryOwner<byte>[] data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
Span<byte> lPlane = data[0].GetSpan();
Span<byte> aPlane = data[1].GetSpan();
Span<byte> bPlane = data[2].GetSpan();
// Allocate temporary buffers to hold the LAB -> RGB conversion.
// This should be the maximum width of a row.
using IMemoryOwner<Rgb> rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Rgb>(width);
using IMemoryOwner<Vector4> vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Vector4>(width);
Span<Rgb> rgbRow = rgbBuffer.Memory.Span;
Span<Vector4> vectorRow = vectorBuffer.Memory.Span;
// Reuse the rgbRow span for lab data since both are 3-float structs, avoiding an extra allocation.
Span<CieLab> cieLabRow = MemoryMarshal.Cast<Rgb, CieLab>(rgbRow);
int stride = width * 2;
if (this.isBigEndian)
{
for (int y = 0; y < height; y++)
{
int rowBase = y * stride;
Span<TPixel> 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<CieLab, Rgb>(cieLabRow, rgbRow);
Rgb.ToScaledVector4(rgbRow, vectorRow);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
}
return;
}
for (int y = 0; y < height; y++)
{
int rowBase = y * stride;
Span<TPixel> 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<CieLab, Rgb>(cieLabRow, rgbRow);
Rgb.ToScaledVector4(rgbRow, vectorRow);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
}
}
}

140
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;
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'CieLab'.
/// Each channel is represented with 16 bits.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
internal class CieLab16TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
}
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> 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<Rgb> rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Rgb>(width);
using IMemoryOwner<Vector4> vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Vector4>(width);
Span<Rgb> rgbRow = rgbBuffer.Memory.Span;
Span<Vector4> vectorRow = vectorBuffer.Memory.Span;
// Reuse the rgbRow span for lab data since both are 3-float structs, avoiding an extra allocation.
Span<CieLab> cieLabRow = MemoryMarshal.Cast<Rgb, CieLab>(rgbRow);
if (this.isBigEndian)
{
for (int y = top; y < top + height; y++)
{
Span<TPixel> 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<CieLab, Rgb>(cieLabRow, rgbRow);
Rgb.ToScaledVector4(rgbRow, vectorRow);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
}
return;
}
for (int y = top; y < top + height; y++)
{
Span<TPixel> 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<CieLab, Rgb>(cieLabRow, rgbRow);
Rgb.ToScaledVector4(rgbRow, vectorRow);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
}
}
}

2
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs → 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.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
internal class CieLabPlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
internal class CieLab8PlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private static readonly ColorProfileConverter ColorProfileConverter = new();

10
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs → src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLab8TiffColor{TPixel}.cs

@ -10,14 +10,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'CieLab'.
/// Each channel is represented with 8 bits.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
internal class CieLabTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
internal class CieLab8TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private static readonly ColorProfileConverter ColorProfileConverter = new();
private const float Inv255 = 1f / 255f;
/// <summary>
/// Initializes a new instance of the <see cref="CieLab8TiffColor{TPixel}" /> class.
/// </summary>
public CieLab8TiffColor()
{
}
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{

69
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<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
}
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> 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<TPixel> : TiffBaseColorDecoder<TPixel>
return;
}
// Allocate temporary buffers to hold the CMYK -> RGB conversion.
// This should be the maximum width of a row.
using IMemoryOwner<Rgb> rgbBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Rgb>(width);
using IMemoryOwner<Vector4> vectorBuffer = this.colorProfileConverter.Options.MemoryAllocator.Allocate<Vector4>(width);
Span<Rgb> rgbRow = rgbBuffer.Memory.Span;
Span<Vector4> vectorRow = vectorBuffer.Memory.Span;
// Reuse the Vector4 buffer as CMYK storage since both are 4-float structs, avoiding an extra allocation.
Span<Cmyk> cmykRow = MemoryMarshal.Cast<Vector4, Cmyk>(vectorRow);
for (int y = top; y < top + height; y++)
{
Span<TPixel> 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<Cmyk, Rgb>(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<Cmyk, float>(cmykRow));
offset += width * 4;
// Convert CMYK -> RGB -> Vector4 -> TPixel
this.colorProfileConverter.Convert<Cmyk, Rgb>(cmykRow, rgbRow);
Rgb.ToScaledVector4(rgbRow, vectorRow);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorRow, pixelRow, PixelConversionModifiers.Scale);
}
}
}

40
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<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
public static TiffBaseColorDecoder<TPixel> Create(
ImageFrameMetadata metadata,
DecoderOptions options,
Configuration configuration,
MemoryAllocator memoryAllocator,
TiffColorType colorType,
@ -396,13 +399,20 @@ internal static class TiffColorDecoderFactory<TPixel>
return new YCbCrTiffColor<TPixel>(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<TPixel>();
DebugGuard.IsTrue(bitsPerSample.Channels == 3, "bitsPerSample");
if (bitsPerSample.Channel0 == 8)
{
return new CieLab8TiffColor<TPixel>();
}
return new CieLab16TiffColor<TPixel>(
configuration,
options,
metadata,
memoryAllocator,
byteOrder == ByteOrder.BigEndian);
case TiffColorType.Cmyk:
DebugGuard.IsTrue(
@ -412,14 +422,19 @@ internal static class TiffColorDecoderFactory<TPixel>
&& bitsPerSample.Channel1 == 8
&& bitsPerSample.Channel0 == 8,
"bitsPerSample");
return new CmykTiffColor<TPixel>(compression);
return new CmykTiffColor<TPixel>(compression, configuration, options, metadata, memoryAllocator);
default:
throw TiffThrowHelper.InvalidColorType(colorType.ToString());
}
}
#pragma warning disable IDE0060 // Remove unused parameter
public static TiffBasePlanarColorDecoder<TPixel> CreatePlanar(
ImageFrameMetadata metadata,
DecoderOptions options,
Configuration configuration,
MemoryAllocator allocator,
TiffColorType colorType,
TiffBitsPerSample bitsPerSample,
TiffExtraSampleType? extraSampleType,
@ -443,7 +458,14 @@ internal static class TiffColorDecoderFactory<TPixel>
return new YCbCrPlanarTiffColor<TPixel>(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
case TiffColorType.CieLabPlanar:
return new CieLabPlanarTiffColor<TPixel>();
return bitsPerSample.Channel0 == 8
? new CieLab8PlanarTiffColor<TPixel>()
: new CieLab16PlanarTiffColor<TPixel>(
configuration,
options,
metadata,
allocator,
byteOrder == ByteOrder.BigEndian);
case TiffColorType.Rgb161616Planar:
DebugGuard.IsTrue(colorMap == null, "colorMap");

2
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);

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

@ -453,7 +453,7 @@ internal class TiffDecoderCore : ImageDecoderCore
}
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(width, bitsPerPixel, frame.Metadata);
TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>();
TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>(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<TPixel>(width, bitsPerPixel, frame.Metadata);
TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>();
TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>(frame.Metadata);
Buffer2D<TPixel> 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<TPixel>(frame.Width, bitsPerPixel, frame.Metadata);
TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>();
TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>(frame.Metadata);
int tileIndex = 0;
int remainingPixelsInColumn = height;
@ -762,7 +762,7 @@ internal class TiffDecoderCore : ImageDecoderCore
Span<byte> tileBufferSpan = tileBuffer.GetSpan();
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel, frame.Metadata, true, tileWidth, tileLength);
TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>();
TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>(frame.Metadata);
int tileIndex = 0;
for (int tileY = 0; tileY < tilesDown; tileY++)
@ -803,9 +803,11 @@ internal class TiffDecoderCore : ImageDecoderCore
}
}
private TiffBaseColorDecoder<TPixel> CreateChunkyColorDecoder<TPixel>()
private TiffBaseColorDecoder<TPixel> CreateChunkyColorDecoder<TPixel>(ImageFrameMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel> =>
TiffColorDecoderFactory<TPixel>.Create(
metadata,
this.Options,
this.configuration,
this.memoryAllocator,
this.ColorType,
@ -818,9 +820,13 @@ internal class TiffDecoderCore : ImageDecoderCore
this.CompressionType,
this.byteOrder);
private TiffBasePlanarColorDecoder<TPixel> CreatePlanarColorDecoder<TPixel>()
private TiffBasePlanarColorDecoder<TPixel> CreatePlanarColorDecoder<TPixel>(ImageFrameMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel> =>
TiffColorDecoderFactory<TPixel>.CreatePlanar(
metadata,
this.Options,
this.configuration,
this.memoryAllocator,
this.ColorType,
this.BitsPerSample,
this.ExtraSamplesType,

10
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;
}

15
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => Assert.Throws<NotSupportedException>(() => 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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)]

6
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

3
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

3
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

3
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

3
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
Loading…
Cancel
Save