Skip to content

Commit 1c183f1

Browse files
authored
Merge branch 'master' into bp/deduceColorSpaceFix
2 parents 51e519e + d105ab4 commit 1c183f1

23 files changed

+506
-40
lines changed

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

4+
using SixLabors.ImageSharp.Memory;
45
using SixLabors.ImageSharp.PixelFormats;
56

67
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
78
{
89
internal static class TiffColorDecoderFactory<TPixel>
910
where TPixel : unmanaged, IPixel<TPixel>
1011
{
11-
public static TiffBaseColorDecoder<TPixel> Create(Configuration configuration, TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder)
12+
public static TiffBaseColorDecoder<TPixel> Create(
13+
Configuration configuration,
14+
MemoryAllocator memoryAllocator,
15+
TiffColorType colorType,
16+
TiffBitsPerSample bitsPerSample,
17+
ushort[] colorMap,
18+
Rational[] referenceBlackAndWhite,
19+
Rational[] ycbcrCoefficients,
20+
ushort[] ycbcrSubSampling,
21+
ByteOrder byteOrder)
1222
{
1323
switch (colorType)
1424
{
@@ -200,19 +210,32 @@ public static TiffBaseColorDecoder<TPixel> Create(Configuration configuration, T
200210
DebugGuard.NotNull(colorMap, "colorMap");
201211
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap);
202212

213+
case TiffColorType.YCbCr:
214+
return new YCbCrTiffColor<TPixel>(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
215+
203216
default:
204217
throw TiffThrowHelper.InvalidColorType(colorType.ToString());
205218
}
206219
}
207220

208-
public static TiffBasePlanarColorDecoder<TPixel> CreatePlanar(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder)
221+
public static TiffBasePlanarColorDecoder<TPixel> CreatePlanar(
222+
TiffColorType colorType,
223+
TiffBitsPerSample bitsPerSample,
224+
ushort[] colorMap,
225+
Rational[] referenceBlackAndWhite,
226+
Rational[] ycbcrCoefficients,
227+
ushort[] ycbcrSubSampling,
228+
ByteOrder byteOrder)
209229
{
210230
switch (colorType)
211231
{
212232
case TiffColorType.Rgb888Planar:
213233
DebugGuard.IsTrue(colorMap == null, "colorMap");
214234
return new RgbPlanarTiffColor<TPixel>(bitsPerSample);
215235

236+
case TiffColorType.YCbCrPlanar:
237+
return new YCbCrPlanarTiffColor<TPixel>(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
238+
216239
case TiffColorType.Rgb161616Planar:
217240
DebugGuard.IsTrue(colorMap == null, "colorMap");
218241
return new Rgb16PlanarTiffColor<TPixel>(byteOrder == ByteOrder.BigEndian);

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,5 +167,15 @@ internal enum TiffColorType
167167
/// RGB Full Color. Planar configuration of data. 32 Bit per color channel.
168168
/// </summary>
169169
Rgb323232Planar,
170+
171+
/// <summary>
172+
/// The pixels are stored in YCbCr format.
173+
/// </summary>
174+
YCbCr,
175+
176+
/// <summary>
177+
/// The pixels are stored in YCbCr format as planar.
178+
/// </summary>
179+
YCbCrPlanar
170180
}
171181
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Runtime.CompilerServices;
6+
using SixLabors.ImageSharp.PixelFormats;
7+
8+
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
9+
{
10+
/// <summary>
11+
/// Converts YCbCr data to rgb data.
12+
/// </summary>
13+
internal class YCbCrConverter
14+
{
15+
private readonly CodingRangeExpander yExpander;
16+
private readonly CodingRangeExpander cbExpander;
17+
private readonly CodingRangeExpander crExpander;
18+
private readonly YCbCrToRgbConverter converter;
19+
20+
private static readonly Rational[] DefaultLuma =
21+
{
22+
new Rational(299, 1000),
23+
new Rational(587, 1000),
24+
new Rational(114, 1000)
25+
};
26+
27+
private static readonly Rational[] DefaultReferenceBlackWhite =
28+
{
29+
new Rational(0, 1), new Rational(255, 1),
30+
new Rational(128, 1), new Rational(255, 1),
31+
new Rational(128, 1), new Rational(255, 1)
32+
};
33+
34+
public YCbCrConverter(Rational[] referenceBlackAndWhite, Rational[] coefficients)
35+
{
36+
referenceBlackAndWhite ??= DefaultReferenceBlackWhite;
37+
coefficients ??= DefaultLuma;
38+
39+
if (referenceBlackAndWhite.Length != 6)
40+
{
41+
TiffThrowHelper.ThrowImageFormatException("reference black and white array should have 6 entry's");
42+
}
43+
44+
if (coefficients.Length != 3)
45+
{
46+
TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's");
47+
}
48+
49+
this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255);
50+
this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127);
51+
this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127);
52+
this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]);
53+
}
54+
55+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
56+
public Rgba32 ConvertToRgba32(byte y, byte cb, byte cr)
57+
{
58+
float yExpanded = this.yExpander.Expand(y);
59+
float cbExpanded = this.cbExpander.Expand(cb);
60+
float crExpanded = this.crExpander.Expand(cr);
61+
62+
Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded);
63+
64+
return rgba;
65+
}
66+
67+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
68+
private static byte RoundAndClampTo8Bit(float value)
69+
{
70+
int input = (int)MathF.Round(value);
71+
return (byte)Numerics.Clamp(input, 0, 255);
72+
}
73+
74+
private readonly struct CodingRangeExpander
75+
{
76+
private readonly float f1;
77+
private readonly float f2;
78+
79+
public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange)
80+
{
81+
float black = referenceBlack.ToSingle();
82+
float white = referenceWhite.ToSingle();
83+
this.f1 = codingRange / (white - black);
84+
this.f2 = this.f1 * black;
85+
}
86+
87+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
88+
public float Expand(float code) => (code * this.f1) - this.f2;
89+
}
90+
91+
private readonly struct YCbCrToRgbConverter
92+
{
93+
private readonly float cr2R;
94+
private readonly float cb2B;
95+
private readonly float y2G;
96+
private readonly float cr2G;
97+
private readonly float cb2G;
98+
99+
public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue)
100+
{
101+
this.cr2R = 2 - (2 * lumaRed.ToSingle());
102+
this.cb2B = 2 - (2 * lumaBlue.ToSingle());
103+
this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle();
104+
this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle();
105+
this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle();
106+
}
107+
108+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
109+
public Rgba32 Convert(float y, float cb, float cr)
110+
{
111+
var pixel = default(Rgba32);
112+
pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y);
113+
pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb));
114+
pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y);
115+
pixel.A = byte.MaxValue;
116+
117+
return pixel;
118+
}
119+
}
120+
}
121+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Buffers;
6+
using SixLabors.ImageSharp.Formats.Tiff.Utils;
7+
using SixLabors.ImageSharp.Memory;
8+
using SixLabors.ImageSharp.PixelFormats;
9+
10+
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
11+
{
12+
internal class YCbCrPlanarTiffColor<TPixel> : TiffBasePlanarColorDecoder<TPixel>
13+
where TPixel : unmanaged, IPixel<TPixel>
14+
{
15+
private readonly YCbCrConverter converter;
16+
17+
private readonly ushort[] ycbcrSubSampling;
18+
19+
public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling)
20+
{
21+
this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients);
22+
this.ycbcrSubSampling = ycbcrSubSampling;
23+
}
24+
25+
/// <inheritdoc/>
26+
public override void Decode(IMemoryOwner<byte>[] data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
27+
{
28+
Span<byte> yData = data[0].GetSpan();
29+
Span<byte> cbData = data[1].GetSpan();
30+
Span<byte> crData = data[2].GetSpan();
31+
32+
if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1))
33+
{
34+
ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], cbData, crData);
35+
}
36+
37+
var color = default(TPixel);
38+
int offset = 0;
39+
int widthPadding = 0;
40+
if (this.ycbcrSubSampling != null)
41+
{
42+
// Round to the next integer multiple of horizontalSubSampling.
43+
widthPadding = TiffUtils.PaddingToNextInteger(width, this.ycbcrSubSampling[0]);
44+
}
45+
46+
for (int y = top; y < top + height; y++)
47+
{
48+
Span<TPixel> pixelRow = pixels.GetRowSpan(y).Slice(left, width);
49+
for (int x = 0; x < pixelRow.Length; x++)
50+
{
51+
Rgba32 rgba = this.converter.ConvertToRgba32(yData[offset], cbData[offset], crData[offset]);
52+
color.FromRgba32(rgba);
53+
pixelRow[x] = color;
54+
offset++;
55+
}
56+
57+
offset += widthPadding;
58+
}
59+
}
60+
61+
private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, Span<byte> planarCb, Span<byte> planarCr)
62+
{
63+
// If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively,
64+
// then the source data will be padded.
65+
width += TiffUtils.PaddingToNextInteger(width, horizontalSubSampling);
66+
height += TiffUtils.PaddingToNextInteger(height, verticalSubSampling);
67+
68+
for (int row = height - 1; row >= 0; row--)
69+
{
70+
for (int col = width - 1; col >= 0; col--)
71+
{
72+
int offset = (row * width) + col;
73+
int subSampleOffset = (row / verticalSubSampling * (width / horizontalSubSampling)) + (col / horizontalSubSampling);
74+
planarCb[offset] = planarCb[subSampleOffset];
75+
planarCr[offset] = planarCr[subSampleOffset];
76+
}
77+
}
78+
}
79+
}
80+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Buffers;
6+
using SixLabors.ImageSharp.Formats.Tiff.Utils;
7+
using SixLabors.ImageSharp.Memory;
8+
using SixLabors.ImageSharp.PixelFormats;
9+
10+
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
11+
{
12+
internal class YCbCrTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
13+
where TPixel : unmanaged, IPixel<TPixel>
14+
{
15+
private readonly MemoryAllocator memoryAllocator;
16+
17+
private readonly YCbCrConverter converter;
18+
19+
private readonly ushort[] ycbcrSubSampling;
20+
21+
public YCbCrTiffColor(MemoryAllocator memoryAllocator, Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling)
22+
{
23+
this.memoryAllocator = memoryAllocator;
24+
this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients);
25+
this.ycbcrSubSampling = ycbcrSubSampling;
26+
}
27+
28+
/// <inheritdoc/>
29+
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
30+
{
31+
ReadOnlySpan<byte> ycbcrData = data;
32+
if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1))
33+
{
34+
// 4 extra rows and columns for possible padding.
35+
int paddedWidth = width + 4;
36+
int paddedHeight = height + 4;
37+
int requiredBytes = paddedWidth * paddedHeight * 3;
38+
using IMemoryOwner<byte> tmpBuffer = this.memoryAllocator.Allocate<byte>(requiredBytes);
39+
Span<byte> tmpBufferSpan = tmpBuffer.GetSpan();
40+
ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], data, tmpBufferSpan);
41+
ycbcrData = tmpBufferSpan;
42+
}
43+
44+
var color = default(TPixel);
45+
int offset = 0;
46+
int widthPadding = 0;
47+
if (this.ycbcrSubSampling != null)
48+
{
49+
// Round to the next integer multiple of horizontalSubSampling.
50+
widthPadding = TiffUtils.PaddingToNextInteger(width, this.ycbcrSubSampling[0]);
51+
}
52+
53+
for (int y = top; y < top + height; y++)
54+
{
55+
Span<TPixel> pixelRow = pixels.GetRowSpan(y).Slice(left, width);
56+
for (int x = 0; x < pixelRow.Length; x++)
57+
{
58+
Rgba32 rgba = this.converter.ConvertToRgba32(ycbcrData[offset], ycbcrData[offset + 1], ycbcrData[offset + 2]);
59+
color.FromRgba32(rgba);
60+
pixelRow[x] = color;
61+
offset += 3;
62+
}
63+
64+
offset += widthPadding * 3;
65+
}
66+
}
67+
68+
private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, ReadOnlySpan<byte> source, Span<byte> destination)
69+
{
70+
// If width and height are not multiples of ChromaSubsampleHoriz and ChromaSubsampleVert respectively,
71+
// then the source data will be padded.
72+
width += TiffUtils.PaddingToNextInteger(width, horizontalSubSampling);
73+
height += TiffUtils.PaddingToNextInteger(height, verticalSubSampling);
74+
int blockWidth = width / horizontalSubSampling;
75+
int blockHeight = height / verticalSubSampling;
76+
int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling;
77+
int blockByteCount = cbCrOffsetInBlock + 2;
78+
79+
for (int blockRow = blockHeight - 1; blockRow >= 0; blockRow--)
80+
{
81+
for (int blockCol = blockWidth - 1; blockCol >= 0; blockCol--)
82+
{
83+
int blockOffset = (blockRow * blockWidth) + blockCol;
84+
ReadOnlySpan<byte> blockData = source.Slice(blockOffset * blockByteCount, blockByteCount);
85+
byte cr = blockData[cbCrOffsetInBlock + 1];
86+
byte cb = blockData[cbCrOffsetInBlock];
87+
88+
for (int row = verticalSubSampling - 1; row >= 0; row--)
89+
{
90+
for (int col = horizontalSubSampling - 1; col >= 0; col--)
91+
{
92+
int offset = 3 * ((((blockRow * verticalSubSampling) + row) * width) + (blockCol * horizontalSubSampling) + col);
93+
destination[offset + 2] = cr;
94+
destination[offset + 1] = cb;
95+
destination[offset] = blockData[(row * horizontalSubSampling) + col];
96+
}
97+
}
98+
}
99+
}
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)