From 8b469b4368a3b6b5f6430558d70d4bb58300c183 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 8 Aug 2021 16:34:21 +0200 Subject: [PATCH 01/11] Add support for decoding ycbcr tiff's --- .../TiffColorDecoderFactory{TPixel}.cs | 11 +- .../TiffColorType.cs | 2 + .../YCbCrTiffColor{TPixel}.cs | 138 ++++++++++++++++++ .../Formats/Tiff/TiffDecoderCore.cs | 12 +- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 14 ++ src/ImageSharp/Primitives/Rational.cs | 45 ++---- .../Formats/Tiff/TiffDecoderTests.cs | 10 ++ tests/ImageSharp.Tests/TestImages.cs | 8 +- .../Input/Tiff/flower-rgb-contig-08.tiff | 3 + .../Input/Tiff/flower_ycbcr_contig-08.tiff | 3 + 10 files changed, 212 insertions(+), 34 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-08.tiff create mode 100644 tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 0c93998c4..010897c3c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -8,7 +8,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal static class TiffColorDecoderFactory where TPixel : unmanaged, IPixel { - public static TiffBaseColorDecoder Create(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) + public static TiffBaseColorDecoder Create( + TiffColorType colorType, + TiffBitsPerSample bitsPerSample, + ushort[] colorMap, + Rational[] referenceBlackAndWhite, + Rational[] ycbcrCoefficients, + ByteOrder byteOrder) { switch (colorType) { @@ -140,6 +146,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.NotNull(colorMap, "colorMap"); return new PaletteTiffColor(bitsPerSample, colorMap); + case TiffColorType.YCbCr: + return new YCbCrTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 331065d27..4e108f58e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -107,5 +107,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// RGB Full Color. Planar configuration of data. /// RgbPlanar, + + YCbCr } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs new file mode 100644 index 000000000..18a85f4e8 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + internal class YCbCrTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly CodingRangeExpander yExpander; + private readonly CodingRangeExpander cbExpander; + private readonly CodingRangeExpander crExpander; + private readonly YCbCrToRgbConverter converter; + + private static readonly Rational[] DefaultLuma = + { + new Rational(299, 1000), + new Rational(587, 1000), + new Rational(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) + }; + + public YCbCrTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) + { + referenceBlackAndWhite ??= DefaultReferenceBlackWhite; + coefficients ??= DefaultLuma; + + if (referenceBlackAndWhite.Length != 6) + { + TiffThrowHelper.ThrowImageFormatException("reference black and white array should have 6 entry's"); + } + + if (coefficients.Length != 3) + { + TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); + } + + this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255); + this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127); + this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127); + this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]); + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + Rgba32 rgba = this.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); + color.FromRgba32(rgba); + pixelRow[x] = color; + offset += 3; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Rgba32 ConvertToRgba32(byte y, byte cb, byte cr) + { + float yExpanded = this.yExpander.Expand(y); + float cbExpanded = this.cbExpander.Expand(cb); + float crExpanded = this.crExpander.Expand(cr); + + Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded); + + return rgba; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte RoundAndClampTo8Bit(float value) + { + int input = (int)MathF.Round(value); + return (byte)Math.Min(Math.Max(input, 0), 255); + } + + private readonly struct CodingRangeExpander + { + private readonly float f1; + private readonly float f2; + + public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange) + { + float black = referenceBlack.ToSingle(); + float white = referenceWhite.ToSingle(); + this.f1 = codingRange / (white - black); + this.f2 = this.f1 * black; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Expand(float code) => (code * this.f1) - this.f2; + } + + private readonly struct YCbCrToRgbConverter + { + private readonly float cr2R; + private readonly float cb2B; + private readonly float y2G; + private readonly float cr2G; + private readonly float cb2G; + + public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue) + { + this.cr2R = 2 - (2 * lumaRed.ToSingle()); + this.cb2B = 2 - (2 * lumaBlue.ToSingle()); + this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle(); + this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle(); + this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 Convert(float y, float cb, float cr) + { + var pixel = default(Rgba32); + pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); + pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); + pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); + pixel.A = byte.MaxValue; + + return pixel; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 011d03779..1693ed452 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -75,6 +75,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffColorType ColorType { get; set; } + public Rational[] ReferenceBlackAndWhite { get; set; } + + public Rational[] YcbcrCoefficients { get; set; } + /// /// Gets or sets the compression used, when the image was encoded. /// @@ -316,7 +320,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.Predictor, this.FaxCompressionOptions); - TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); + TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( + this.ColorType, + this.BitsPerSample, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.byteOrder); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 14c527a34..8ab543b50 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -69,6 +69,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb; options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24; options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0); + options.ReferenceBlackAndWhite = exifProfile.GetValue(ExifTag.ReferenceBlackWhite)?.Value; + options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value; options.ParseColorType(exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile); @@ -264,6 +266,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } + case TiffPhotometricInterpretation.YCbCr: + { + options.ColorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value; + if (options.BitsPerSample.Channels != 3) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + } + + options.ColorType = TiffColorType.YCbCr; + break; + } + default: { TiffThrowHelper.ThrowNotSupported($"The specified TIFF photometric interpretation is not supported: {options.PhotometricInterpretation}"); diff --git a/src/ImageSharp/Primitives/Rational.cs b/src/ImageSharp/Primitives/Rational.cs index b6f83e277..b14681c69 100644 --- a/src/ImageSharp/Primitives/Rational.cs +++ b/src/ImageSharp/Primitives/Rational.cs @@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp { if (simplify) { - var rational = new LongRational(numerator, denominator).Simplify(); + LongRational rational = new LongRational(numerator, denominator).Simplify(); this.Numerator = (uint)rational.Numerator; this.Denominator = (uint)rational.Denominator; @@ -93,10 +93,7 @@ namespace SixLabors.ImageSharp /// The first to compare. /// The second to compare. /// The - public static bool operator ==(Rational left, Rational right) - { - return left.Equals(right); - } + public static bool operator ==(Rational left, Rational right) => left.Equals(right); /// /// Determines whether the specified instances are not considered equal. @@ -104,10 +101,7 @@ namespace SixLabors.ImageSharp /// The first to compare. /// The second to compare. /// The - public static bool operator !=(Rational left, Rational right) - { - return !left.Equals(right); - } + public static bool operator !=(Rational left, Rational right) => !left.Equals(right); /// /// Converts the specified to an instance of this type. @@ -116,10 +110,7 @@ namespace SixLabors.ImageSharp /// /// The . /// - public static Rational FromDouble(double value) - { - return new Rational(value, false); - } + public static Rational FromDouble(double value) => new Rational(value, false); /// /// Converts the specified to an instance of this type. @@ -129,16 +120,10 @@ namespace SixLabors.ImageSharp /// /// The . /// - public static Rational FromDouble(double value, bool bestPrecision) - { - return new Rational(value, bestPrecision); - } + public static Rational FromDouble(double value, bool bestPrecision) => new Rational(value, bestPrecision); /// - public override bool Equals(object obj) - { - return obj is Rational other && this.Equals(other); - } + public override bool Equals(object obj) => obj is Rational other && this.Equals(other); /// public bool Equals(Rational other) @@ -162,16 +147,18 @@ namespace SixLabors.ImageSharp /// /// The . /// - public double ToDouble() - { - return this.Numerator / (double)this.Denominator; - } + public double ToDouble() => this.Numerator / (double)this.Denominator; + + /// + /// Converts a rational number to the nearest . + /// + /// + /// The . + /// + public float ToSingle() => this.Numerator / (float)this.Denominator; /// - public override string ToString() - { - return this.ToString(CultureInfo.InvariantCulture); - } + public override string ToString() => this.ToString(CultureInfo.InvariantCulture); /// /// Converts the numeric value of this instance to its equivalent string representation using diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 67892b14b..4b7438af4 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -153,6 +153,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_16Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(FlowerRgb888Contiguous, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_24Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(FlowerYCbCr888Contiguous, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 929e37524..ce3e7ecb3 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -569,15 +569,17 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb161616PlanarLittleEndian = "Tiff/flower-rgb-planar-16_lsb.tiff"; public const string FlowerRgb141414Contiguous = "Tiff/flower-rgb-contig-14.tiff"; public const string FlowerRgb141414Planar = "Tiff/flower-rgb-planar-14.tiff"; + public const string FlowerRgb121212Contiguous = "Tiff/flower-rgb-contig-12.tiff"; public const string FlowerRgb101010Contiguous = "Tiff/flower-rgb-contig-10.tiff"; public const string FlowerRgb101010Planar = "Tiff/flower-rgb-planar-10.tiff"; - public const string FlowerRgb121212Contiguous = "Tiff/flower-rgb-contig-12.tiff"; + public const string FlowerRgb888Contiguous = "Tiff/flower-rgb-contig-08.tiff"; + public const string FlowerRgb888Planar6Strips = "Tiff/flower-rgb-planar-08-6strips.tiff"; + public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; + public const string FlowerYCbCr888Contiguous = "Tiff/flower_ycbcr_contig-08.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"; public const string FlowerRgb222Planar = "Tiff/flower-rgb-planar-02.tiff"; - public const string FlowerRgb888Planar6Strips = "Tiff/flower-rgb-planar-08-6strips.tiff"; - public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; public const string Flower2BitGray = "Tiff/flower-minisblack-02.tiff"; public const string Flower2BitPalette = "Tiff/flower-palette-02.tiff"; public const string Flower4BitPalette = "Tiff/flower-palette-04.tiff"; diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-08.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-08.tiff new file mode 100644 index 000000000..53e890e3c --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-08.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd1333eb93d8e7ea614b755ca1c8909c67b4b44fc03a8cab6be5491bf4d15841 +size 9753 diff --git a/tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff b/tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff new file mode 100644 index 000000000..bc7f2178b --- /dev/null +++ b/tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95f7b71c3a333734f799d73076032e31a6dfff1802bb3b454ba1eada7be50b0d +size 10058 From da8f14d97f1572916c64343bea798e178e0e0fac Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 8 Aug 2021 17:29:38 +0200 Subject: [PATCH 02/11] Add support for decoding ycbcr tiff's with planar configuration --- .../TiffColorDecoderFactory{TPixel}.cs | 11 +- .../TiffColorType.cs | 10 +- .../YCbCrConverter.cs | 121 ++++++++++++++++++ .../YCbCrPlanarTiffColor{TPixel}.cs | 40 ++++++ .../YCbCrTiffColor{TPixel}.cs | 79 +----------- .../Formats/Tiff/TiffDecoderCore.cs | 19 ++- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 3 +- tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-ycbcr-planar-08.tiff | 3 + 9 files changed, 205 insertions(+), 82 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 010897c3c..d27971274 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -154,7 +154,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } - public static TiffBasePlanarColorDecoder CreatePlanar(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) + public static TiffBasePlanarColorDecoder CreatePlanar( + TiffColorType colorType, + TiffBitsPerSample bitsPerSample, + ushort[] colorMap, + Rational[] referenceBlackAndWhite, + Rational[] ycbcrCoefficients, + ByteOrder byteOrder) { switch (colorType) { @@ -167,6 +173,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new RgbPlanarTiffColor(bitsPerSample); + case TiffColorType.YCbCrPlanar: + return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 4e108f58e..51c9be83f 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -108,6 +108,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// RgbPlanar, - YCbCr + /// + /// The pixels are stored in YCbCr format. + /// + YCbCr, + + /// + /// The pixels are stored in YCbCr format as planar. + /// + YCbCrPlanar } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs new file mode 100644 index 000000000..3e28e30bc --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Converts YCbCr data to rgb data. + /// + internal class YCbCrConverter + { + private readonly CodingRangeExpander yExpander; + private readonly CodingRangeExpander cbExpander; + private readonly CodingRangeExpander crExpander; + private readonly YCbCrToRgbConverter converter; + + private static readonly Rational[] DefaultLuma = + { + new Rational(299, 1000), + new Rational(587, 1000), + new Rational(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) + }; + + public YCbCrConverter(Rational[] referenceBlackAndWhite, Rational[] coefficients) + { + referenceBlackAndWhite ??= DefaultReferenceBlackWhite; + coefficients ??= DefaultLuma; + + if (referenceBlackAndWhite.Length != 6) + { + TiffThrowHelper.ThrowImageFormatException("reference black and white array should have 6 entry's"); + } + + if (coefficients.Length != 3) + { + TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); + } + + this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255); + this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127); + this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127); + this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 ConvertToRgba32(byte y, byte cb, byte cr) + { + float yExpanded = this.yExpander.Expand(y); + float cbExpanded = this.cbExpander.Expand(cb); + float crExpanded = this.crExpander.Expand(cr); + + Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded); + + return rgba; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte RoundAndClampTo8Bit(float value) + { + int input = (int)MathF.Round(value); + return (byte)Math.Min(Math.Max(input, 0), 255); + } + + private readonly struct CodingRangeExpander + { + private readonly float f1; + private readonly float f2; + + public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange) + { + float black = referenceBlack.ToSingle(); + float white = referenceWhite.ToSingle(); + this.f1 = codingRange / (white - black); + this.f2 = this.f1 * black; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Expand(float code) => (code * this.f1) - this.f2; + } + + private readonly struct YCbCrToRgbConverter + { + private readonly float cr2R; + private readonly float cb2B; + private readonly float y2G; + private readonly float cr2G; + private readonly float cb2G; + + public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue) + { + this.cr2R = 2 - (2 * lumaRed.ToSingle()); + this.cb2B = 2 - (2 * lumaBlue.ToSingle()); + this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle(); + this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle(); + this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 Convert(float y, float cb, float cr) + { + var pixel = default(Rgba32); + pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); + pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); + pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); + pixel.A = byte.MaxValue; + + return pixel; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..126033e31 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + internal class YCbCrPlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly YCbCrConverter converter; + + public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span yData = data[0].GetSpan(); + Span cbData = data[1].GetSpan(); + Span crData = data[2].GetSpan(); + + var color = default(TPixel); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + Rgba32 rgba = this.converter.ConvertToRgba32(yData[offset], cbData[offset], crData[offset]); + color.FromRgba32(rgba); + pixelRow[x] = color; + offset++; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 18a85f4e8..55ad67a3c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -11,10 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class YCbCrTiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { - private readonly CodingRangeExpander yExpander; - private readonly CodingRangeExpander cbExpander; - private readonly CodingRangeExpander crExpander; - private readonly YCbCrToRgbConverter converter; + private readonly YCbCrConverter converter; private static readonly Rational[] DefaultLuma = { @@ -45,10 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); } - this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255); - this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127); - this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127); - this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]); + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); } /// @@ -61,78 +54,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { - Rgba32 rgba = this.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); + Rgba32 rgba = this.converter.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); color.FromRgba32(rgba); pixelRow[x] = color; offset += 3; } } } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Rgba32 ConvertToRgba32(byte y, byte cb, byte cr) - { - float yExpanded = this.yExpander.Expand(y); - float cbExpanded = this.cbExpander.Expand(cb); - float crExpanded = this.crExpander.Expand(cr); - - Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded); - - return rgba; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static byte RoundAndClampTo8Bit(float value) - { - int input = (int)MathF.Round(value); - return (byte)Math.Min(Math.Max(input, 0), 255); - } - - private readonly struct CodingRangeExpander - { - private readonly float f1; - private readonly float f2; - - public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange) - { - float black = referenceBlack.ToSingle(); - float white = referenceWhite.ToSingle(); - this.f1 = codingRange / (white - black); - this.f2 = this.f1 * black; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float Expand(float code) => (code * this.f1) - this.f2; - } - - private readonly struct YCbCrToRgbConverter - { - private readonly float cr2R; - private readonly float cb2B; - private readonly float y2G; - private readonly float cr2G; - private readonly float cb2G; - - public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue) - { - this.cr2R = 2 - (2 * lumaRed.ToSingle()); - this.cb2B = 2 - (2 * lumaBlue.ToSingle()); - this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle(); - this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle(); - this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Rgba32 Convert(float y, float cb, float cr) - { - var pixel = default(Rgba32); - pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); - pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); - pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); - pixel.A = byte.MaxValue; - - return pixel; - } - } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 1693ed452..69b6c6615 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -268,9 +268,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); } - using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions); - - TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); + using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( + this.CompressionType, + this.memoryAllocator, + this.PhotometricInterpretation, + frame.Width, + bitsPerPixel, + this.Predictor, + this.FaxCompressionOptions); + + TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( + this.ColorType, + this.BitsPerSample, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.byteOrder); for (int i = 0; i < stripsPerPlane; i++) { diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 8ab543b50..857509f87 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -274,7 +274,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); } - options.ColorType = TiffColorType.YCbCr; + options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.YCbCr : TiffColorType.YCbCrPlanar; + break; } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ce3e7ecb3..5c2d3ceb5 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -576,6 +576,7 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb888Planar6Strips = "Tiff/flower-rgb-planar-08-6strips.tiff"; public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; public const string FlowerYCbCr888Contiguous = "Tiff/flower_ycbcr_contig-08.tiff"; + public const string FlowerYCbCr888Planar = "Tiff/flower_ycbcr_planar-08.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"; diff --git a/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff b/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff new file mode 100644 index 000000000..4506ff3e9 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdc4d8033214a6737f41c4e32d9314db77b3b1ae14515496f10468047390f6c5 +size 10042 From 1ec339458958cffc226a2089e9f4ad622505d785 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 8 Aug 2021 19:30:01 +0200 Subject: [PATCH 03/11] Throw NotSupported exception when luma and chroma subsampling is not equal --- .../YCbCrTiffColor{TPixel}.cs | 32 +------------------ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 12 +++++++ .../Formats/Tiff/TiffDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 4 +-- ...ig-08.tiff => flower-ycbcr-contig-08.tiff} | 0 5 files changed, 16 insertions(+), 33 deletions(-) rename tests/Images/Input/Tiff/{flower_ycbcr_contig-08.tiff => flower-ycbcr-contig-08.tiff} (100%) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 55ad67a3c..067827212 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -12,37 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { private readonly YCbCrConverter converter; - private static readonly Rational[] DefaultLuma = - { - new Rational(299, 1000), - new Rational(587, 1000), - new Rational(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) - }; - - public YCbCrTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) - { - referenceBlackAndWhite ??= DefaultReferenceBlackWhite; - coefficients ??= DefaultLuma; - - if (referenceBlackAndWhite.Length != 6) - { - TiffThrowHelper.ThrowImageFormatException("reference black and white array should have 6 entry's"); - } - - if (coefficients.Length != 3) - { - TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); - } - - this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); - } + public YCbCrTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 857509f87..1d1473dc4 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -57,6 +57,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } + ushort[] ycbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; + if (ycbcrSubSampling != null && ycbcrSubSampling[0] != ycbcrSubSampling[1]) + { + TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images with equal luma and chroma samples."); + } + if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null) { TiffThrowHelper.ThrowNotSupported("Variable-sized strips are not supported."); @@ -274,6 +280,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); } + ushort bitsPerChannel = options.BitsPerSample.Channel0; + if (bitsPerChannel != 8) + { + TiffThrowHelper.ThrowNotSupported("Only 8 bits per channel is supported for YCbCr images."); + } + options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.YCbCr : TiffColorType.YCbCrPlanar; break; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 4b7438af4..b2e2e2535 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -160,6 +160,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerYCbCr888Contiguous, PixelTypes.Rgba32)] + [WithFile(FlowerYCbCr888Planar, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 5c2d3ceb5..a79ff9da8 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -575,8 +575,8 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb888Contiguous = "Tiff/flower-rgb-contig-08.tiff"; public const string FlowerRgb888Planar6Strips = "Tiff/flower-rgb-planar-08-6strips.tiff"; public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; - public const string FlowerYCbCr888Contiguous = "Tiff/flower_ycbcr_contig-08.tiff"; - public const string FlowerYCbCr888Planar = "Tiff/flower_ycbcr_planar-08.tiff"; + public const string FlowerYCbCr888Contiguous = "Tiff/flower-ycbcr-contig-08.tiff"; + public const string FlowerYCbCr888Planar = "Tiff/flower-ycbcr-planar-08.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"; diff --git a/tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08.tiff similarity index 100% rename from tests/Images/Input/Tiff/flower_ycbcr_contig-08.tiff rename to tests/Images/Input/Tiff/flower-ycbcr-contig-08.tiff From 1cbab324681f3b5c6ed1085fd45a75c0dd5ae6ae Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 8 Aug 2021 20:25:54 +0200 Subject: [PATCH 04/11] Throw not suported for YCbCr with subsampling --- src/ImageSharp/Formats/Tiff/README.md | 8 ++++---- src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 5b116b819..3e4afe92e 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -63,7 +63,7 @@ |PaletteColor | Y | Y | General implementation only | |TransparencyMask | | | | |Separated (TIFF Extension) | | | | -|YCbCr (TIFF Extension) | | | | +|YCbCr (TIFF Extension) | | Y | | |CieLab (TIFF Extension) | | | | |IccLab (TechNote 1) | | | | @@ -165,10 +165,10 @@ |JPEGQTables | | | | |JPEGDCTables | | | | |JPEGACTables | | | | -|YCbCrCoefficients | | | | -|YCbCrSubSampling | | | | +|YCbCrCoefficients | | Y | | +|YCbCrSubSampling | | Y | | |YCbCrPositioning | | | | -|ReferenceBlackWhite | | | | +|ReferenceBlackWhite | | Y | | |StripRowCounts | - | - | See RFC 2301 (File Format for Internet Fax). | |XMP | Y | Y | | |ImageID | | | | diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 1d1473dc4..86dc3c1df 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -63,6 +63,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images with equal luma and chroma samples."); } + if (ycbcrSubSampling != null && ycbcrSubSampling[0] != 1) + { + TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images without subsampling."); + } + if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null) { TiffThrowHelper.ThrowNotSupported("Variable-sized strips are not supported."); From 617a66d120c9d4e74611b7c05b534738fa210def Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 11:32:10 +0200 Subject: [PATCH 05/11] Reverse chroma sub sampling --- .../TiffColorDecoderFactory{TPixel}.cs | 8 ++- .../YCbCrPlanarTiffColor{TPixel}.cs | 27 +++++++++- .../YCbCrTiffColor{TPixel}.cs | 53 ++++++++++++++++++- .../Formats/Tiff/TiffDecoderCore.cs | 14 +++++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 9 ++-- .../Formats/Tiff/TiffDecoderTests.cs | 12 ++++- tests/ImageSharp.Tests/TestImages.cs | 4 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff | 3 ++ 12 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index d27971274..13f95fedf 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation @@ -9,11 +10,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation where TPixel : unmanaged, IPixel { public static TiffBaseColorDecoder Create( + MemoryAllocator memoryAllocator, TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, Rational[] referenceBlackAndWhite, Rational[] ycbcrCoefficients, + ushort[] ycbcrSubSampling, ByteOrder byteOrder) { switch (colorType) @@ -147,7 +150,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new PaletteTiffColor(bitsPerSample, colorMap); case TiffColorType.YCbCr: - return new YCbCrTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + return new YCbCrTiffColor(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); @@ -160,6 +163,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ushort[] colorMap, Rational[] referenceBlackAndWhite, Rational[] ycbcrCoefficients, + ushort[] ycbcrSubSampling, ByteOrder byteOrder) { switch (colorType) @@ -174,7 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new RgbPlanarTiffColor(bitsPerSample); case TiffColorType.YCbCrPlanar: - return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs index 126033e31..0e411c69d 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -13,7 +13,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { private readonly YCbCrConverter converter; - public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + private readonly ushort[] ycbcrSubSampling; + + public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling) + { + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + this.ycbcrSubSampling = ycbcrSubSampling; + } /// public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) @@ -22,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span cbData = data[1].GetSpan(); Span crData = data[2].GetSpan(); + if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) + { + ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], cbData, crData); + } + var color = default(TPixel); int offset = 0; for (int y = top; y < top + height; y++) @@ -36,5 +47,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } } + + private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, Span planarCb, Span planarCr) + { + for (int row = height - 1; row >= 0; row--) + { + for (int col = width - 1; col >= 0; col--) + { + int offset = (row * width) + col; + int subSampleOffset = (row / verticalSubSampling * (width / horizontalSubSampling)) + (col / horizontalSubSampling); + planarCb[offset] = planarCb[subSampleOffset]; + planarCr[offset] = planarCr[subSampleOffset]; + } + } + } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 067827212..7a7e1ad87 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -10,13 +11,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class YCbCrTiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { + private readonly MemoryAllocator memoryAllocator; + private readonly YCbCrConverter converter; - public YCbCrTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + private readonly ushort[] ycbcrSubSampling; + + public YCbCrTiffColor(MemoryAllocator memoryAllocator, Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling) + { + this.memoryAllocator = memoryAllocator; + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + this.ycbcrSubSampling = ycbcrSubSampling; + } /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { + ReadOnlySpan ycbcrData = data; + if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) + { + using IMemoryOwner tmpBuffer = this.memoryAllocator.Allocate(data.Length); + Span tmpBufferSpan = tmpBuffer.GetSpan(); + ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], data, tmpBufferSpan); + ycbcrData = tmpBufferSpan; + } + var color = default(TPixel); int offset = 0; for (int y = top; y < top + height; y++) @@ -24,12 +43,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { - Rgba32 rgba = this.converter.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); + Rgba32 rgba = this.converter.ConvertToRgba32(ycbcrData[offset], ycbcrData[offset + 1], ycbcrData[offset + 2]); color.FromRgba32(rgba); pixelRow[x] = color; offset += 3; } } } + + private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, ReadOnlySpan source, Span destination) + { + int blockWidth = width / horizontalSubSampling; + int blockHeight = height / verticalSubSampling; + int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; + int blockByteCount = cbCrOffsetInBlock + 2; + + for (int blockRow = blockHeight - 1; blockRow >= 0; blockRow--) + { + for (int blockCol = blockWidth - 1; blockCol >= 0; blockCol--) + { + int blockOffset = (blockRow * blockWidth) + blockCol; + ReadOnlySpan blockData = source.Slice(blockOffset * blockByteCount, blockByteCount); + byte cr = blockData[cbCrOffsetInBlock + 1]; + byte cb = blockData[cbCrOffsetInBlock]; + + for (int row = verticalSubSampling - 1; row >= 0; row--) + { + for (int col = horizontalSubSampling - 1; col >= 0; col--) + { + int offset = 3 * ((((blockRow * verticalSubSampling) + row) * width) + (blockCol * horizontalSubSampling) + col); + destination[offset + 2] = cr; + destination[offset + 1] = cb; + destination[offset] = blockData[(row * horizontalSubSampling) + col]; + } + } + } + } + } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 69b6c6615..080cdb22a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -75,10 +75,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffColorType ColorType { get; set; } + /// + /// Gets or sets the reference black and white for decoding YCbCr pixel data. + /// public Rational[] ReferenceBlackAndWhite { get; set; } + /// + /// Gets or sets the YCbCr coefficients. + /// public Rational[] YcbcrCoefficients { get; set; } + /// + /// Gets or sets the YCbCr sub sampling. + /// + public ushort[] YcbcrSubSampling { get; set; } + /// /// Gets or sets the compression used, when the image was encoded. /// @@ -283,6 +294,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.ColorMap, this.ReferenceBlackAndWhite, this.YcbcrCoefficients, + this.YcbcrSubSampling, this.byteOrder); for (int i = 0; i < stripsPerPlane; i++) @@ -334,11 +346,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.FaxCompressionOptions); TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( + this.memoryAllocator, this.ColorType, this.BitsPerSample, this.ColorMap, this.ReferenceBlackAndWhite, this.YcbcrCoefficients, + this.YcbcrSubSampling, this.byteOrder); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 86dc3c1df..15764cffa 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -58,14 +58,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff } ushort[] ycbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; - if (ycbcrSubSampling != null && ycbcrSubSampling[0] != ycbcrSubSampling[1]) + if (ycbcrSubSampling != null && ycbcrSubSampling.Length != 2) { - TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images with equal luma and chroma samples."); + TiffThrowHelper.ThrowImageFormatException("Invalid YCbCrSubsampling, expected 2 values."); } - if (ycbcrSubSampling != null && ycbcrSubSampling[0] != 1) + if (ycbcrSubSampling != null && ycbcrSubSampling[1] > ycbcrSubSampling[0]) { - TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images without subsampling."); + TiffThrowHelper.ThrowImageFormatException("ChromaSubsampleVert shall always be less than or equal to ChromaSubsampleHoriz."); } if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null) @@ -82,6 +82,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0); options.ReferenceBlackAndWhite = exifProfile.GetValue(ExifTag.ReferenceBlackWhite)?.Value; options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value; + options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; options.ParseColorType(exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index b2e2e2535..18007d1c4 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -161,8 +161,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [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)] public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + where TPixel : unmanaged, IPixel + { + // Note: The image from MagickReferenceDecoder does not look right, maybe we are doing something wrong + // converting the pixel data from Magick.Net to our format with YCbCr? + using Image image = provider.GetImage(); + image.DebugSave(provider); + } [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a79ff9da8..3ad30b545 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -577,6 +577,10 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; public const string FlowerYCbCr888Contiguous = "Tiff/flower-ycbcr-contig-08.tiff"; public const string FlowerYCbCr888Planar = "Tiff/flower-ycbcr-planar-08.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 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"; diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff new file mode 100644 index 000000000..801cab574 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:389ee18596cd3d9f1f7f04b4db8fd21edce2900837c17ebb57cc4b64a6820f3e +size 120354 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff new file mode 100644 index 000000000..82350e5b2 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95b1ba4ff48ea2263041eca4ada44d009277297bb3b3a185d48580bdf3f7caaf +size 81382 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff new file mode 100644 index 000000000..b282b1742 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbd835c2406700523b239b80299b2b02c36d41182ac338f7ed7164979a787c60 +size 63438 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff new file mode 100644 index 000000000..0c8c048dc --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:399d5bc062baa00c2054a138489709379032f8683fbcb292bb2125b62e715b5f +size 50336 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff new file mode 100644 index 000000000..45341ed26 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58c4914b32b27df1ef303bb127fe9211c2aeda23e17bb5f4b349543c96d845b7 +size 45152 From 23c692656584b0481563250cddea33710245eb0f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 13:20:05 +0200 Subject: [PATCH 06/11] Add padding when width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert --- .../YCbCrTiffColor{TPixel}.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 7a7e1ad87..b6944168d 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -30,7 +30,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ReadOnlySpan ycbcrData = data; if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) { - using IMemoryOwner tmpBuffer = this.memoryAllocator.Allocate(data.Length); + // 4 extra rows and columns for possible padding. + int paddedWidth = width + 4; + int paddedHeight = height + 4; + int requiredBytes = paddedWidth * paddedHeight * 3; + using IMemoryOwner tmpBuffer = this.memoryAllocator.Allocate(requiredBytes); Span tmpBufferSpan = tmpBuffer.GetSpan(); ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], data, tmpBufferSpan); ycbcrData = tmpBufferSpan; @@ -53,6 +57,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, ReadOnlySpan source, Span destination) { + // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, + // then the source data will be padded. + width += width % horizontalSubSampling; + height += height % verticalSubSampling; int blockWidth = width / horizontalSubSampling; int blockHeight = height / verticalSubSampling; int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; From 5a2a28bfb87ede49fdf91555d45189aeb6d9a85d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 14:41:43 +0200 Subject: [PATCH 07/11] Add padding to width to next integer multiple of horizontalSubSampling --- .../YCbCrTiffColor{TPixel}.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index b6944168d..f28a2399e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -42,6 +42,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var color = default(TPixel); int offset = 0; + int widthPadding = 0; + if (this.ycbcrSubSampling != null) + { + // Round to the next integer multiple of horizontalSubSampling. + widthPadding = PaddingToNextInteger(width, this.ycbcrSubSampling[0]); + } + for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); @@ -52,6 +59,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation pixelRow[x] = color; offset += 3; } + + offset += widthPadding * 3; } } @@ -59,8 +68,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, // then the source data will be padded. - width += width % horizontalSubSampling; - height += height % verticalSubSampling; + width += PaddingToNextInteger(width, horizontalSubSampling); + height += PaddingToNextInteger(height, verticalSubSampling); int blockWidth = width / horizontalSubSampling; int blockHeight = height / verticalSubSampling; int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; @@ -88,5 +97,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } } + + private static int PaddingToNextInteger(int valueToRoundUp, int subSampling) + { + if (valueToRoundUp % subSampling == 0) + { + return 0; + } + + int padding = subSampling - (valueToRoundUp % subSampling); + return padding; + } } } From b576f15b13fe1c57f5a7aed1f1e45025cc4f8b1b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 15:02:30 +0200 Subject: [PATCH 08/11] Additional test images --- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 3 +++ tests/ImageSharp.Tests/TestImages.cs | 7 +++++-- ...bcr-contig-08.tiff => flower-ycbcr-contig-08_h1v1.tiff} | 0 tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff | 3 +++ tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff | 3 +++ tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff | 3 +++ ...bcr-planar-08.tiff => flower-ycbcr-planar-08_h1v1.tiff} | 0 7 files changed, 17 insertions(+), 2 deletions(-) rename tests/Images/Input/Tiff/{flower-ycbcr-contig-08.tiff => flower-ycbcr-contig-08_h1v1.tiff} (100%) create mode 100644 tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff create mode 100644 tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff create mode 100644 tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff rename tests/Images/Input/Tiff/{flower-ycbcr-planar-08.tiff => flower-ycbcr-planar-08_h1v1.tiff} (100%) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index d0fed1e20..b52c02d26 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -179,6 +179,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [WithFile(RgbYCbCr888Contiguoush2v1, PixelTypes.Rgba32)] [WithFile(RgbYCbCr888Contiguoush2v2, PixelTypes.Rgba32)] [WithFile(RgbYCbCr888Contiguoush4v4, PixelTypes.Rgba32)] + [WithFile(FlowerYCbCr888Contiguoush2v1, PixelTypes.Rgba32)] + [WithFile(FlowerYCbCr888Contiguoush2v2, PixelTypes.Rgba32)] + [WithFile(FlowerYCbCr888Contiguoush4v4, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 65b9bfe44..db258683c 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -585,8 +585,11 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb888Contiguous = "Tiff/flower-rgb-contig-08.tiff"; public const string FlowerRgb888Planar6Strips = "Tiff/flower-rgb-planar-08-6strips.tiff"; public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; - public const string FlowerYCbCr888Contiguous = "Tiff/flower-ycbcr-contig-08.tiff"; - public const string FlowerYCbCr888Planar = "Tiff/flower-ycbcr-planar-08.tiff"; + public const string FlowerYCbCr888Contiguous = "Tiff/flower-ycbcr-contig-08_h1v1.tiff"; + public const string FlowerYCbCr888Planar = "Tiff/flower-ycbcr-planar-08_h1v1.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"; diff --git a/tests/Images/Input/Tiff/flower-ycbcr-contig-08.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h1v1.tiff similarity index 100% rename from tests/Images/Input/Tiff/flower-ycbcr-contig-08.tiff rename to tests/Images/Input/Tiff/flower-ycbcr-contig-08_h1v1.tiff diff --git a/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff new file mode 100644 index 000000000..f5133b9f3 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:270e0331818a755f5fac600172eacbcbebda86f93f521bfc8d75f4b8bc530177 +size 6944 diff --git a/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff new file mode 100644 index 000000000..98fb13d66 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h2v2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ef6ebc9dfe72fbe6ed65ebfc2465ebb18f326119a640faf3301aa4cfa31990f +size 5464 diff --git a/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff new file mode 100644 index 000000000..79aace2a3 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h4v4.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5ea966cc7b823a5d228b49cdc55a261353f73b1eb94a218f1c68321d757e25f +size 4342 diff --git a/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff b/tests/Images/Input/Tiff/flower-ycbcr-planar-08_h1v1.tiff similarity index 100% rename from tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff rename to tests/Images/Input/Tiff/flower-ycbcr-planar-08_h1v1.tiff From 2789f6a6a7a430d7cbae94c5e3f9ca7d822ed2cb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 16:42:51 +0200 Subject: [PATCH 09/11] Add width padding for planar ycbcr --- .../YCbCrPlanarTiffColor{TPixel}.cs | 15 +++++++++++++++ .../YCbCrTiffColor{TPixel}.cs | 18 ++++-------------- src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs index 0e411c69d..70578a744 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -35,6 +36,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var color = default(TPixel); int offset = 0; + int widthPadding = 0; + if (this.ycbcrSubSampling != null) + { + // Round to the next integer multiple of horizontalSubSampling. + widthPadding = TiffUtils.PaddingToNextInteger(width, this.ycbcrSubSampling[0]); + } + for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); @@ -45,11 +53,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation pixelRow[x] = color; offset++; } + + offset += widthPadding; } } private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, Span planarCb, Span planarCr) { + // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, + // then the source data will be padded. + width += TiffUtils.PaddingToNextInteger(width, horizontalSubSampling); + height += TiffUtils.PaddingToNextInteger(height, verticalSubSampling); + for (int row = height - 1; row >= 0; row--) { for (int col = width - 1; col >= 0; col--) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index f28a2399e..e31b4984d 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation if (this.ycbcrSubSampling != null) { // Round to the next integer multiple of horizontalSubSampling. - widthPadding = PaddingToNextInteger(width, this.ycbcrSubSampling[0]); + widthPadding = TiffUtils.PaddingToNextInteger(width, this.ycbcrSubSampling[0]); } for (int y = top; y < top + height; y++) @@ -68,8 +69,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { // If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively, // then the source data will be padded. - width += PaddingToNextInteger(width, horizontalSubSampling); - height += PaddingToNextInteger(height, verticalSubSampling); + width += TiffUtils.PaddingToNextInteger(width, horizontalSubSampling); + height += TiffUtils.PaddingToNextInteger(height, verticalSubSampling); int blockWidth = width / horizontalSubSampling; int blockHeight = height / verticalSubSampling; int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; @@ -97,16 +98,5 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } } - - private static int PaddingToNextInteger(int valueToRoundUp, int subSampling) - { - if (valueToRoundUp % subSampling == 0) - { - return 0; - } - - int padding = subSampling - (valueToRoundUp % subSampling); - return padding; - } } } diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index f2872858c..4f71fa35c 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -98,5 +98,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils color.FromVector4(colorVector); return color; } + + /// + /// Finds the padding needed to round 'valueToRoundUp' to the next integer multiple of subSampling value. + /// + /// The width or height to round up. + /// The sub sampling. + /// The padding. + public static int PaddingToNextInteger(int valueToRoundUp, int subSampling) + { + if (valueToRoundUp % subSampling == 0) + { + return 0; + } + + int padding = subSampling - (valueToRoundUp % subSampling); + return padding; + } } } From 59b470f4daf85e1554eaf1ffb2faea7b7b59811a Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Mon, 9 Aug 2021 17:05:01 +0200 Subject: [PATCH 10/11] Use Math.Clamp(input, 0, 255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- .../Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs index 3e28e30bc..aa64a6e32 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation private static byte RoundAndClampTo8Bit(float value) { int input = (int)MathF.Round(value); - return (byte)Math.Min(Math.Max(input, 0), 255); + return (byte)Math.Clamp(input, 0, 255); } private readonly struct CodingRangeExpander From d6bbdadeaef22c5a8dcf1be66679e91f177d658d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 17:12:25 +0200 Subject: [PATCH 11/11] Use Numerics.Clamp instead Math.Clamp (not available with net472) --- .../Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs index aa64a6e32..c6594f908 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation private static byte RoundAndClampTo8Bit(float value) { int input = (int)MathF.Round(value); - return (byte)Math.Clamp(input, 0, 255); + return (byte)Numerics.Clamp(input, 0, 255); } private readonly struct CodingRangeExpander