diff --git a/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs b/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs index 61308ee2f8..6f7140fa2f 100644 --- a/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs +++ b/src/ImageSharp/Formats/Jpg/Components/Decoder/YCbCrImage.cs @@ -8,62 +8,79 @@ namespace ImageSharp.Formats.Jpg using System.Buffers; /// - /// Represents an image made up of three color components (luminance, blue chroma, red chroma) + /// Represents an image made up of three color components (luminance, blue chroma, red chroma) /// internal class YCbCrImage : IDisposable { + // Complex value type field + mutable + available to other classes = the field MUST NOT be private :P +#pragma warning disable SA1401 // FieldsMustBePrivate /// - /// Initializes a new instance of the class. + /// Gets the luminance components channel as . + /// + public JpegPixelArea YChannel; + + /// + /// Gets the blue chroma components channel as . + /// + public JpegPixelArea CbChannel; + + /// + /// Gets an offseted to the Cr channel + /// + public JpegPixelArea CrChannel; +#pragma warning restore SA1401 + + /// + /// Initializes a new instance of the class. /// /// The width. /// The height. /// The ratio. public YCbCrImage(int width, int height, YCbCrSubsampleRatio ratio) { - int cw, ch; - YCbCrSize(width, height, ratio, out cw, out ch); - this.YPixels = BytePool.Rent(width * height); - this.CbPixels = BytePool.Rent(cw * ch); - this.CrPixels = BytePool.Rent(cw * ch); + Size cSize = CalculateChrominanceSize(width, height, ratio); + this.Ratio = ratio; - this.YOffset = 0; - this.COffset = 0; this.YStride = width; - this.CStride = cw; + this.CStride = cSize.Width; + + this.YChannel = JpegPixelArea.CreatePooled(width, height); + this.CbChannel = JpegPixelArea.CreatePooled(cSize.Width, cSize.Height); + this.CrChannel = JpegPixelArea.CreatePooled(cSize.Width, cSize.Height); } /// - /// Provides enumeration of the various available subsample ratios. + /// Provides enumeration of the various available subsample ratios. /// public enum YCbCrSubsampleRatio { /// - /// YCbCrSubsampleRatio444 + /// YCbCrSubsampleRatio444 /// YCbCrSubsampleRatio444, /// - /// YCbCrSubsampleRatio422 + /// YCbCrSubsampleRatio422 /// YCbCrSubsampleRatio422, /// - /// YCbCrSubsampleRatio420 + /// YCbCrSubsampleRatio420 /// YCbCrSubsampleRatio420, /// - /// YCbCrSubsampleRatio440 + /// YCbCrSubsampleRatio440 /// YCbCrSubsampleRatio440, /// - /// YCbCrSubsampleRatio411 + /// YCbCrSubsampleRatio411 /// YCbCrSubsampleRatio411, /// - /// YCbCrSubsampleRatio410 + /// YCbCrSubsampleRatio410 /// YCbCrSubsampleRatio410, } @@ -71,77 +88,37 @@ namespace ImageSharp.Formats.Jpg private static ArrayPool BytePool => ArrayPool.Shared; /// - /// Gets an offseted to the Cb channel - /// - public JpegPixelArea CbChannel => new JpegPixelArea(this.CbPixels, this.CStride, this.COffset); - - /// - /// Gets the blue chroma components channel. - /// - public byte[] CbPixels { get; } - - /// - /// Gets the index of the first element of red or blue chroma. - /// - public int COffset { get; } - - /// - /// Gets an offseted to the Cr channel + /// Gets the Y slice index delta between vertically adjacent pixels. /// - public JpegPixelArea CrChannel => new JpegPixelArea(this.CrPixels, this.CStride, this.COffset); - - /// - /// Gets the red chroma components channel. - /// - public byte[] CrPixels { get; } + public int YStride { get; } /// - /// Gets the red and blue chroma slice index delta between vertically adjacent pixels - /// that map to separate chroma samples. + /// Gets the red and blue chroma slice index delta between vertically adjacent pixels + /// that map to separate chroma samples. /// public int CStride { get; } /// - /// Gets or sets the subsampling ratio. + /// Gets or sets the subsampling ratio. /// public YCbCrSubsampleRatio Ratio { get; set; } /// - /// Gets an offseted to the Y channel - /// - public JpegPixelArea YChannel => new JpegPixelArea(this.YPixels, this.YStride, this.YOffset); - - /// - /// Gets the index of the first luminance element. - /// - public int YOffset { get; } - - /// - /// Gets the luminance components channel. - /// - public byte[] YPixels { get; } - - /// - /// Gets the Y slice index delta between vertically adjacent pixels. - /// - public int YStride { get; } - - /// - /// Disposes the returning rented arrays to the pools. + /// Disposes the returning rented arrays to the pools. /// public void Dispose() { - BytePool.Return(this.YPixels); - BytePool.Return(this.CrPixels); - BytePool.Return(this.CbPixels); + this.YChannel.ReturnPooled(); + this.CbChannel.ReturnPooled(); + this.CrChannel.ReturnPooled(); } /// - /// Returns the offset of the first chroma component at the given row + /// Returns the offset of the first chroma component at the given row /// /// The row number. /// - /// The . + /// The . /// public int GetRowCOffset(int y) { @@ -163,11 +140,11 @@ namespace ImageSharp.Formats.Jpg } /// - /// Returns the offset of the first luminance component at the given row + /// Returns the offset of the first luminance component at the given row /// /// The row number. /// - /// The . + /// The . /// public int GetRowYOffset(int y) { @@ -175,48 +152,32 @@ namespace ImageSharp.Formats.Jpg } /// - /// Returns the height and width of the chroma components + /// Returns the height and width of the chroma components /// /// The width. /// The height. /// The subsampling ratio. - /// The chroma width. - /// The chroma height. - private static void YCbCrSize( + /// The of the chrominance channel + internal static Size CalculateChrominanceSize( int width, int height, - YCbCrSubsampleRatio ratio, - out int chromaWidth, - out int chromaHeight) + YCbCrSubsampleRatio ratio) { switch (ratio) { case YCbCrSubsampleRatio.YCbCrSubsampleRatio422: - chromaWidth = (width + 1) / 2; - chromaHeight = height; - break; + return new Size((width + 1) / 2, height); case YCbCrSubsampleRatio.YCbCrSubsampleRatio420: - chromaWidth = (width + 1) / 2; - chromaHeight = (height + 1) / 2; - break; + return new Size((width + 1) / 2, (height + 1) / 2); case YCbCrSubsampleRatio.YCbCrSubsampleRatio440: - chromaWidth = width; - chromaHeight = (height + 1) / 2; - break; + return new Size(width, (height + 1) / 2); case YCbCrSubsampleRatio.YCbCrSubsampleRatio411: - chromaWidth = (width + 3) / 4; - chromaHeight = height; - break; + return new Size((width + 3) / 4, height); case YCbCrSubsampleRatio.YCbCrSubsampleRatio410: - chromaWidth = (width + 3) / 4; - chromaHeight = (height + 1) / 2; - break; + return new Size((width + 3) / 4, (height + 1) / 2); default: - // Default to 4:4:4 subsampling. - chromaWidth = width; - chromaHeight = height; - break; + return new Size(width, height); } } } diff --git a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs index a792a19912..284ae807b4 100644 --- a/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpg/JpegDecoderCore.cs @@ -21,7 +21,7 @@ namespace ImageSharp.Formats /// public const int MaxComponents = 4; - // Complex value type field + mutable + available to other classes == the field MUST NOT be private :P + // Complex value type field + mutable + available to other classes = the field MUST NOT be private :P #pragma warning disable SA1401 // FieldsMustBePrivate /// /// Holds the unprocessed bits that have been taken from the byte-stream. @@ -650,14 +650,15 @@ namespace ImageSharp.Formats height, y => { + // TODO: Simplify + optimize + share duplicate code across converter methods int yo = this.ycbcrImage.GetRowYOffset(y); int co = this.ycbcrImage.GetRowCOffset(y); for (int x = 0; x < width; x++) { - byte cyan = this.ycbcrImage.YPixels[yo + x]; - byte magenta = this.ycbcrImage.CbPixels[co + (x / scale)]; - byte yellow = this.ycbcrImage.CrPixels[co + (x / scale)]; + byte cyan = this.ycbcrImage.YChannel.Pixels[yo + x]; + byte magenta = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)]; + byte yellow = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)]; TColor packed = default(TColor); this.PackCmyk(ref packed, cyan, magenta, yellow, x, y); @@ -725,14 +726,15 @@ namespace ImageSharp.Formats Bootstrapper.ParallelOptions, y => { + // TODO: Simplify + optimize + share duplicate code across converter methods int yo = this.ycbcrImage.GetRowYOffset(y); int co = this.ycbcrImage.GetRowCOffset(y); for (int x = 0; x < width; x++) { - byte red = this.ycbcrImage.YPixels[yo + x]; - byte green = this.ycbcrImage.CbPixels[co + (x / scale)]; - byte blue = this.ycbcrImage.CrPixels[co + (x / scale)]; + byte red = this.ycbcrImage.YChannel.Pixels[yo + x]; + byte green = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)]; + byte blue = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)]; TColor packed = default(TColor); packed.PackFromBytes(red, green, blue, 255); @@ -765,14 +767,15 @@ namespace ImageSharp.Formats Bootstrapper.ParallelOptions, y => { + // TODO: Simplify + optimize + share duplicate code across converter methods int yo = this.ycbcrImage.GetRowYOffset(y); int co = this.ycbcrImage.GetRowCOffset(y); for (int x = 0; x < width; x++) { - byte yy = this.ycbcrImage.YPixels[yo + x]; - byte cb = this.ycbcrImage.CbPixels[co + (x / scale)]; - byte cr = this.ycbcrImage.CrPixels[co + (x / scale)]; + byte yy = this.ycbcrImage.YChannel.Pixels[yo + x]; + byte cb = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)]; + byte cr = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)]; TColor packed = default(TColor); PackYcbCr(ref packed, yy, cb, cr); @@ -805,14 +808,15 @@ namespace ImageSharp.Formats height, y => { + // TODO: Simplify + optimize + share duplicate code across converter methods int yo = this.ycbcrImage.GetRowYOffset(y); int co = this.ycbcrImage.GetRowCOffset(y); for (int x = 0; x < width; x++) { - byte yy = this.ycbcrImage.YPixels[yo + x]; - byte cb = this.ycbcrImage.CbPixels[co + (x / scale)]; - byte cr = this.ycbcrImage.CrPixels[co + (x / scale)]; + byte yy = this.ycbcrImage.YChannel.Pixels[yo + x]; + byte cb = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)]; + byte cr = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)]; TColor packed = default(TColor); this.PackYcck(ref packed, yy, cb, cr, x, y); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs index 81156d9703..53c44d836c 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs @@ -94,7 +94,7 @@ namespace ImageSharp.Tests Image img = new Image(bytes); }, $"Decode {fileName}"); - + } //[Theory] // Benchmark, enable manually @@ -133,7 +133,7 @@ namespace ImageSharp.Tests for (int j = 0; j < 10; j++) { Vector4 v = new Vector4(i/10f, j/10f, 0, 1); - + TColor color = default(TColor); color.PackFromVector4(v); @@ -171,11 +171,11 @@ namespace ImageSharp.Tests [Theory] [WithMemberFactory(nameof(CreateTestImage), PixelTypes.Color | PixelTypes.StandardImageClass | PixelTypes.Argb)] - public void CopyStretchedRGBTo_WithOffset(TestImageProvider provider) + public void CopyStretchedRGBTo_WithOffset(TestImageProvider provider) where TColor : struct, IPackedPixel, IEquatable { var src = provider.GetImage(); - + PixelArea area = new PixelArea(8, 8, ComponentOrder.Xyz); var dest = provider.Factory.CreateImage(8, 8); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/YCbCrImageTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/YCbCrImageTests.cs new file mode 100644 index 0000000000..b90973a9cf --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/YCbCrImageTests.cs @@ -0,0 +1,65 @@ +namespace ImageSharp.Tests +{ + using ImageSharp.Formats.Jpg; + + using Xunit; + using Xunit.Abstractions; + + public class YCbCrImageTests + { + public YCbCrImageTests(ITestOutputHelper output) + { + this.Output = output; + } + + private ITestOutputHelper Output { get; } + + private void PrintChannel(string name, JpegPixelArea channel) + { + this.Output.WriteLine($"{name}: Stride={channel.Stride}"); + } + + [Theory] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio410, 4, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio411, 4, 1)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio420, 2, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio422, 2, 1)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio440, 1, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444, 1, 1)] + public void CalculateChrominanceSize(int ratioValue, int expectedDivX, int expectedDivY) + { + YCbCrImage.YCbCrSubsampleRatio ratio = (YCbCrImage.YCbCrSubsampleRatio)ratioValue; + + //this.Output.WriteLine($"RATIO: {ratio}"); + Size size = YCbCrImage.CalculateChrominanceSize(400, 400, ratio); + //this.Output.WriteLine($"Ch Size: {size}"); + + Assert.Equal(new Size(400/expectedDivX, 400/expectedDivY), size); + } + + [Theory] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio410, 4)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio411, 4)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio420, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio422, 2)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio440, 1)] + [InlineData(YCbCrImage.YCbCrSubsampleRatio.YCbCrSubsampleRatio444, 1)] + public void Create(int ratioValue, int expectedCStrideDiv) + { + YCbCrImage.YCbCrSubsampleRatio ratio = (YCbCrImage.YCbCrSubsampleRatio)ratioValue; + + this.Output.WriteLine($"RATIO: {ratio}"); + + var img = new YCbCrImage(400, 400, ratio); + + //this.PrintChannel("Y", img.YChannel); + //this.PrintChannel("Cb", img.CbChannel); + //this.PrintChannel("Cr", img.CrChannel); + + Assert.Equal(img.YChannel.Stride, 400); + Assert.Equal(img.CbChannel.Stride, 400 / expectedCStrideDiv); + Assert.Equal(img.CrChannel.Stride, 400 / expectedCStrideDiv); + } + + } +} \ No newline at end of file