Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static void Undo(Span<byte> pixelBytes, int width, TiffColorType colorTyp
UndoGray32Bit(pixelBytes, width, isBigEndian);
break;
case TiffColorType.Rgb888:
case TiffColorType.CieLab:
UndoRgb24Bit(pixelBytes, width);
break;
case TiffColorType.Rgba8888:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System;
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
using SixLabors.ImageSharp.Memory;
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.
/// </summary>
internal class CieLabPlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private static readonly ColorSpaceConverter ColorSpaceConverter = new();

private const float Inv255 = 1.0f / 255.0f;

/// <inheritdoc/>
public override void Decode(IMemoryOwner<byte>[] data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
Span<byte> l = data[0].GetSpan();
Span<byte> a = data[1].GetSpan();
Span<byte> b = data[2].GetSpan();

var color = default(TPixel);
int offset = 0;
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++)
{
var lab = new CieLab((l[offset] & 0xFF) * 100f * Inv255, (sbyte)a[offset], (sbyte)b[offset]);
var rgb = ColorSpaceConverter.ToRgb(lab);

color.FromVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f));
pixelRow[x] = color;

offset++;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System;
using System.Numerics;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;

namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'CieLab'.
/// </summary>
internal class CieLabTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private static readonly ColorSpaceConverter ColorSpaceConverter = new();

private const float Inv255 = 1.0f / 255.0f;

/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
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++)
{
float l = (data[offset] & 0xFF) * 100f * Inv255;
var lab = new CieLab(l, (sbyte)data[offset + 1], (sbyte)data[offset + 2]);
var rgb = ColorSpaceConverter.ToRgb(lab);

color.FromVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f));
pixelRow[x] = color;

offset += 3;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -385,8 +385,23 @@ public static TiffBaseColorDecoder<TPixel> Create(
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap);

case TiffColorType.YCbCr:
DebugGuard.IsTrue(
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 8
&& bitsPerSample.Channel1 == 8
&& bitsPerSample.Channel0 == 8,
"bitsPerSample");
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>();

default:
throw TiffThrowHelper.InvalidColorType(colorType.ToString());
}
Expand Down Expand Up @@ -415,6 +430,9 @@ public static TiffBasePlanarColorDecoder<TPixel> CreatePlanar(
case TiffColorType.YCbCrPlanar:
return new YCbCrPlanarTiffColor<TPixel>(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);

case TiffColorType.CieLabPlanar:
return new CieLabPlanarTiffColor<TPixel>();

case TiffColorType.Rgb161616Planar:
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new Rgb16PlanarTiffColor<TPixel>(byteOrder == ByteOrder.BigEndian);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,16 @@ internal enum TiffColorType
/// <summary>
/// The pixels are stored in YCbCr format as planar.
/// </summary>
YCbCrPlanar
YCbCrPlanar,

/// <summary>
/// The pixels are stored in CieLab format.
/// </summary>
CieLab,

/// <summary>
/// The pixels are stored in CieLab format as planar.
/// </summary>
CieLabPlanar,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'YCbCr' with the planar configuration.
/// </summary>
internal class YCbCrPlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements decoding pixel data with photometric interpretation of type 'YCbCr'.
/// </summary>
internal class YCbCrTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Tiff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
|TransparencyMask | | | |
|Separated (TIFF Extension) | | | |
|YCbCr (TIFF Extension) | | Y | |
|CieLab (TIFF Extension) | | | |
|CieLab (TIFF Extension) | | Y | |
|IccLab (TechNote 1) | | | |

### Baseline TIFF Tags
Expand Down
20 changes: 19 additions & 1 deletion src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ private static void ParseColorType(this TiffDecoderCore options, ExifProfile exi
options.ColorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value;
if (options.BitsPerSample.Channels != 3)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for YCbCr images.");
}

ushort bitsPerChannel = options.BitsPerSample.Channel0;
Expand All @@ -395,6 +395,24 @@ private static void ParseColorType(this TiffDecoderCore options, ExifProfile exi
break;
}

case TiffPhotometricInterpretation.CieLab:
{
if (options.BitsPerSample.Channels != 3)
{
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;

break;
}

default:
{
TiffThrowHelper.ThrowNotSupported($"The specified TIFF photometric interpretation is not supported: {options.PhotometricInterpretation}");
Expand Down
14 changes: 14 additions & 0 deletions tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,20 @@ public void TiffDecoder_CanDecode_YCbCr_24Bit<TPixel>(TestImageProvider<TPixel>
image.CompareToReferenceOutput(ImageComparer.Exact, provider);
}

[Theory]
[WithFile(CieLab, PixelTypes.Rgba32)]
[WithFile(CieLabPlanar, PixelTypes.Rgba32)]
[WithFile(CieLabLzwPredictor, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_CieLab<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
// Note: The image from MagickReferenceDecoder does not look right, maybe we are doing something wrong
// converting the pixel data from Magick.NET to our format with CieLab?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's happening here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image Tiff\CieLab.tiff looks wrong when using the MagickReferenceDecoder:
output

It should look like this:
expected

ImageMagick itself can decode this image just fine, that's why I thought we maybe doing something wrong in MagickReferenceDecoder.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right.... Yep,

IMagickImage<T> contains ColorSpace enum which we are blindly ignoring when converting. I always assumed they did the same as us and converted color spaces on decode.

It looks like we should be checking that first and converting on the fly.

@dlemstra Am I correct here?

using Image<TPixel> image = provider.GetImage();
image.DebugSave(provider);
image.CompareToReferenceOutput(ImageComparer.Exact, provider);
}

[Theory]
[WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)]
Expand Down
5 changes: 5 additions & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,11 @@ public static class Tiff
public const string Rgba32BitPlanarUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlphaPlanar32bit_msb.tiff";
public const string Rgba32BitPlanarUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlphaPlanar32bit_lsb.tiff";

// Cie Lab color space.
public const string CieLab = "Tiff/CieLab.tiff";
public const string CieLabPlanar = "Tiff/CieLabPlanar.tiff";
public const string CieLabLzwPredictor = "Tiff/CieLab_lzwcompressed_predictor.tiff";

public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff";
public const string Issues1891 = "Tiff/Issues/Issue1891.tiff";
public const string Issues2123 = "Tiff/Issues/Issue2123.tiff";
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/Images/Input/Tiff/CieLab.tiff
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Tiff/CieLabPlanar.tiff
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Tiff/CieLab_lzwcompressed_predictor.tiff
Git LFS file not shown