diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs index f6ee06db66..ab8f51844c 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -41,6 +41,7 @@ public static void Undo(Span pixelBytes, int width, TiffColorType colorTyp UndoGray32Bit(pixelBytes, width, isBigEndian); break; case TiffColorType.Rgb888: + case TiffColorType.CieLab: UndoRgb24Bit(pixelBytes, width); break; case TiffColorType.Rgba8888: diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000000..3baf60e786 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs @@ -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 +{ + /// + /// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration. + /// + internal class CieLabPlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private static readonly ColorSpaceConverter ColorSpaceConverter = new(); + + private const float Inv255 = 1.0f / 255.0f; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span l = data[0].GetSpan(); + Span a = data[1].GetSpan(); + Span b = data[2].GetSpan(); + + var color = default(TPixel); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span 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++; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs new file mode 100644 index 0000000000..5b272b2be5 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs @@ -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 +{ + /// + /// Implements decoding pixel data with photometric interpretation of type 'CieLab'. + /// + internal class CieLabTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private static readonly ColorSpaceConverter ColorSpaceConverter = new(); + + private const float Inv255 = 1.0f / 255.0f; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D 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 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; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 5c1fba5ef4..2ef46dd84e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -385,8 +385,23 @@ public static TiffBaseColorDecoder Create( return new PaletteTiffColor(bitsPerSample, colorMap); case TiffColorType.YCbCr: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 8 + && bitsPerSample.Channel1 == 8 + && bitsPerSample.Channel0 == 8, + "bitsPerSample"); 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(); + default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); } @@ -415,6 +430,9 @@ public static TiffBasePlanarColorDecoder CreatePlanar( case TiffColorType.YCbCrPlanar: return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); + case TiffColorType.CieLabPlanar: + return new CieLabPlanarTiffColor(); + case TiffColorType.Rgb161616Planar: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 619bfab4d6..cce5c95ad5 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -276,6 +276,16 @@ internal enum TiffColorType /// /// The pixels are stored in YCbCr format as planar. /// - YCbCrPlanar + YCbCrPlanar, + + /// + /// The pixels are stored in CieLab format. + /// + CieLab, + + /// + /// The pixels are stored in CieLab format as planar. + /// + CieLabPlanar, } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs index c53b755439..6abec16fa2 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -9,6 +9,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'YCbCr' with the planar configuration. + /// internal class YCbCrPlanarTiffColor : TiffBasePlanarColorDecoder where TPixel : unmanaged, IPixel { diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index a850c67872..f4ff75c15e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -9,6 +9,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'YCbCr'. + /// internal class YCbCrTiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 8cb327a7b0..865b2f9c21 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -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 diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index a5d58c2dd7..2e53acec8f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -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; @@ -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}"); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 12e0b819d4..96b48ab309 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -318,6 +318,20 @@ public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider image.CompareToReferenceOutput(ImageComparer.Exact, provider); } + [Theory] + [WithFile(CieLab, PixelTypes.Rgba32)] + [WithFile(CieLabPlanar, PixelTypes.Rgba32)] + [WithFile(CieLabLzwPredictor, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_CieLab(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // 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? + using Image image = provider.GetImage(); + image.DebugSave(provider); + image.CompareToReferenceOutput(ImageComparer.Exact, provider); + } + [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 616eb3ecdf..b364b1d2c0 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -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"; diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab.png new file mode 100644 index 0000000000..bdd240663b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13bc9da102f85124855217fad757ca907f5d68442e54e3b7039ac048d7b2ad3f +size 25791 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLabPlanar.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLabPlanar.png new file mode 100644 index 0000000000..bdd240663b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLabPlanar.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13bc9da102f85124855217fad757ca907f5d68442e54e3b7039ac048d7b2ad3f +size 25791 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab_lzwcompressed_predictor.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab_lzwcompressed_predictor.png new file mode 100644 index 0000000000..1938ebe110 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab_lzwcompressed_predictor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f9481c91c58ca7bbab9de4b9ae95fe4a2197ae4b6ef6b15b72d4858aba3a1a4 +size 25782 diff --git a/tests/Images/Input/Tiff/CieLab.tiff b/tests/Images/Input/Tiff/CieLab.tiff new file mode 100644 index 0000000000..59a667dd5e --- /dev/null +++ b/tests/Images/Input/Tiff/CieLab.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7542b5b3abe049614f2ddaf78ffe995edac13e768f0b2fc9f324c6ef43b379eb +size 1312046 diff --git a/tests/Images/Input/Tiff/CieLabPlanar.tiff b/tests/Images/Input/Tiff/CieLabPlanar.tiff new file mode 100644 index 0000000000..d964a96947 --- /dev/null +++ b/tests/Images/Input/Tiff/CieLabPlanar.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28592d9da8d51f60700b7136369d2d6bd40550d5f8c7758e570b5e624c71a3e4 +size 1307488 diff --git a/tests/Images/Input/Tiff/CieLab_lzwcompressed_predictor.tiff b/tests/Images/Input/Tiff/CieLab_lzwcompressed_predictor.tiff new file mode 100644 index 0000000000..2284e1e17f --- /dev/null +++ b/tests/Images/Input/Tiff/CieLab_lzwcompressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6affced5550e51441c4cde7f1770d4e57cfa594bd271a12f9571359733c2185d +size 55346