Browse Source

Merge branch 'main' into js/decoder-options

pull/2180/head
James Jackson-South 4 years ago
parent
commit
688b9efded
  1. 115
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  2. 6
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
  3. 31
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
  4. 50
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs
  5. 55
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs
  6. 5
      src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
  7. 4
      src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
  8. 8
      src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs
  9. 5
      src/ImageSharp/Formats/Tiff/README.md
  10. 6
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  11. 71
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs
  12. 11
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  13. 2
      tests/ImageSharp.Tests/TestImages.cs
  14. 3
      tests/Images/Input/Tiff/webp_compressed.tiff

115
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -86,14 +86,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private byte[] xmpData;
/// <summary>
/// Contains information about the JFIF marker.
/// Whether the image has a APP14 adobe marker. This is needed to determine image encoded colorspace.
/// </summary>
private JFifMarker jFif;
private bool hasAdobeMarker;
/// <summary>
/// Whether the image has a JFIF marker. This is needed to determine, if the colorspace is YCbCr.
/// Contains information about the JFIF marker.
/// </summary>
private bool hasJFif;
private JFifMarker jFif;
/// <summary>
/// Contains information about the Adobe marker.
@ -507,11 +507,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
/// <summary>
/// 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.
/// </summary>
/// <param name="componentCount">The number of components.</param>
/// <param name="componentCount">Number of components.</param>
/// <param name="adobeMarker">Parsed adobe APP14 marker.</param>
/// <returns>The <see cref="JpegColorSpace"/></returns>
private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount, ref AdobeMarker adobeMarker)
{
if (componentCount == 1)
{
@ -520,80 +521,47 @@ 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 (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;
}
/// <summary>
/// Returns encoded colorspace based on the component count.
/// </summary>
/// <param name="componentCount">Number of components.</param>
/// <returns>The <see cref="JpegColorSpace"/></returns>
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;
}
@ -761,8 +729,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="remaining">The remaining bytes in the segment block.</param>
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.
@ -1065,7 +1031,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)
{
@ -1258,7 +1227,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];
@ -1309,7 +1278,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)

6
src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs

@ -73,10 +73,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
case TiffPhotometricInterpretation.YCbCr:
case TiffPhotometricInterpretation.Rgb:
{
using SpectralConverter<Rgb24> spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr
? new RgbJpegSpectralConverter<Rgb24>(this.options.GeneralOptions.Configuration)
: new SpectralConverter<Rgb24>(this.options.GeneralOptions.Configuration);
using SpectralConverter<Rgb24> spectralConverter =
new TiffJpegSpectralConverter<Rgb24>(configuration, this.photometricInterpretation);
var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None);
jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
jpegDecoder.ParseStream(stream, spectralConverter, CancellationToken.None);

31
src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs

@ -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
{
/// <summary>
/// Spectral converter for YCbCr TIFF's which use the JPEG compression.
/// The jpeg data should be always treated as RGB color space.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
internal sealed class RgbJpegSpectralConverter<TPixel> : SpectralConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="RgbJpegSpectralConverter{TPixel}"/> class.
/// This Spectral converter will always convert the pixel data to RGB color.
/// </summary>
/// <param name="configuration">The configuration.</param>
public RgbJpegSpectralConverter(Configuration configuration)
: base(configuration)
{
}
/// <inheritdoc/>
protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, frame.Precision);
}
}

50
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
{
/// <summary>
/// Spectral converter for YCbCr TIFF's which use the JPEG compression.
/// The jpeg data should be always treated as RGB color space.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
internal sealed class TiffJpegSpectralConverter<TPixel> : SpectralConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TiffPhotometricInterpretation photometricInterpretation;
/// <summary>
/// Initializes a new instance of the <see cref="TiffJpegSpectralConverter{TPixel}"/> class.
/// This Spectral converter will always convert the pixel data to RGB color.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="photometricInterpretation">Tiff photometric interpretation.</param>
public TiffJpegSpectralConverter(Configuration configuration, TiffPhotometricInterpretation photometricInterpretation)
: base(configuration)
=> this.photometricInterpretation = photometricInterpretation;
/// <inheritdoc/>
protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData)
{
JpegColorSpace colorSpace = GetJpegColorSpaceFromPhotometricInterpretation(this.photometricInterpretation);
return JpegColorConverterBase.GetConverter(colorSpace, frame.Precision);
}
/// <remarks>
/// This converter must be used only for RGB and YCbCr color spaces for performance reasons.
/// For grayscale images <see cref="GrayJpegSpectralConverter{TPixel}"/> must be used.
/// </remarks>
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}"),
};
}
}

55
src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs

@ -0,0 +1,55 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
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
{
/// <summary>
/// Class to handle cases where TIFF image data is compressed as a webp stream.
/// </summary>
internal class WebpTiffCompression : TiffBaseDecompressor
{
/// <summary>
/// Initializes a new instance of the <see cref="WebpTiffCompression"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="width">The width of the image.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param>
/// <param name="predictor">The predictor.</param>
public WebpTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None)
: base(memoryAllocator, width, bitsPerPixel, predictor)
{
}
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span<byte> buffer)
{
using var image = Image.Load<Rgb24>(stream, new WebpDecoder());
CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer);
}
private static void CopyImageBytesToBuffer(Span<byte> buffer, Buffer2D<Rgb24> pixelBuffer)
{
int offset = 0;
for (int y = 0; y < pixelBuffer.Height; y++)
{
Span<Rgb24> pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y);
Span<byte> rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan);
rgbBytes.CopyTo(buffer.Slice(offset));
offset += rgbBytes.Length;
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

5
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.
/// </summary>
Jpeg = 7,
/// <summary>
/// The image data is compressed as a WEBP stream.
/// </summary>
Webp = 8,
}
}

4
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(new() { GeneralOptions = options }, 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));
}

8
src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs

@ -103,5 +103,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants
/// if this is chosen.
/// </summary>
OldDeflate = 32946,
/// <summary>
/// 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.
/// </summary>
Webp = 50001,
}
}

5
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

6
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -488,6 +488,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");

71
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<NotSupportedException>(() => 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<NotSupportedException>(() => JpegDecoderCore.DeduceJpegColorSpace(componentCount));
}
}

11
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -667,6 +667,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffDecoder_CanDecode_JpegCompressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false);
[Theory]
[WithFile(WebpCompressed, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_WebpCompressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.IsWindows)
{
TestTiffDecoder(provider, useExactComparer: false);
}
}
// https://github.com/SixLabors/ImageSharp/issues/1891
[Theory]
[WithFile(Issues1891, PixelTypes.Rgba32)]

2
tests/ImageSharp.Tests/TestImages.cs

@ -763,8 +763,8 @@ namespace SixLabors.ImageSharp.Tests
public const string Fax4Compressed = "Tiff/basi3p02_fax4.tiff";
public const string Fax4Compressed2 = "Tiff/CCITTGroup4.tiff";
public const string Fax4CompressedLowerOrderBitsFirst = "Tiff/basi3p02_fax4_lowerOrderBitsFirst.tiff";
public const string WebpCompressed = "Tiff/webp_compressed.tiff";
public const string Fax4CompressedMinIsBlack = "Tiff/CCITTGroup4_minisblack.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";

3
tests/Images/Input/Tiff/webp_compressed.tiff

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72fd7fa941aa6201faa5368349764b4c17b582bee9be65861bad6308a8c5e4fe
size 4898
Loading…
Cancel
Save