diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs new file mode 100644 index 000000000..58c81727a --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs @@ -0,0 +1,47 @@ +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +using SixLabors.ImageSharp.ColorSpaces; +using SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation.YCbCrColorSapce; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder +{ + internal abstract partial class JpegColorConverter + { + private class FromYCbCr : JpegColorConverter + { + private static readonly YCbCrAndRgbConverter Converter = new YCbCrAndRgbConverter(); + + public FromYCbCr() + : base(JpegColorSpace.YCbCr) + { + } + + public override void ConvertToRGBA(ComponentValues values, Span result) + { + // TODO: We can optimize a lot here with Vector and SRCS.Unsafe()! + ReadOnlySpan yVals = values.Component0; + ReadOnlySpan cbVals = values.Component1; + ReadOnlySpan crVals = values.Component2; + + Vector4 rgbaVector = new Vector4(0, 0, 0, 1); + + for (int i = 0; i < result.Length; i++) + { + float colY = yVals[i]; + float colCb = cbVals[i]; + float colCr = crVals[i]; + + YCbCr yCbCr = new YCbCr(colY, colCb, colCr); + + // Slow conversion for now: + Rgb rgb = Converter.Convert(yCbCr); + + Unsafe.As(ref rgbaVector) = rgb.Vector; + result[i] = rgbaVector; + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs new file mode 100644 index 000000000..2ff0f7a10 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder +{ + internal abstract partial class JpegColorConverter + { + private static readonly JpegColorConverter[] Converters = { new FromYCbCr(), }; + + protected JpegColorConverter(JpegColorSpace colorSpace) + { + this.ColorSpace = colorSpace; + } + + public JpegColorSpace ColorSpace { get; } + + public static JpegColorConverter GetConverter(JpegColorSpace colorSpace) + { + JpegColorConverter converter = Converters.FirstOrDefault(c => c.ColorSpace == colorSpace); + if (converter == null) + { + throw new Exception($"Could not find any converter for JpegColorSpace {colorSpace}!"); + } + + return converter; + } + + public abstract void ConvertToRGBA(ComponentValues values, Span result); + + public struct ComponentValues + { + public readonly int ComponentCount; + + public readonly ReadOnlySpan Component0; + + public readonly ReadOnlySpan Component1; + + public readonly ReadOnlySpan Component2; + + public readonly ReadOnlySpan Component3; + + public ComponentValues(IReadOnlyList> componentBuffers, int row) + { + this.ComponentCount = componentBuffers.Count; + + this.Component0 = componentBuffers[0].GetRowSpan(row); + this.Component1 = Span.Empty; + this.Component2 = Span.Empty; + this.Component3 = Span.Empty; + + if (this.ComponentCount > 1) + { + this.Component1 = componentBuffers[1].GetRowSpan(row); + if (this.ComponentCount > 2) + { + this.Component2 = componentBuffers[2].GetRowSpan(row); + if (this.ComponentCount > 3) + { + this.Component3 = componentBuffers[3].GetRowSpan(row); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegImagePostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegImagePostProcessor.cs index 535863e4b..882cc1349 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegImagePostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegImagePostProcessor.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.ColorSpaces; using SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation.YCbCrColorSapce; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -15,6 +17,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder public const int PixelRowsPerStep = 4 * 8; + private readonly Buffer rgbaBuffer; + + private JpegColorConverter colorConverter; + public JpegImagePostProcessor(IRawJpegData rawJpeg) { this.RawJpeg = rawJpeg; @@ -23,6 +29,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder this.PostProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, PixelRowsPerStep); this.ComponentProcessors = rawJpeg.Components.Select(c => new JpegComponentPostProcessor(this, c)).ToArray(); + this.rgbaBuffer = new Buffer(rawJpeg.ImageSizeInPixels.Width); + this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace); } public JpegComponentPostProcessor[] ComponentProcessors { get; } @@ -41,6 +49,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { cpp.Dispose(); } + + this.rgbaBuffer.Dispose(); } public bool DoPostProcessorStep(Image destination) @@ -65,6 +75,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder public void PostProcess(Image destination) where TPixel : struct, IPixel { + if (this.RawJpeg.ImageSizeInPixels != destination.Size()) + { + throw new ArgumentException("Input image is not of the size of the processed one!"); + } + while (this.DoPostProcessorStep(destination)) { } @@ -75,31 +90,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder { int maxY = Math.Min(destination.Height, this.CurrentImageRowInPixels + PixelRowsPerStep); - JpegComponentPostProcessor[] cp = this.ComponentProcessors; - - YCbCrAndRgbConverter converter = new YCbCrAndRgbConverter(); - - Vector4 rgbaVector = new Vector4(0, 0, 0, 1); + Buffer2D[] buffers = this.ComponentProcessors.Select(cp => cp.ColorBuffer).ToArray(); for (int yy = this.CurrentImageRowInPixels; yy < maxY; yy++) { int y = yy - this.CurrentImageRowInPixels; - Span destRow = destination.GetRowSpan(yy); - - for (int x = 0; x < destination.Width; x++) - { - float colY = cp[0].ColorBuffer[x, y]; - float colCb = cp[1].ColorBuffer[x, y]; - float colCr = cp[2].ColorBuffer[x, y]; - - YCbCr yCbCr = new YCbCr(colY, colCb, colCr); - Rgb rgb = converter.Convert(yCbCr); + var values = new JpegColorConverter.ComponentValues(buffers, y); + this.colorConverter.ConvertToRGBA(values, this.rgbaBuffer); - Unsafe.As(ref rgbaVector) = rgb.Vector; + Span destRow = destination.GetRowSpan(yy); - destRow[x].PackFromVector4(rgbaVector); - } + PixelOperations.Instance.PackFromVector4(this.rgbaBuffer, destRow, destination.Width); } } } diff --git a/src/ImageSharp/Memory/Buffer.cs b/src/ImageSharp/Memory/Buffer.cs index bbe37b859..f5c9ed00a 100644 --- a/src/ImageSharp/Memory/Buffer.cs +++ b/src/ImageSharp/Memory/Buffer.cs @@ -7,12 +7,13 @@ using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Memory { + /// /// /// Manages a buffer of value type objects as a Disposable resource. /// The backing array is either pooled or comes from the outside. /// /// The value type. - internal class Buffer : IDisposable + internal class Buffer : IBuffer where T : struct { /// @@ -205,7 +206,8 @@ namespace SixLabors.ImageSharp.Memory { if (this.IsDisposedOrLostArrayOwnership) { - throw new InvalidOperationException("TakeArrayOwnership() is invalid: either Buffer is disposed or TakeArrayOwnership() has been called multiple times!"); + throw new InvalidOperationException( + "TakeArrayOwnership() is invalid: either Buffer is disposed or TakeArrayOwnership() has been called multiple times!"); } this.IsDisposedOrLostArrayOwnership = true; diff --git a/src/ImageSharp/Memory/IBuffer.cs b/src/ImageSharp/Memory/IBuffer.cs new file mode 100644 index 000000000..f59c5d5ea --- /dev/null +++ b/src/ImageSharp/Memory/IBuffer.cs @@ -0,0 +1,18 @@ +using System; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// + /// Represents a contigous memory buffer of value-type items "promising" a + /// + /// The value type + internal interface IBuffer : IDisposable + where T : struct + { + /// + /// Gets the span to the memory "promised" by this buffer + /// + Span Span { get; } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs new file mode 100644 index 000000000..9b7a63a97 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Numerics; + +using SixLabors.ImageSharp.ColorSpaces; +using SixLabors.ImageSharp.ColorSpaces.Conversion; +using SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation.YCbCrColorSapce; +using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.Colorspaces; +using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; + +using Xunit; +using Xunit.Abstractions; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + public class JpegColorConverterTests + { + private const float Precision = 0.1f; + + private const int InputBufferLength = 42; + + // The result buffer could be shorter + private const int ResultBufferLength = 40; + + private readonly Vector4[] Result = new Vector4[ResultBufferLength]; + + private static readonly ColorSpaceConverter ColorSpaceConverter = new ColorSpaceConverter(); + + public JpegColorConverterTests(ITestOutputHelper output) + { + this.Output = output; + } + + private ITestOutputHelper Output { get; } + + private static JpegColorConverter.ComponentValues CreateRandomValues(int componentCount, float maxVal = 255f) + { + var rnd = new Random(42); + Buffer2D[] buffers = new Buffer2D[componentCount]; + for (int i = 0; i < componentCount; i++) + { + float[] values = new float[InputBufferLength]; + + for (int j = 0; j < InputBufferLength; j++) + { + values[j] = (float)rnd.NextDouble() * maxVal; + } + + // no need to dispose when buffer is not array owner + buffers[i] = new Buffer2D(values, values.Length, 1); + } + return new JpegColorConverter.ComponentValues(buffers, 0); + } + + [Fact] + public void ConvertFromYCbCr() + { + var converter = JpegColorConverter.GetConverter(JpegColorSpace.YCbCr); + + JpegColorConverter.ComponentValues values = CreateRandomValues(3); + + converter.ConvertToRGBA(values, this.Result); + + for (int i = 0; i < ResultBufferLength; i++) + { + float y = values.Component0[i]; + float cb = values.Component1[i]; + float cr = values.Component2[i]; + YCbCr ycbcr = new YCbCr(y, cb, cr); + + Vector4 rgba = this.Result[i]; + Rgb actual = new Rgb(rgba.X, rgba.Y, rgba.Z); + Rgb expected = ColorSpaceConverter.ToRgb(ycbcr); + + Assert.True(actual.AlmostEquals(expected, Precision)); + Assert.Equal(1, rgba.W); + } + } + } +} \ No newline at end of file