diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index d5997e4128..11187fb910 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
@@ -86,14 +86,14 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
private byte[] xmpData;
///
- /// Contains information about the JFIF marker.
+ /// Whether the image has a APP14 adobe marker. This is needed to determine image encoded colorspace.
///
- private JFifMarker jFif;
+ private bool hasAdobeMarker;
///
- /// Whether the image has a JFIF marker. This is needed to determine, if the colorspace is YCbCr.
+ /// Contains information about the JFIF marker.
///
- private bool hasJFif;
+ private JFifMarker jFif;
///
/// Contains information about the Adobe marker.
@@ -517,11 +517,12 @@ public void Dispose()
}
///
- /// Returns the correct colorspace based on the image component count and the jpeg frame component id's.
+ /// Returns encoded colorspace based on the adobe APP14 marker.
///
- /// The number of components.
+ /// Number of components.
+ /// Parsed adobe APP14 marker.
/// The
- private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
+ internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount, ref AdobeMarker adobeMarker)
{
if (componentCount == 1)
{
@@ -530,80 +531,47 @@ private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
if (componentCount == 3)
{
- // We prioritize adobe marker over jfif marker, if somebody really encoded this image with redundant adobe marker,
- // then it's most likely an adobe jfif image.
- if (!this.adobe.Equals(default))
+ if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
{
- if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYCbCr)
- {
- return JpegColorSpace.YCbCr;
- }
-
- if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
- {
- return JpegColorSpace.RGB;
- }
-
- // Fallback to the id color deduction: If these values are 1-3 for a 3-channel image, then the image is assumed to be YCbCr.
- if (this.Components[2].Id == 3 && this.Components[1].Id == 2 && this.Components[0].Id == 1)
- {
- return JpegColorSpace.YCbCr;
- }
-
- JpegThrowHelper.ThrowNotSupportedColorSpace();
+ return JpegColorSpace.RGB;
}
- if (this.hasJFif)
- {
- // JFIF implies YCbCr.
- return JpegColorSpace.YCbCr;
- }
+ return JpegColorSpace.YCbCr;
+ }
- // Fallback to the id color deduction.
- // If the component Id's are R, G, B in ASCII the colorspace is RGB and not YCbCr.
- // See: https://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#color
- if (this.Components[2].Id == 66 && this.Components[1].Id == 71 && this.Components[0].Id == 82)
+ if (componentCount == 4)
+ {
+ if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformYcck)
{
- return JpegColorSpace.RGB;
+ return JpegColorSpace.Ycck;
}
- // If these values are 1-3 for a 3-channel image, then the image is assumed to be YCbCr.
- if (this.Components[2].Id == 3 && this.Components[1].Id == 2 && this.Components[0].Id == 1)
- {
- return JpegColorSpace.YCbCr;
- }
+ return JpegColorSpace.Cmyk;
+ }
- // 3-channel non-subsampled images are assumed to be RGB.
- if (this.Components[2].VerticalSamplingFactor == 1 && this.Components[1].VerticalSamplingFactor == 1 && this.Components[0].VerticalSamplingFactor == 1 &&
- this.Components[2].HorizontalSamplingFactor == 1 && this.Components[1].HorizontalSamplingFactor == 1 && this.Components[0].HorizontalSamplingFactor == 1)
- {
- return JpegColorSpace.RGB;
- }
+ JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount);
+ return default;
+ }
+
+ ///
+ /// Returns encoded colorspace based on the component count.
+ ///
+ /// Number of components.
+ /// The
+ internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount)
+ {
+ if (componentCount == 1)
+ {
+ return JpegColorSpace.Grayscale;
+ }
- // Some images are poorly encoded and contain incorrect colorspace transform metadata.
- // We ignore that and always fall back to the default colorspace.
+ if (componentCount == 3)
+ {
return JpegColorSpace.YCbCr;
}
if (componentCount == 4)
{
- // jfif images doesn't not support 4 component images, so we only check adobe.
- if (!this.adobe.Equals(default))
- {
- if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYcck)
- {
- return JpegColorSpace.Ycck;
- }
-
- if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
- {
- return JpegColorSpace.Cmyk;
- }
-
- JpegThrowHelper.ThrowNotSupportedColorSpace();
- }
-
- // Fallback to cmyk as neither of cmyk nor ycck have 'special' component ids.
return JpegColorSpace.Cmyk;
}
@@ -771,8 +739,6 @@ private void ExtendProfile(ref byte[] profile, byte[] extension)
/// The remaining bytes in the segment block.
private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining)
{
- this.hasJFif = true;
-
// We can only decode JFif identifiers.
// Some images contain multiple JFIF markers (Issue 1932) so we check to see
// if it's already been read.
@@ -1075,7 +1041,10 @@ private void ProcessApp14Marker(BufferedReadStream stream, int remaining)
stream.Read(this.temp, 0, MarkerLength);
remaining -= MarkerLength;
- AdobeMarker.TryParse(this.temp, out this.adobe);
+ if (AdobeMarker.TryParse(this.temp, out this.adobe))
+ {
+ this.hasAdobeMarker = true;
+ }
if (remaining > 0)
{
@@ -1268,7 +1237,7 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining,
int maxH = 0;
int maxV = 0;
int index = 0;
- for (int i = 0; i < componentCount; i++)
+ for (int i = 0; i < this.Frame.Components.Length; i++)
{
// 1 byte: component identifier
byte componentId = this.temp[index];
@@ -1319,7 +1288,9 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining,
index += componentBytes;
}
- this.ColorSpace = this.DeduceJpegColorSpace(componentCount);
+ this.ColorSpace = this.hasAdobeMarker
+ ? DeduceJpegColorSpace(componentCount, ref this.adobe)
+ : DeduceJpegColorSpace(componentCount);
this.Metadata.GetJpegMetadata().ColorType = this.DeduceJpegColorType();
if (!metadataOnly)
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
index d0543917a0..0d9a6ba139 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
@@ -59,7 +59,8 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
{
- using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(this.configuration);
+ using SpectralConverter spectralConverterGray =
+ new GrayJpegSpectralConverter(this.configuration);
var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, CancellationToken.None);
jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray);
jpegDecoder.ParseStream(stream, spectralConverterGray, CancellationToken.None);
@@ -73,8 +74,8 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
case TiffPhotometricInterpretation.YCbCr:
case TiffPhotometricInterpretation.Rgb:
{
- using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ?
- new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration);
+ using SpectralConverter spectralConverter =
+ new TiffJpegSpectralConverter(this.configuration, this.photometricInterpretation);
var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None);
jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
jpegDecoder.ParseStream(stream, spectralConverter, CancellationToken.None);
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
deleted file mode 100644
index f8cf3e2a9f..0000000000
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
-using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
-using SixLabors.ImageSharp.PixelFormats;
-
-namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
-{
- ///
- /// Spectral converter for YCbCr TIFF's which use the JPEG compression.
- /// The jpeg data should be always treated as RGB color space.
- ///
- /// The type of the pixel.
- internal sealed class RgbJpegSpectralConverter : SpectralConverter
- where TPixel : unmanaged, IPixel
- {
- ///
- /// Initializes a new instance of the class.
- /// This Spectral converter will always convert the pixel data to RGB color.
- ///
- /// The configuration.
- public RgbJpegSpectralConverter(Configuration configuration)
- : base(configuration)
- {
- }
-
- ///
- protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, frame.Precision);
- }
-}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs
new file mode 100644
index 0000000000..ced6b9027c
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
+using SixLabors.ImageSharp.Formats.Tiff.Constants;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
+{
+ ///
+ /// Spectral converter for YCbCr TIFF's which use the JPEG compression.
+ /// The jpeg data should be always treated as RGB color space.
+ ///
+ /// The type of the pixel.
+ internal sealed class TiffJpegSpectralConverter : SpectralConverter
+ where TPixel : unmanaged, IPixel
+ {
+ private readonly TiffPhotometricInterpretation photometricInterpretation;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// This Spectral converter will always convert the pixel data to RGB color.
+ ///
+ /// The configuration.
+ /// Tiff photometric interpretation.
+ public TiffJpegSpectralConverter(Configuration configuration, TiffPhotometricInterpretation photometricInterpretation)
+ : base(configuration)
+ => this.photometricInterpretation = photometricInterpretation;
+
+ ///
+ protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData)
+ {
+ JpegColorSpace colorSpace = GetJpegColorSpaceFromPhotometricInterpretation(this.photometricInterpretation);
+ return JpegColorConverterBase.GetConverter(colorSpace, frame.Precision);
+ }
+
+ ///
+ /// This converter must be used only for RGB and YCbCr color spaces for performance reasons.
+ /// For grayscale images must be used.
+ ///
+ private static JpegColorSpace GetJpegColorSpaceFromPhotometricInterpretation(TiffPhotometricInterpretation interpretation)
+ => interpretation switch
+ {
+ TiffPhotometricInterpretation.Rgb => JpegColorSpace.RGB,
+ TiffPhotometricInterpretation.YCbCr => JpegColorSpace.RGB,
+ _ => throw new InvalidImageContentException($"Invalid tiff photometric interpretation for jpeg encoding: {interpretation}"),
+ };
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs
new file mode 100644
index 0000000000..6bf7ae88fa
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
+using SixLabors.ImageSharp.Tests.TestUtilities;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
+using Xunit;
+using Xunit.Abstractions;
+
+// ReSharper disable InconsistentNaming
+namespace SixLabors.ImageSharp.Tests.Formats.Jpg
+{
+ [Trait("Format", "Jpg")]
+ public partial class JpegDecoderTests
+ {
+ [Theory]
+ [InlineData(1, 0, JpegColorSpace.Grayscale)]
+ [InlineData(3, JpegConstants.Adobe.ColorTransformUnknown, JpegColorSpace.RGB)]
+ [InlineData(3, JpegConstants.Adobe.ColorTransformYCbCr, JpegColorSpace.YCbCr)]
+ [InlineData(4, JpegConstants.Adobe.ColorTransformUnknown, JpegColorSpace.Cmyk)]
+ [InlineData(4, JpegConstants.Adobe.ColorTransformYcck, JpegColorSpace.Ycck)]
+ internal void DeduceJpegColorSpaceAdobeMarker_ShouldReturnValidColorSpace(byte componentCount, byte adobeFlag, JpegColorSpace expectedColorSpace)
+ {
+ byte[] adobeMarkerPayload = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, adobeFlag };
+ ProfileResolver.AdobeMarker.CopyTo(adobeMarkerPayload);
+ _ = AdobeMarker.TryParse(adobeMarkerPayload, out AdobeMarker adobeMarker);
+
+ JpegColorSpace actualColorSpace = JpegDecoderCore.DeduceJpegColorSpace(componentCount, ref adobeMarker);
+
+ Assert.Equal(expectedColorSpace, actualColorSpace);
+ }
+
+ [Theory]
+ [InlineData(2)]
+ [InlineData(5)]
+ public void DeduceJpegColorSpaceAdobeMarker_ShouldThrowOnUnsupportedComponentCount(byte componentCount)
+ {
+ AdobeMarker adobeMarker = default;
+
+ Assert.Throws(() => JpegDecoderCore.DeduceJpegColorSpace(componentCount, ref adobeMarker));
+ }
+
+ [Theory]
+ [InlineData(1, JpegColorSpace.Grayscale)]
+ [InlineData(3, JpegColorSpace.YCbCr)]
+ [InlineData(4, JpegColorSpace.Cmyk)]
+ internal void DeduceJpegColorSpace_ShouldReturnValidColorSpace(byte componentCount, JpegColorSpace expectedColorSpace)
+ {
+ JpegColorSpace actualColorSpace = JpegDecoderCore.DeduceJpegColorSpace(componentCount);
+
+ Assert.Equal(expectedColorSpace, actualColorSpace);
+ }
+
+ [Theory]
+ [InlineData(2)]
+ [InlineData(5)]
+ public void DeduceJpegColorSpace_ShouldThrowOnUnsupportedComponentCount(byte componentCount)
+ => Assert.Throws(() => JpegDecoderCore.DeduceJpegColorSpace(componentCount));
+ }
+}