Skip to content

Commit 0eb411c

Browse files
Merge pull request #2124 from SixLabors/bp/Issue2123
Jpeg compressed tiff: jpeg decoder should handle the conversion from YCbCr to RGB
2 parents c661ab1 + c167062 commit 0eb411c

23 files changed

Lines changed: 128 additions & 27 deletions

src/ImageSharp/Common/Helpers/Numerics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,7 @@ public static uint RotateRightSoftwareFallback(uint value, int offset)
968968
/// Tells whether input value is outside of the given range.
969969
/// </summary>
970970
/// <param name="value">Value.</param>
971-
/// <param name="min">Mininum value, inclusive.</param>
971+
/// <param name="min">Minimum value, inclusive.</param>
972972
/// <param name="max">Maximum value, inclusive.</param>
973973
[MethodImpl(MethodImplOptions.AggressiveInlining)]
974974
public static bool IsOutOfRange(int value, int min, int max)

src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
9090
/// </summary>
9191
private JFifMarker jFif;
9292

93+
/// <summary>
94+
/// Whether the image has a JFIF marker. This is needed to determine, if the colorspace is YCbCr.
95+
/// </summary>
96+
private bool hasJFif;
97+
9398
/// <summary>
9499
/// Contains information about the Adobe marker.
95100
/// </summary>
@@ -514,27 +519,75 @@ private JpegColorSpace DeduceJpegColorSpace(byte componentCount)
514519

515520
if (componentCount == 3)
516521
{
517-
if (!this.adobe.Equals(default) && this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
522+
// We prioritize adobe marker over jfif marker, if somebody really encoded this image with redundant adobe marker,
523+
// then it's most likely an adobe jfif image.
524+
if (!this.adobe.Equals(default))
518525
{
519-
return JpegColorSpace.RGB;
526+
if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYCbCr)
527+
{
528+
return JpegColorSpace.YCbCr;
529+
}
530+
531+
if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
532+
{
533+
return JpegColorSpace.RGB;
534+
}
535+
536+
// 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.
537+
if (this.Components[2].Id == 3 && this.Components[1].Id == 2 && this.Components[0].Id == 1)
538+
{
539+
return JpegColorSpace.YCbCr;
540+
}
541+
542+
JpegThrowHelper.ThrowNotSupportedColorSpace();
543+
}
544+
545+
if (this.hasJFif)
546+
{
547+
// JFIF implies YCbCr.
548+
return JpegColorSpace.YCbCr;
520549
}
521550

551+
// Fallback to the id color deduction.
522552
// If the component Id's are R, G, B in ASCII the colorspace is RGB and not YCbCr.
553+
// See: https://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#color
523554
if (this.Components[2].Id == 66 && this.Components[1].Id == 71 && this.Components[0].Id == 82)
524555
{
525556
return JpegColorSpace.RGB;
526557
}
527558

559+
// 3-channel non-subsampled images are assumed to be RGB.
560+
if (this.Components[2].VerticalSamplingFactor == 1 && this.Components[1].VerticalSamplingFactor == 1 && this.Components[0].VerticalSamplingFactor == 1 &&
561+
this.Components[2].HorizontalSamplingFactor == 1 && this.Components[1].HorizontalSamplingFactor == 1 && this.Components[0].HorizontalSamplingFactor == 1)
562+
{
563+
return JpegColorSpace.RGB;
564+
}
565+
528566
// Some images are poorly encoded and contain incorrect colorspace transform metadata.
529567
// We ignore that and always fall back to the default colorspace.
530568
return JpegColorSpace.YCbCr;
531569
}
532570

533571
if (componentCount == 4)
534572
{
535-
return this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYcck
536-
? JpegColorSpace.Ycck
537-
: JpegColorSpace.Cmyk;
573+
// jfif images doesn't not support 4 component images, so we only check adobe.
574+
if (!this.adobe.Equals(default))
575+
{
576+
if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYcck)
577+
{
578+
return JpegColorSpace.Ycck;
579+
}
580+
581+
if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown)
582+
{
583+
return JpegColorSpace.Cmyk;
584+
}
585+
586+
JpegThrowHelper.ThrowNotSupportedColorSpace();
587+
}
588+
589+
// Fallback to cmyk as neither of cmyk nor ycck have 'special' component ids.
590+
return JpegColorSpace.Cmyk;
538591
}
539592

540593
JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount);
@@ -701,6 +754,8 @@ private void ExtendProfile(ref byte[] profile, byte[] extension)
701754
/// <param name="remaining">The remaining bytes in the segment block.</param>
702755
private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining)
703756
{
757+
this.hasJFif = true;
758+
704759
// We can only decode JFif identifiers.
705760
// Some images contain multiple JFIF markers (Issue 1932) so we check to see
706761
// if it's already been read.

src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,8 @@ internal static class JpegThrowHelper
5151

5252
[MethodImpl(InliningOptions.ColdPath)]
5353
public static void ThrowNotSupportedComponentCount(int componentCount) => throw new NotSupportedException($"Images with {componentCount} components are not supported.");
54+
55+
[MethodImpl(InliningOptions.ColdPath)]
56+
public static void ThrowNotSupportedColorSpace() => throw new NotSupportedException("Image color space could not be deduced.");
5457
}
5558
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,11 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
6565
jpegDecoder.ParseStream(stream, spectralConverterGray, CancellationToken.None);
6666

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

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

8482
// TODO: Should we pass through the CancellationToken from the tiff decoder?
85-
using var decompressedBuffer = spectralConverter.GetPixelBuffer(CancellationToken.None);
83+
using Buffer2D<Rgb24> decompressedBuffer = spectralConverter.GetPixelBuffer(CancellationToken.None);
8684
CopyImageBytesToBuffer(buffer, decompressedBuffer);
8785
break;
8886
}

