Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f15b336
Fix issue disposing tmp buffer too early
brianpopow May 20, 2022
233dfe1
Update Magick reference to 11.1.2
brianpopow May 20, 2022
b6e26ed
Remove invalid test image: not actually ycbcr
brianpopow May 20, 2022
9268701
Another attempt: Remove invalid test image, not actually ycbcr
brianpopow May 20, 2022
82e7ac1
Override PhotometricInterpretation to RGB, if it's YCbCr and Jpeg com…
brianpopow May 20, 2022
3f192f9
Merge branch 'main' into bp/Issue2123
brianpopow May 20, 2022
c1c4f52
If component Id's are 1-3 for a 3-channel image, then the image is as…
brianpopow May 21, 2022
89668a6
3-channel non-subsampled images are assumed to be RGB.
brianpopow May 21, 2022
afc13e0
Add test for jpeg compressed tiff, which is actually RGB colorspace
brianpopow May 21, 2022
30e4354
JFIF implies YCbCr
brianpopow May 21, 2022
44c1ad5
Use flag to indicate if JFIF marker is present (invalid markers also …
brianpopow May 21, 2022
535372d
Another attempt to get deducing jpeg color space right
brianpopow May 21, 2022
58927f3
Also set color type to rgb, when compression is jpeg
brianpopow May 22, 2022
d652a4a
Add test case for replicating issue #2123
brianpopow May 22, 2022
e42fdd0
Change deducing color space according to review
brianpopow May 23, 2022
bc73322
Fix jpeg compressed tiff without jpeg table
brianpopow May 23, 2022
4e84800
Fallback to id color deduction for adobe marker
brianpopow May 23, 2022
c167062
Add compare to reference output for decoding tiff images with YCbCr data
brianpopow May 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ImageSharp/Common/Helpers/Numerics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ public static uint RotateRightSoftwareFallback(uint value, int offset)
/// Tells whether input value is outside of the given range.
/// </summary>
/// <param name="value">Value.</param>
/// <param name="min">Mininum value, inclusive.</param>
/// <param name="min">Minimum value, inclusive.</param>
/// <param name="max">Maximum value, inclusive.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsOutOfRange(int value, int min, int max)
Expand Down
32 changes: 32 additions & 0 deletions src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
/// </summary>
private JFifMarker jFif;

/// <summary>
/// Whether the image has a JFIF marker. This is needed to determine, if the colorspace is YCbCr.
/// </summary>
private bool hasJFif;

/// <summary>
/// Contains information about the Adobe marker.
/// </summary>
Expand Down Expand Up @@ -519,12 +524,37 @@ private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
return JpegColorSpace.RGB;
}

if (!this.adobe.Equals(default) && this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYCbCr)
{
return JpegColorSpace.YCbCr;
}

// 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 (this.hasJFif)
{
// JFIF implies YCbCr.
return JpegColorSpace.YCbCr;
}

// 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.
return JpegColorSpace.YCbCr;
Expand Down Expand Up @@ -701,6 +731,8 @@ private void ExtendProfile(ref byte[] profile, byte[] extension)
/// <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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,11 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
jpegDecoder.ParseStream(stream, spectralConverterGray, CancellationToken.None);

// TODO: Should we pass through the CancellationToken from the tiff decoder?
using var decompressedBuffer = spectralConverterGray.GetPixelBuffer(CancellationToken.None);
using Buffer2D<L8> decompressedBuffer = spectralConverterGray.GetPixelBuffer(CancellationToken.None);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
break;
}

// If the PhotometricInterpretation is YCbCr we explicitly assume the JPEG data is in RGB color space.
// There seems no other way to determine that the JPEG data is RGB colorspace (no APP14 marker, componentId's are not RGB).
case TiffPhotometricInterpretation.YCbCr:
case TiffPhotometricInterpretation.Rgb:
{
Expand All @@ -82,7 +80,7 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
jpegDecoder.ParseStream(stream, spectralConverter, CancellationToken.None);

// TODO: Should we pass through the CancellationToken from the tiff decoder?
using var decompressedBuffer = spectralConverter.GetPixelBuffer(CancellationToken.None);
using Buffer2D<Rgb24> decompressedBuffer = spectralConverter.GetPixelBuffer(CancellationToken.None);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ internal class YCbCrConverter

private static readonly Rational[] DefaultLuma =
{
new Rational(299, 1000),
new Rational(587, 1000),
new Rational(114, 1000)
new(299, 1000),
new(587, 1000),
new(114, 1000)
};

private static readonly Rational[] DefaultReferenceBlackWhite =
{
new Rational(0, 1), new Rational(255, 1),
new Rational(128, 1), new Rational(255, 1),
new Rational(128, 1), new Rational(255, 1)
new(0, 1), new(255, 1),
new(128, 1), new(255, 1),
new(128, 1), new(255, 1)
};

public YCbCrConverter(Rational[] referenceBlackAndWhite, Rational[] coefficients)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,15 @@ public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, in
Span<byte> tmpBufferSpan = tmpBuffer.GetSpan();
ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], data, tmpBufferSpan);
ycbcrData = tmpBufferSpan;
this.DecodeYCbCrData(pixels, left, top, width, height, ycbcrData);
return;
}

this.DecodeYCbCrData(pixels, left, top, width, height, ycbcrData);
}

