diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs
new file mode 100644
index 0000000000..7173ea1c41
--- /dev/null
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.ColorSpaces;
+using SixLabors.ImageSharp.ColorSpaces.Conversion;
+
+namespace SixLabors.ImageSharp.Formats.Jpeg.Components;
+
+internal abstract partial class JpegColorConverterBase
+{
+ ///
+ /// Color converter for tiff images, which use the jpeg compression and CMYK colorspace.
+ ///
+ internal sealed class TiffCmykScalar : JpegColorConverterScalar
+ {
+ public TiffCmykScalar(int precision)
+ : base(JpegColorSpace.TiffCmyk, precision)
+ {
+ }
+
+ ///
+ public override void ConvertToRgbInplace(in ComponentValues values)
+ => ConvertToRgbInplace(values, this.MaximumValue);
+
+ public override void ConvertFromRgb(in ComponentValues values, Span rLane, Span gLane, Span bLane) => throw new NotImplementedException();
+
+ internal static void ConvertToRgbInplace(ComponentValues values, float maxValue)
+ {
+ float invMax = 1 / maxValue;
+ for (int i = 0; i < values.Component0.Length; i++)
+ {
+ Cmyk cmyk = new(values.Component0[i] * invMax, values.Component1[i] * invMax, values.Component2[i] * invMax, values.Component3[i] * invMax);
+ Rgb rgb = ColorSpaceConverter.ToRgb(in cmyk);
+ values.Component0[i] = rgb.R;
+ values.Component1[i] = rgb.G;
+ values.Component2[i] = rgb.B;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
index 041f6b0578..88505554dc 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
@@ -105,8 +105,8 @@ internal abstract partial class JpegColorConverterBase
///
private static JpegColorConverterBase[] CreateConverters()
{
- // 5 color types with 2 supported precisions: 8 bit & 12 bit
- const int colorConvertersCount = 5 * 2;
+ // 6 color types with 2 supported precisions: 8 bit & 12 bit
+ const int colorConvertersCount = 6 * 2;
JpegColorConverterBase[] converters = new JpegColorConverterBase[colorConvertersCount];
@@ -116,13 +116,15 @@ internal abstract partial class JpegColorConverterBase
converters[2] = GetCmykConverter(8);
converters[3] = GetGrayScaleConverter(8);
converters[4] = GetRgbConverter(8);
+ converters[5] = GetTiffCmykConverter(8);
// 12-bit converters
- converters[5] = GetYCbCrConverter(12);
- converters[6] = GetYccKConverter(12);
- converters[7] = GetCmykConverter(12);
- converters[8] = GetGrayScaleConverter(12);
- converters[9] = GetRgbConverter(12);
+ converters[6] = GetYCbCrConverter(12);
+ converters[7] = GetYccKConverter(12);
+ converters[8] = GetCmykConverter(12);
+ converters[9] = GetGrayScaleConverter(12);
+ converters[10] = GetRgbConverter(12);
+ converters[11] = GetTiffCmykConverter(12);
return converters;
}
@@ -247,6 +249,8 @@ internal abstract partial class JpegColorConverterBase
return new RgbScalar(precision);
}
+ private static JpegColorConverterBase GetTiffCmykConverter(int precision) => new TiffCmykScalar(precision);
+
///
/// A stack-only struct to reference the input buffers using -s.
///
diff --git a/src/ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs b/src/ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs
index a2ec0666b0..d0e4079946 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs
@@ -23,6 +23,11 @@ internal enum JpegColorSpace
///
Cmyk,
+ ///
+ /// Cmyk color space with 4 components, used with tiff images, which use jpeg compression.
+ ///
+ TiffCmyk,
+
///
/// Color space with 3 components.
///
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
index 82b26232af..bd113941dd 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
@@ -80,6 +80,7 @@ internal sealed class JpegTiffCompression : TiffBaseDecompressor
case TiffPhotometricInterpretation.YCbCr:
case TiffPhotometricInterpretation.Rgb:
+ case TiffPhotometricInterpretation.Separated:
{
using SpectralConverter spectralConverter = new TiffJpegSpectralConverter(configuration, this.photometricInterpretation);
HuffmanScanDecoder scanDecoder = new(stream, spectralConverter, cancellationToken);
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs
index f051aaea1d..8217e663b0 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs
@@ -36,7 +36,9 @@ internal sealed class TiffJpegSpectralConverter : SpectralConverter
- /// This converter must be used only for RGB and YCbCr color spaces for performance reasons.
+ /// Photometric interpretation Rgb and YCbCr will be mapped to RGB colorspace, which means the jpeg decompression will leave the data as is (no color conversion).
+ /// The color conversion will be done after the decompression. For Separated/CMYK, the jpeg color converter will handle the color conversion,
+ /// since the jpeg color converter needs to return RGB data and cannot return 4 component data.
/// For grayscale images must be used.
///
private static JpegColorSpace GetJpegColorSpaceFromPhotometricInterpretation(TiffPhotometricInterpretation interpretation)
@@ -44,6 +46,7 @@ internal sealed class TiffJpegSpectralConverter : SpectralConverter JpegColorSpace.RGB,
TiffPhotometricInterpretation.YCbCr => JpegColorSpace.RGB,
+ TiffPhotometricInterpretation.Separated => JpegColorSpace.TiffCmyk,
_ => throw new InvalidImageContentException($"Invalid tiff photometric interpretation for jpeg encoding: {interpretation}"),
};
}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs
index c87051d527..23de699c90 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CmykTiffColor{TPixel}.cs
@@ -4,6 +4,7 @@
using System.Numerics;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
+using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -14,11 +15,33 @@ internal class CmykTiffColor : TiffBaseColorDecoder
{
private const float Inv255 = 1 / 255.0f;
+ private readonly TiffDecoderCompressionType compression;
+
+ public CmykTiffColor(TiffDecoderCompressionType compression) => this.compression = compression;
+
///
public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height)
{
TPixel color = default;
int offset = 0;
+
+ if (this.compression == TiffDecoderCompressionType.Jpeg)
+ {
+ for (int y = top; y < top + height; y++)
+ {
+ Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
+ for (int x = 0; x < pixelRow.Length; x++)
+ {
+ color.FromVector4(new Vector4(data[offset] * Inv255, data[offset + 1] * Inv255, data[offset + 2] * Inv255, 1.0f));
+ pixelRow[x] = color;
+
+ offset += 3;
+ }
+ }
+
+ return;
+ }
+
for (int y = top; y < top + height; y++)
{
Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
index c59b08a55c..e2eb82e3b4 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -19,6 +20,7 @@ internal static class TiffColorDecoderFactory
Rational[] referenceBlackAndWhite,
Rational[] ycbcrCoefficients,
ushort[] ycbcrSubSampling,
+ TiffDecoderCompressionType compression,
ByteOrder byteOrder)
{
switch (colorType)
@@ -410,7 +412,7 @@ internal static class TiffColorDecoderFactory
&& bitsPerSample.Channel1 == 8
&& bitsPerSample.Channel0 == 8,
"bitsPerSample");
- return new CmykTiffColor();
+ return new CmykTiffColor(compression);
default:
throw TiffThrowHelper.InvalidColorType(colorType.ToString());
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index aed6d4ec66..1ce25d1552 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -703,6 +703,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
this.ReferenceBlackAndWhite,
this.YcbcrCoefficients,
this.YcbcrSubSampling,
+ this.CompressionType,
this.byteOrder);
private TiffBasePlanarColorDecoder CreatePlanarColorDecoder()
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index 2c8268d413..7a2538f2a6 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
// ReSharper disable InconsistentNaming
-using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Tiff;
@@ -309,6 +308,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[Theory]
[WithFile(Cmyk, PixelTypes.Rgba32)]
[WithFile(CmykLzwPredictor, PixelTypes.Rgba32)]
+ [WithFile(CmykJpeg, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_Cmyk(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index a25424b6d9..7cf92104a1 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -968,6 +968,7 @@ public static class TestImages
public const string Cmyk = "Tiff/Cmyk.tiff";
public const string Cmyk64BitDeflate = "Tiff/cmyk_deflate_64bit.tiff";
public const string CmykLzwPredictor = "Tiff/Cmyk-lzw-predictor.tiff";
+ public const string CmykJpeg = "Tiff/Cmyk-jpeg.tiff";
public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff";
public const string Issues1891 = "Tiff/Issues/Issue1891.tiff";
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_Cmyk_Rgba32_Cmyk-jpeg.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_Cmyk_Rgba32_Cmyk-jpeg.png
new file mode 100644
index 0000000000..06d60e0303
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_Cmyk_Rgba32_Cmyk-jpeg.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7f68db78d765a7f36570cd7b57a1f06cfca24c3b4916d0692a4aa051209ec327
+size 616
diff --git a/tests/Images/Input/Tiff/Cmyk-jpeg.tiff b/tests/Images/Input/Tiff/Cmyk-jpeg.tiff
new file mode 100644
index 0000000000..e486403e4f
--- /dev/null
+++ b/tests/Images/Input/Tiff/Cmyk-jpeg.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:abb923e457acc31a7f18c46a7d58fc5a42f5c3d197236403921e3ee623fa4fac
+size 2046
diff --git a/tests/Images/Input/Tiff/Cmyk-planar-jpg.tiff b/tests/Images/Input/Tiff/Cmyk-planar-jpg.tiff
new file mode 100644
index 0000000000..e486403e4f
--- /dev/null
+++ b/tests/Images/Input/Tiff/Cmyk-planar-jpg.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:abb923e457acc31a7f18c46a7d58fc5a42f5c3d197236403921e3ee623fa4fac
+size 2046