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