private void DecodeYCbCrData(Buffer2D<TPixel> pixels, int left, int top, int width, int height, ReadOnlySpan<byte> ycbcrData)
{
var color = default(TPixel);
int offset = 0;
int widthPadding = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
/// <returns> The tiff frame. </returns>
/// <returns>The tiff frame.</returns>
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Expand Down
8 changes: 8 additions & 0 deletions src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ private static void ParseCompression(this TiffDecoderCore options, TiffCompressi
case TiffCompression.Jpeg:
{
options.CompressionType = TiffDecoderCompressionType.Jpeg;

if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null)
{
// Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data.
options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb;
options.ColorType = TiffColorType.Rgb;
}

break;
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<PackageReference Update="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.0" Condition="'$(IsWindows)'=='true'" />
<PackageReference Update="Colourful" Version="3.0.0" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="8.0.1" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="11.1.2" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="6.0.0-beta.21311.3" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="6.0.0-beta.21311.3" />
<PackageReference Update="Moq" Version="4.14.6" />
Expand Down
5 changes: 3 additions & 2 deletions tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,6 @@ public void TiffDecoder_CanDecode_24Bit_Gray<TPixel>(TestImageProvider<TPixel> p
[Theory]
[WithFile(FlowerYCbCr888Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerYCbCr888Planar, PixelTypes.Rgba32)]
[WithFile(RgbYCbCr888Contiguoush1v1, PixelTypes.Rgba32)]
[WithFile(RgbYCbCr888Contiguoush2v1, PixelTypes.Rgba32)]
[WithFile(RgbYCbCr888Contiguoush2v2, PixelTypes.Rgba32)]
[WithFile(RgbYCbCr888Contiguoush4v4, PixelTypes.Rgba32)]
[WithFile(FlowerYCbCr888Contiguoush2v1, PixelTypes.Rgba32)]
Expand Down Expand Up @@ -642,10 +640,13 @@ public void CanDecodeJustOneFrame<TPixel>(TestImageProvider<TPixel> provider)

[Theory]
[WithFile(RgbJpegCompressed, PixelTypes.Rgba32)]
[WithFile(RgbJpegCompressed2, PixelTypes.Rgba32)]
[WithFile(RgbWithStripsJpegCompressed, PixelTypes.Rgba32)]
[WithFile(YCbCrJpegCompressed, PixelTypes.Rgba32)]
[WithFile(YCbCrJpegCompressed2, PixelTypes.Rgba32)]
[WithFile(RgbJpegCompressedNoJpegTable, PixelTypes.Rgba32)]
[WithFile(GrayscaleJpegCompressed, PixelTypes.Rgba32)]
[WithFile(Issues2123, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_JpegCompressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false);

Expand Down
5 changes: 3 additions & 2 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ public static class Tiff
public const string RgbDeflatePredictor = "Tiff/rgb_deflate_predictor.tiff";
public const string RgbDeflateMultistrip = "Tiff/rgb_deflate_multistrip.tiff";
public const string RgbJpegCompressed = "Tiff/rgb_jpegcompression.tiff";
public const string RgbJpegCompressed2 = "Tiff/twain-rgb-jpeg-with-bogus-ycbcr-subsampling.tiff";
public const string RgbWithStripsJpegCompressed = "Tiff/rgb_jpegcompressed_stripped.tiff";
public const string RgbJpegCompressedNoJpegTable = "Tiff/rgb_jpegcompressed_nojpegtable.tiff";
public const string RgbLzwPredictor = "Tiff/rgb_lzw_predictor.tiff";
Expand Down Expand Up @@ -825,11 +826,10 @@ public static class Tiff
public const string FlowerYCbCr888Contiguoush2v1 = "Tiff/flower-ycbcr-contig-08_h2v1.tiff";
public const string FlowerYCbCr888Contiguoush2v2 = "Tiff/flower-ycbcr-contig-08_h2v2.tiff";
public const string FlowerYCbCr888Contiguoush4v4 = "Tiff/flower-ycbcr-contig-08_h4v4.tiff";
public const string RgbYCbCr888Contiguoush1v1 = "Tiff/rgb-ycbcr-contig-08_h1v1.tiff";
public const string RgbYCbCr888Contiguoush2v1 = "Tiff/rgb-ycbcr-contig-08_h2v1.tiff";
public const string RgbYCbCr888Contiguoush2v2 = "Tiff/rgb-ycbcr-contig-08_h2v2.tiff";
public const string RgbYCbCr888Contiguoush4v4 = "Tiff/rgb-ycbcr-contig-08_h4v4.tiff";
public const string YCbCrJpegCompressed = "Tiff/ycbcr_jpegcompressed.tiff";
public const string YCbCrJpegCompressed2 = "Tiff/ycbcr_jpegcompressed2.tiff";
public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff";
public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff";
public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff";
Expand Down Expand Up @@ -912,6 +912,7 @@ public static class Tiff

public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff";
public const string Issues1891 = "Tiff/Issues/Issue1891.tiff";
public const string Issues2123 = "Tiff/Issues/Issue2123.tiff";

public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff";
public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff";
Expand Down
3 changes: 3 additions & 0 deletions tests/Images/Input/Tiff/Issues/Issue2123.tiff
Git LFS file not shown
3 changes: 0 additions & 3 deletions tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff

This file was deleted.

Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/Images/Input/Tiff/ycbcr_jpegcompressed2.tiff
Git LFS file not shown