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)); + } +}