Browse Source

Allow conversion for CIE Lab and CMYK

pull/3054/head
James Jackson-South 5 months 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. /// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
private static readonly ColorProfileConverter ColorProfileConverter = new(); 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> /// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'CieLab'. /// Implements decoding pixel data with photometric interpretation of type 'CieLab'.
/// Each channel is represented with 8 bits.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
private static readonly ColorProfileConverter ColorProfileConverter = new(); private static readonly ColorProfileConverter ColorProfileConverter = new();
private const float Inv255 = 1f / 255f; private const float Inv255 = 1f / 255f;
/// <summary>
/// Initializes a new instance of the <see cref="CieLab8TiffColor{TPixel}" /> class.
/// </summary>
public CieLab8TiffColor()
{
}
/// <inheritdoc/> /// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height) 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. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.ColorProfiles; using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Formats.Tiff.Compression; using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
@ -12,18 +17,48 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
internal class CmykTiffColor<TPixel> : TiffBaseColorDecoder<TPixel> internal class CmykTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<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 const float Inv255 = 1f / 255f;
private readonly TiffDecoderCompressionType compression; 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/> /// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height) public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{ {
int offset = 0; int offset = 0;
if (this.compression == TiffDecoderCompressionType.Jpeg) if (this.compression == TiffDecoderCompressionType.Jpeg)
{ {
for (int y = top; y < top + height; y++) for (int y = top; y < top + height; y++)
@ -40,17 +75,31 @@ internal class CmykTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
return; 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++) for (int y = top; y < top + height; y++)
{ {
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); 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.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
@ -11,6 +12,8 @@ internal static class TiffColorDecoderFactory<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
public static TiffBaseColorDecoder<TPixel> Create( public static TiffBaseColorDecoder<TPixel> Create(
ImageFrameMetadata metadata,
DecoderOptions options,
Configuration configuration, Configuration configuration,
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
TiffColorType colorType, TiffColorType colorType,
@ -396,13 +399,20 @@ internal static class TiffColorDecoderFactory<TPixel>
return new YCbCrTiffColor<TPixel>(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); return new YCbCrTiffColor<TPixel>(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
case TiffColorType.CieLab: case TiffColorType.CieLab:
DebugGuard.IsTrue(
bitsPerSample.Channels == 3 DebugGuard.IsTrue(bitsPerSample.Channels == 3, "bitsPerSample");
&& bitsPerSample.Channel2 == 8
&& bitsPerSample.Channel1 == 8 if (bitsPerSample.Channel0 == 8)
&& bitsPerSample.Channel0 == 8, {
"bitsPerSample"); return new CieLab8TiffColor<TPixel>();
return new CieLabTiffColor<TPixel>(); }
return new CieLab16TiffColor<TPixel>(
configuration,
options,
metadata,
memoryAllocator,
byteOrder == ByteOrder.BigEndian);
case TiffColorType.Cmyk: case TiffColorType.Cmyk:
DebugGuard.IsTrue( DebugGuard.IsTrue(
@ -412,14 +422,19 @@ internal static class TiffColorDecoderFactory<TPixel>
&& bitsPerSample.Channel1 == 8 && bitsPerSample.Channel1 == 8
&& bitsPerSample.Channel0 == 8, && bitsPerSample.Channel0 == 8,
"bitsPerSample"); "bitsPerSample");
return new CmykTiffColor<TPixel>(compression); return new CmykTiffColor<TPixel>(compression, configuration, options, metadata, memoryAllocator);
default: default:
throw TiffThrowHelper.InvalidColorType(colorType.ToString()); throw TiffThrowHelper.InvalidColorType(colorType.ToString());
} }
} }
#pragma warning disable IDE0060 // Remove unused parameter
public static TiffBasePlanarColorDecoder<TPixel> CreatePlanar( public static TiffBasePlanarColorDecoder<TPixel> CreatePlanar(
ImageFrameMetadata metadata,
DecoderOptions options,
Configuration configuration,
MemoryAllocator allocator,
TiffColorType colorType, TiffColorType colorType,
TiffBitsPerSample bitsPerSample, TiffBitsPerSample bitsPerSample,
TiffExtraSampleType? extraSampleType, TiffExtraSampleType? extraSampleType,
@ -443,7 +458,14 @@ internal static class TiffColorDecoderFactory<TPixel>
return new YCbCrPlanarTiffColor<TPixel>(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); return new YCbCrPlanarTiffColor<TPixel>(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
case TiffColorType.CieLabPlanar: 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: case TiffColorType.Rgb161616Planar:
DebugGuard.IsTrue(colorMap == null, "colorMap"); DebugGuard.IsTrue(colorMap == null, "colorMap");

2
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs

@ -107,7 +107,7 @@ internal class YCbCrConverter
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Rgba32 Convert(float y, float cb, float cr) public Rgba32 Convert(float y, float cb, float cr)
{ {
Rgba32 pixel = default(Rgba32); Rgba32 pixel = default;
pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y);
pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb));
pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); 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); 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++) for (int i = 0; i < stripsPerPlane; i++)
{ {
@ -518,7 +518,7 @@ internal class TiffDecoderCore : ImageDecoderCore
int bitsPerPixel = this.BitsPerPixel; int bitsPerPixel = this.BitsPerPixel;
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(width, bitsPerPixel, frame.Metadata); 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; Buffer2D<TPixel> pixels = frame.PixelBuffer;
// There exists in this world TIFF files with uncompressed strips larger than Int32.MaxValue. // 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); 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 tileIndex = 0;
int remainingPixelsInColumn = height; int remainingPixelsInColumn = height;
@ -762,7 +762,7 @@ internal class TiffDecoderCore : ImageDecoderCore
Span<byte> tileBufferSpan = tileBuffer.GetSpan(); Span<byte> tileBufferSpan = tileBuffer.GetSpan();
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel, frame.Metadata, true, tileWidth, tileLength); 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; int tileIndex = 0;
for (int tileY = 0; tileY < tilesDown; tileY++) 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> => where TPixel : unmanaged, IPixel<TPixel> =>
TiffColorDecoderFactory<TPixel>.Create( TiffColorDecoderFactory<TPixel>.Create(
metadata,
this.Options,
this.configuration, this.configuration,
this.memoryAllocator, this.memoryAllocator,
this.ColorType, this.ColorType,
@ -818,9 +820,13 @@ internal class TiffDecoderCore : ImageDecoderCore
this.CompressionType, this.CompressionType,
this.byteOrder); this.byteOrder);
private TiffBasePlanarColorDecoder<TPixel> CreatePlanarColorDecoder<TPixel>() private TiffBasePlanarColorDecoder<TPixel> CreatePlanarColorDecoder<TPixel>(ImageFrameMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel> => where TPixel : unmanaged, IPixel<TPixel> =>
TiffColorDecoderFactory<TPixel>.CreatePlanar( TiffColorDecoderFactory<TPixel>.CreatePlanar(
metadata,
this.Options,
this.configuration,
this.memoryAllocator,
this.ColorType, this.ColorType,
this.BitsPerSample, this.BitsPerSample,
this.ExtraSamplesType, 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."); TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for CieLab images.");
} }
ushort bitsPerChannel = options.BitsPerSample.Channel0; options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky
if (bitsPerChannel != 8) ? TiffColorType.CieLab
{ : TiffColorType.CieLabPlanar;
TiffThrowHelper.ThrowNotSupported("Only 8 bits per channel is supported for CieLab images.");
}
options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.CieLab : TiffColorType.CieLabPlanar;
break; break;
} }

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

@ -23,7 +23,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
public static readonly string[] MultiframeTestImages = Multiframes; public static readonly string[] MultiframeTestImages = Multiframes;
[Theory] [Theory]
[WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] // [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)]
[WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)]
public void ThrowsNotSupported<TPixel>(TestImageProvider<TPixel> provider) public void ThrowsNotSupported<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => Assert.Throws<NotSupportedException>(() => provider.GetImage(TiffDecoder.Instance)); 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); 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] [Theory]
[WithFile(Issues2454_A, PixelTypes.Rgba32)] [WithFile(Issues2454_A, PixelTypes.Rgba32)]
[WithFile(Issues2454_B, 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 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 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