src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,16 @@ internal class YCbCrConverter
1919

2020
private static readonly Rational[] DefaultLuma =
2121
{
22-
new Rational(299, 1000),
23-
new Rational(587, 1000),
24-
new Rational(114, 1000)
22+
new(299, 1000),
23+
new(587, 1000),
24+
new(114, 1000)
2525
};
2626

2727
private static readonly Rational[] DefaultReferenceBlackWhite =
2828
{
29-
new Rational(0, 1), new Rational(255, 1),
30-
new Rational(128, 1), new Rational(255, 1),
31-
new Rational(128, 1), new Rational(255, 1)
29+
new(0, 1), new(255, 1),
30+
new(128, 1), new(255, 1),
31+
new(128, 1), new(255, 1)
3232
};
3333

3434
public YCbCrConverter(Rational[] referenceBlackAndWhite, Rational[] coefficients)

src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,15 @@ public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, in
3939
Span<byte> tmpBufferSpan = tmpBuffer.GetSpan();
4040
ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], data, tmpBufferSpan);
4141
ycbcrData = tmpBufferSpan;
42+
this.DecodeYCbCrData(pixels, left, top, width, height, ycbcrData);
43+
return;
4244
}
4345

46+
this.DecodeYCbCrData(pixels, left, top, width, height, ycbcrData);
47+
}
48+
49+
private void DecodeYCbCrData(Buffer2D<TPixel> pixels, int left, int top, int width, int height, ReadOnlySpan<byte> ycbcrData)
50+
{
4451
var color = default(TPixel);
4552
int offset = 0;
4653
int widthPadding = 0;

src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
228228
/// <typeparam name="TPixel">The pixel format.</typeparam>
229229
/// <param name="tags">The IFD tags.</param>
230230
/// <param name="cancellationToken">The token to monitor cancellation.</param>
231-
/// <returns> The tiff frame. </returns>
231+
/// <returns>The tiff frame.</returns>
232232
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken)
233233
where TPixel : unmanaged, IPixel<TPixel>
234234
{

src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,14 @@ private static void ParseCompression(this TiffDecoderCore options, TiffCompressi
459459
case TiffCompression.Jpeg:
460460
{
461461
options.CompressionType = TiffDecoderCompressionType.Jpeg;
462+
463+
if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null)
464+
{
465+
// Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data.
466+
options.PhotometricInterpretation = TiffPhotometricInterpretation.Rgb;
467+
options.ColorType = TiffColorType.Rgb;
468+
}
469+
462470
break;
463471
}
464472

tests/Directory.Build.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<PackageReference Update="BenchmarkDotNet" Version="0.13.0" />
2222
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.0" Condition="'$(IsWindows)'=='true'" />
2323
<PackageReference Update="Colourful" Version="3.0.0" />
24-
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="8.0.1" />
24+
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="11.1.2" />
2525
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="6.0.0-beta.21311.3" />
2626
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="6.0.0-beta.21311.3" />
2727
<PackageReference Update="Moq" Version="4.14.6" />

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,6 @@ public void TiffDecoder_CanDecode_24Bit_Gray<TPixel>(TestImageProvider<TPixel> p
303303
[Theory]
304304
[WithFile(FlowerYCbCr888Contiguous, PixelTypes.Rgba32)]
305305
[WithFile(FlowerYCbCr888Planar, PixelTypes.Rgba32)]
306-
[WithFile(RgbYCbCr888Contiguoush1v1, PixelTypes.Rgba32)]
307-
[WithFile(RgbYCbCr888Contiguoush2v1, PixelTypes.Rgba32)]
308306
[WithFile(RgbYCbCr888Contiguoush2v2, PixelTypes.Rgba32)]
309307
[WithFile(RgbYCbCr888Contiguoush4v4, PixelTypes.Rgba32)]
310308
[WithFile(FlowerYCbCr888Contiguoush2v1, PixelTypes.Rgba32)]
@@ -317,6 +315,7 @@ public void TiffDecoder_CanDecode_YCbCr_24Bit<TPixel>(TestImageProvider<TPixel>
317315
// converting the pixel data from Magick.NET to our format with YCbCr?
318316
using Image<TPixel> image = provider.GetImage();
319317
image.DebugSave(provider);
318+
image.CompareToReferenceOutput(ImageComparer.Exact, provider);
320319
}
321320

322321
[Theory]
@@ -642,10 +641,13 @@ public void CanDecodeJustOneFrame<TPixel>(TestImageProvider<TPixel> provider)
642641

643642
[Theory]
644643
[WithFile(RgbJpegCompressed, PixelTypes.Rgba32)]
644+
[WithFile(RgbJpegCompressed2, PixelTypes.Rgba32)]
645645
[WithFile(RgbWithStripsJpegCompressed, PixelTypes.Rgba32)]
646646
[WithFile(YCbCrJpegCompressed, PixelTypes.Rgba32)]
647+
[WithFile(YCbCrJpegCompressed2, PixelTypes.Rgba32)]
647648
[WithFile(RgbJpegCompressedNoJpegTable, PixelTypes.Rgba32)]
648649
[WithFile(GrayscaleJpegCompressed, PixelTypes.Rgba32)]
650+
[WithFile(Issues2123, PixelTypes.Rgba32)]
649651
public void TiffDecoder_CanDecode_JpegCompressed<TPixel>(TestImageProvider<TPixel> provider)
650652
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false);
651653

0 commit comments

Comments
 (0)