From b6e09b5bb43369799a2b0dbb8c69230437f7e352 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 26 May 2022 18:03:48 +0200 Subject: [PATCH 01/10] Add support for decoding tiff with webp compressed data --- .../Decompressors/WebpTiffCompression.cs | 55 +++++++++++++++++++ .../Compression/TiffDecoderCompressionType.cs | 5 ++ .../Compression/TiffDecompressorsFactory.cs | 4 ++ .../Formats/Tiff/Constants/TiffCompression.cs | 8 +++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 6 ++ 5 files changed, 78 insertions(+) create mode 100644 src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs new file mode 100644 index 000000000..8cceecbc1 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is compressed as a webp stream. + /// + internal class WebpTiffCompression : TiffBaseDecompressor + { + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + /// The predictor. + public WebpTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) + { + using var image = Image.Load(stream, new WebpDecoder()); + CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); + } + + private static void CopyImageBytesToBuffer(Span buffer, Buffer2D pixelBuffer) + { + int offset = 0; + for (int y = 0; y < pixelBuffer.Height; y++) + { + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); + Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); + rgbBytes.CopyTo(buffer.Slice(offset)); + offset += rgbBytes.Length; + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs index d8843c107..77d77f98f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs @@ -47,5 +47,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// The image data is compressed as a JPEG stream. /// Jpeg = 7, + + /// + /// The image data is compressed as a WEBP stream. + /// + Webp = 8, } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index a7e2e276b..0e526f3d9 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -60,6 +60,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); return new JpegTiffCompression(configuration, allocator, width, bitsPerPixel, jpegTables, photometricInterpretation); + case TiffDecoderCompressionType.Webp: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new WebpTiffCompression(allocator, width, bitsPerPixel); + default: throw TiffThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs index 765f2c237..0591c24dd 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs @@ -103,5 +103,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants /// if this is chosen. /// OldDeflate = 32946, + + /// + /// Pixel data is compressed with webp encoder. + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + Webp = 50001, } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 6d99fed3e..1d52b95ac 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -470,6 +470,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } + case TiffCompression.Webp: + { + options.CompressionType = TiffDecoderCompressionType.Webp; + break; + } + default: { TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported"); From 9d461fa794a640db351143f3208d9327d46a8223 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 26 May 2022 18:09:10 +0200 Subject: [PATCH 02/10] Add test for tiff with webp compressed data --- src/ImageSharp/Formats/Tiff/README.md | 5 +++-- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 5 +++++ tests/ImageSharp.Tests/TestImages.cs | 2 +- tests/Images/Input/Tiff/webp_compressed.tiff | 3 +++ 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 tests/Images/Input/Tiff/webp_compressed.tiff diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 8cb327a7b..0e6f8a17c 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -37,11 +37,12 @@ |PackBits | Y | Y | | |CcittGroup3Fax | Y | Y | | |CcittGroup4Fax | Y | Y | | -|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | -|Old Jpeg | | | We should not even try to support this | +|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case. | +|Old Jpeg | | | We should not even try to support this. | |Jpeg (Technote 2) | Y | Y | | |Deflate (Technote 2) | Y | Y | Based on PNG Deflate. | |Old Deflate (Technote 2) | | Y | | +|Webp | | Y | | ### Photometric Interpretation Formats diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index b9569c7a4..9d4fa81e5 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -651,6 +651,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false); + [Theory] + [WithFile(WebpCompressed, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_WEbpCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false); + // https://github.com/SixLabors/ImageSharp/issues/1891 [Theory] [WithFile(Issues1891, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 023efd707..81c5a9a86 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -760,7 +760,7 @@ namespace SixLabors.ImageSharp.Tests public const string Calliphora_BiColorUncompressed = "Tiff/Calliphora_bicolor_uncompressed.tiff"; public const string Fax4Compressed = "Tiff/basi3p02_fax4.tiff"; public const string Fax4CompressedLowerOrderBitsFirst = "Tiff/basi3p02_fax4_lowerOrderBitsFirst.tiff"; - + public const string WebpCompressed = "Tiff/webp_compressed.tiff"; public const string CcittFax3AllTermCodes = "Tiff/ccitt_fax3_all_terminating_codes.tiff"; public const string CcittFax3AllMakeupCodes = "Tiff/ccitt_fax3_all_makeup_codes.tiff"; public const string HuffmanRleAllTermCodes = "Tiff/huffman_rle_all_terminating_codes.tiff"; diff --git a/tests/Images/Input/Tiff/webp_compressed.tiff b/tests/Images/Input/Tiff/webp_compressed.tiff new file mode 100644 index 000000000..71248e521 --- /dev/null +++ b/tests/Images/Input/Tiff/webp_compressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72fd7fa941aa6201faa5368349764b4c17b582bee9be65861bad6308a8c5e4fe +size 4898 From 49dd8d4725413201d2537e8e281b8214a76127c5 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 26 May 2022 18:23:19 +0200 Subject: [PATCH 03/10] Only execute tiff with webp test on windows --- .../ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 9d4fa81e5..7e5864dd4 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -653,8 +653,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(WebpCompressed, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_WEbpCompressed(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false); + public void TiffDecoder_CanDecode_WebpCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsWindows) + { + TestTiffDecoder(provider, useExactComparer: false); + } + } // https://github.com/SixLabors/ImageSharp/issues/1891 [Theory] From 52ad9c67f617378432f1d457d352f518715ed82a Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 16 Jul 2022 03:18:14 +0300 Subject: [PATCH 04/10] Jpeg colorspace deduction fix --- .../Decoder/SpectralConverter{TPixel}.cs | 2 +- .../Formats/Jpeg/JpegDecoderCore.cs | 52 +------------------ 2 files changed, 3 insertions(+), 51 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 532892e06..2c3dccdee 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } - var buffer = this.pixelBuffer; + Buffer2D buffer = this.pixelBuffer; this.pixelBuffer = null; return buffer; } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 58c85bd34..94439f35a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -90,11 +90,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private JFifMarker jFif; - /// - /// Whether the image has a JFIF marker. This is needed to determine, if the colorspace is YCbCr. - /// - private bool hasJFif; - /// /// Contains information about the Adobe marker. /// @@ -516,8 +511,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg 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 (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYCbCr) @@ -529,51 +522,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { 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(); - } - - if (this.hasJFif) - { - // JFIF implies 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) - { - return JpegColorSpace.RGB; - } - - // 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; - } - - // 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; - } - - // Some images are poorly encoded and contain incorrect colorspace transform metadata. - // We ignore that and always fall back to the default colorspace. + // Fallback to YCbCr 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) @@ -585,11 +541,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { return JpegColorSpace.Cmyk; } - - JpegThrowHelper.ThrowNotSupportedColorSpace(); } - // Fallback to cmyk as neither of cmyk nor ycck have 'special' component ids. + // Fallback to CMYK return JpegColorSpace.Cmyk; } @@ -757,8 +711,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// 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. From 0cc3c683ccc2a5250ea71827e752db89f16d9da7 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 16 Jul 2022 04:15:26 +0300 Subject: [PATCH 05/10] Tests --- .../Jpeg/Components/Decoder/AdobeMarker.cs | 2 +- .../Formats/Jpeg/JpegDecoderCore.cs | 69 +++++++++++-------- .../Formats/Jpg/JpegDecoderTests.Internal.cs | 69 +++++++++++++++++++ 3 files changed, 109 insertions(+), 31 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs index b41d52aa4..63a295d43 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/AdobeMarker.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 94439f35a..5104f606d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -85,6 +85,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private byte[] xmpData; + /// + /// Whether the image has a APP14 adobe marker. This is needed to determine image encoded colorspace. + /// + private bool hasAdobeMarker; + /// /// Contains information about the JFIF marker. /// @@ -501,49 +506,48 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Returns the correct colorspace based on the image component count and the jpeg frame component id's. /// /// The 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) - { - return JpegColorSpace.Grayscale; - } - if (componentCount == 3) { - 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; - } + return JpegColorSpace.RGB; } - // Fallback to YCbCr return JpegColorSpace.YCbCr; } if (componentCount == 4) { - if (!this.adobe.Equals(default)) + if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformYcck) { - if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYcck) - { - return JpegColorSpace.Ycck; - } - - if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown) - { - return JpegColorSpace.Cmyk; - } + return JpegColorSpace.Ycck; } - // Fallback to CMYK + return JpegColorSpace.Cmyk; + } + + JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount); + return default; + } + + internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount) + { + if (componentCount == 1) + { + return JpegColorSpace.Grayscale; + } + + if (componentCount == 3) + { + return JpegColorSpace.YCbCr; + } + + if (componentCount == 4) + { return JpegColorSpace.Cmyk; } @@ -1013,7 +1017,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg 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) { @@ -1260,7 +1267,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg 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/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs new file mode 100644 index 000000000..71951cfef --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs @@ -0,0 +1,69 @@ +// 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(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)); + } +} From 1959bf6531963444897b2f5c6c3b73b8945e3f8b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 16 Jul 2022 14:19:23 +0200 Subject: [PATCH 06/10] Always use RgbJpegSpectralConverter for decompressed jpeg data --- .../Tiff/Compression/Decompressors/JpegTiffCompression.cs | 5 +++-- .../Compression/Decompressors/RgbJpegSpectralConverter.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index 3b2c765b3..3c0ebbb6f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -73,8 +73,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors case TiffPhotometricInterpretation.YCbCr: case TiffPhotometricInterpretation.Rgb: { - using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ? - new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration); + // The jpeg data should treated as RGB color space. If the PhotometricInterpretation is YCbCr, + // the conversion to RGB will be handled in the next step by the YCbCr color decoder. + using SpectralConverter spectralConverter = new RgbJpegSpectralConverter(this.configuration); 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 index a83518064..fa7d0ffc9 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs @@ -8,8 +8,9 @@ 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. + /// Spectral converter for TIFF's which use the JPEG compression. + /// The compressed jpeg data should be always treated as RGB color space. + /// If PhotometricInterpretation indicates the data is YCbCr, the color decoder will handle the conversion. /// /// The type of the pixel. internal sealed class RgbJpegSpectralConverter : SpectralConverter From 1c9777e628f448509aa16a631609f845e24e9c8e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 16 Jul 2022 17:59:47 +0300 Subject: [PATCH 07/10] Tiif decoding fix --- .../Formats/Jpeg/JpegDecoderCore.cs | 15 ++++-- .../Decompressors/JpegTiffCompression.cs | 8 +-- .../Decompressors/RgbJpegSpectralConverter.cs | 32 ------------ .../TiffJpegSpectralConverter{TPixel}.cs | 50 +++++++++++++++++++ .../Formats/Jpg/JpegDecoderTests.Internal.cs | 3 +- 5 files changed, 68 insertions(+), 40 deletions(-) delete mode 100644 src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs create mode 100644 src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 5104f606d..f75525ab9 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -503,9 +503,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// 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 internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount, ref AdobeMarker adobeMarker) @@ -534,6 +534,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return default; } + /// + /// Returns encoded colorspace based on the component count and component ids. + /// + /// + /// Must take into account atleast RGB component identifiers i.e. [82, 71, 66] + /// as TIFF images with jpeg encoding don't have APP14 marker. + /// + /// Number of components. + /// The internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount) { if (componentCount == 1) @@ -1216,7 +1225,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg 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]; diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index 3c0ebbb6f..e3df4b565 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -59,7 +59,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors 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,9 +74,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors case TiffPhotometricInterpretation.YCbCr: case TiffPhotometricInterpretation.Rgb: { - // The jpeg data should treated as RGB color space. If the PhotometricInterpretation is YCbCr, - // the conversion to RGB will be handled in the next step by the YCbCr color decoder. - using SpectralConverter spectralConverter = new RgbJpegSpectralConverter(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 fa7d0ffc9..000000000 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -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 TIFF's which use the JPEG compression. - /// The compressed jpeg data should be always treated as RGB color space. - /// If PhotometricInterpretation indicates the data is YCbCr, the color decoder will handle the conversion. - /// - /// 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 000000000..ea550b126 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +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 index 71951cfef..e4e59cdd9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs @@ -33,8 +33,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { 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); @@ -46,6 +46,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public void DeduceJpegColorSpaceAdobeMarker_ShouldThrowOnUnsupportedComponentCount(byte componentCount) { AdobeMarker adobeMarker = default; + Assert.Throws(() => JpegDecoderCore.DeduceJpegColorSpace(componentCount, ref adobeMarker)); } From 95273d35b65838eaec6e2dcff7f21d6dbd58294d Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 16 Jul 2022 18:37:06 +0300 Subject: [PATCH 08/10] Added grayscale app14 support, build fix --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 5 +++++ .../Decompressors/TiffJpegSpectralConverter{TPixel}.cs | 2 +- .../Formats/Jpg/JpegDecoderTests.Internal.cs | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 69770dfe4..48d397a54 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -510,6 +510,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount, ref AdobeMarker adobeMarker) { + if (componentCount == 1) + { + return JpegColorSpace.Grayscale; + } + if (componentCount == 3) { if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs index ea550b126..ced6b9027 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs @@ -1,5 +1,5 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs index e4e59cdd9..6bf7ae88f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs @@ -25,6 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.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)] From 8ca517b58670d261df2931f2067850134613a148 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 16 Jul 2022 21:43:36 +0300 Subject: [PATCH 09/10] Docs fix --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 48d397a54..91606e9ef 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -540,12 +540,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Returns encoded colorspace based on the component count and component ids. + /// Returns encoded colorspace based on the component count. /// - /// - /// Must take into account atleast RGB component identifiers i.e. [82, 71, 66] - /// as TIFF images with jpeg encoding don't have APP14 marker. - /// /// Number of components. /// The internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount) From 88f106d3b20eea78d6c3b6f01b5919a8fa10a193 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 18 Jul 2022 01:18:09 +0200 Subject: [PATCH 10/10] Fix file header copyright text --- .../Tiff/Compression/Decompressors/WebpTiffCompression.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs index 8cceecbc1..0d63382ff 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs @@ -1,5 +1,5 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. using System; using System.Runtime.InteropServices;