diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index e27c0a61c..a8c4cef4e 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 @@ -8,7 +9,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal static class TiffColorDecoderFactory where TPixel : unmanaged, IPixel { - public static TiffBaseColorDecoder Create(Configuration configuration, TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) + public static TiffBaseColorDecoder Create( + Configuration configuration, + MemoryAllocator memoryAllocator, + TiffColorType colorType, + TiffBitsPerSample bitsPerSample, + ushort[] colorMap, + Rational[] referenceBlackAndWhite, + Rational[] ycbcrCoefficients, + ushort[] ycbcrSubSampling, + ByteOrder byteOrder) { switch (colorType) { @@ -200,12 +210,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.NotNull(colorMap, "colorMap"); return new PaletteTiffColor(bitsPerSample, colorMap); + case TiffColorType.YCbCr: + return new YCbCrTiffColor(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); + default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); } } - 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, + ushort[] ycbcrSubSampling, + ByteOrder byteOrder) { switch (colorType) { @@ -213,6 +233,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbPlanarTiffColor(bitsPerSample); + case TiffColorType.YCbCrPlanar: + return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); + case TiffColorType.Rgb161616Planar: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 81db744a1..8caefaed5 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -167,5 +167,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// RGB Full Color. Planar configuration of data. 32 Bit per color channel. /// Rgb323232Planar, + + /// + /// 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..c6594f908 --- /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)Numerics.Clamp(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..70578a744 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +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; + + 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) + { + Span yData = data[0].GetSpan(); + 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; + 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); + 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++; + } + + 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--) + { + 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 new file mode 100644 index 000000000..e31b4984d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + internal class YCbCrTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly MemoryAllocator memoryAllocator; + + private readonly YCbCrConverter converter; + + 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)) + { + // 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; + } + + 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); + for (int x = 0; x < pixelRow.Length; x++) + { + Rgba32 rgba = this.converter.ConvertToRgba32(ycbcrData[offset], ycbcrData[offset + 1], ycbcrData[offset + 2]); + color.FromRgba32(rgba); + pixelRow[x] = color; + offset += 3; + } + + offset += widthPadding * 3; + } + } + + 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 += TiffUtils.PaddingToNextInteger(width, horizontalSubSampling); + height += TiffUtils.PaddingToNextInteger(height, verticalSubSampling); + 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/README.md b/src/ImageSharp/Formats/Tiff/README.md index 7b7c0efd5..ab3394c56 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/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index ce8e58798..28afe4c6f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -75,6 +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. /// @@ -286,7 +301,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.FillOrder, this.byteOrder); - TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); + TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( + this.ColorType, + this.BitsPerSample, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.YcbcrSubSampling, + this.byteOrder); for (int i = 0; i < stripsPerPlane; i++) { @@ -339,7 +361,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.FillOrder, this.byteOrder); - TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.Configuration, this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); + TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( + this.Configuration, + 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 a493c4467..bb435affc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -59,6 +59,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } + ushort[] ycbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; + if (ycbcrSubSampling != null && ycbcrSubSampling.Length != 2) + { + TiffThrowHelper.ThrowImageFormatException("Invalid YCbCrSubsampling, expected 2 values."); + } + + if (ycbcrSubSampling != null && ycbcrSubSampling[1] > ycbcrSubSampling[0]) + { + TiffThrowHelper.ThrowImageFormatException("ChromaSubsampleVert shall always be less than or equal to ChromaSubsampleHoriz."); + } + if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null) { TiffThrowHelper.ThrowNotSupported("Variable-sized strips are not supported."); @@ -72,6 +83,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.SampleFormat = sampleFormat ?? TiffSampleFormat.UnsignedInteger; 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.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; options.FillOrder = fillOrder; options.ParseColorType(exifProfile); @@ -339,6 +353,25 @@ 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."); + } + + 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; + } + default: { TiffThrowHelper.ThrowNotSupported($"The specified TIFF photometric interpretation is not supported: {options.PhotometricInterpretation}"); 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; + } } } 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 176f8c1ae..5a0495e0a 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -178,6 +178,25 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSave(provider); } + [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)] + [WithFile(FlowerYCbCr888Contiguoush2v2, PixelTypes.Rgba32)] + [WithFile(FlowerYCbCr888Contiguoush4v4, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider 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)] [WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a41150a61..0e892baec 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -585,15 +585,25 @@ 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_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"; + 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"; 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_h1v1.tiff b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h1v1.tiff new file mode 100644 index 000000000..bc7f2178b --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-contig-08_h1v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95f7b71c3a333734f799d73076032e31a6dfff1802bb3b454ba1eada7be50b0d +size 10058 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_h1v1.tiff b/tests/Images/Input/Tiff/flower-ycbcr-planar-08_h1v1.tiff new file mode 100644 index 000000000..4506ff3e9 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-planar-08_h1v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdc4d8033214a6737f41c4e32d9314db77b3b1ae14515496f10468047390f6c5 +size 10042 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