diff --git a/src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs b/src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs index 8cd3004c3..13208822e 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs @@ -5,6 +5,8 @@ using System.Text; namespace SixLabors.ImageSharp.Formats.Jpeg.Common { + using SixLabors.ImageSharp.Memory; + /// /// Represents a Jpeg block with coefficiens. /// @@ -44,7 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common } } - public short this[int y, int x] + public short this[int x, int y] { get => this[(y * 8) + x]; set => this[(y * 8) + x] = value; diff --git a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs index 5d30e345f..71a1e001c 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs @@ -10,6 +10,8 @@ using System.Runtime.InteropServices; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Formats.Jpeg.Common { + using SixLabors.ImageSharp.Memory; + /// /// Represents a Jpeg block with coefficients. /// @@ -83,7 +85,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common } } - public float this[int y, int x] + public float this[int x, int y] { get => this[(y * 8) + x]; set => this[(y * 8) + x] = value; @@ -304,6 +306,60 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CopyRowImpl(ref byte selfBase, ref byte destBase, int destStride, int row) + { + ref byte s = ref Unsafe.Add(ref selfBase, row * 8 * sizeof(float)); + ref byte d = ref Unsafe.Add(ref destBase, row * destStride); + Unsafe.CopyBlock(ref d, ref s, 8 * sizeof(float)); + } + + public void CopyTo(BufferArea area) + { + ref byte selfBase = ref Unsafe.As(ref this); + ref byte destBase = ref Unsafe.As(ref area.DangerousGetPinnableReference()); + int destStride = area.Stride * sizeof(float); + + CopyRowImpl(ref selfBase, ref destBase, destStride, 0); + CopyRowImpl(ref selfBase, ref destBase, destStride, 1); + CopyRowImpl(ref selfBase, ref destBase, destStride, 2); + CopyRowImpl(ref selfBase, ref destBase, destStride, 3); + CopyRowImpl(ref selfBase, ref destBase, destStride, 4); + CopyRowImpl(ref selfBase, ref destBase, destStride, 5); + CopyRowImpl(ref selfBase, ref destBase, destStride, 6); + CopyRowImpl(ref selfBase, ref destBase, destStride, 7); + } + + public void CopyTo(BufferArea area, int horizontalScale, int verticalScale) + { + if (horizontalScale == 1 && verticalScale == 1) + { + this.CopyTo(area); + return; + } + + // TODO: Optimize: implement all the cases with loopless special code! (T4?) + for (int y = 0; y < 8; y++) + { + int yy = y * verticalScale; + + for (int x = 0; x < 8; x++) + { + int xx = x * horizontalScale; + + float value = this[(y * 8) + x]; + + for (int i = 0; i < verticalScale; i++) + { + for (int j = 0; j < horizontalScale; j++) + { + area[xx + j, yy + i] = value; + } + } + } + } + } + public float[] ToArray() { float[] result = new float[Size]; @@ -520,5 +576,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common DebugGuard.MustBeLessThan(idx, Size, nameof(idx)); DebugGuard.MustBeGreaterThanOrEqualTo(idx, 0, nameof(idx)); } + + [StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))] + private struct Row + { + } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/ComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Common/ComponentPostProcessor.cs new file mode 100644 index 000000000..93e6a6705 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/ComponentPostProcessor.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using SixLabors.ImageSharp.Memory; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Common +{ + internal class JpegPostProcessor + { + private ComponentPostProcessor[] componentProcessors; + + public JpegPostProcessor(IRawJpegData data) + { + this.Data = data; + this.componentProcessors = data.Components.Select(c => new ComponentPostProcessor(this, c)).ToArray(); + } + + public IRawJpegData Data { get; } + } + + internal class ComponentPostProcessor : IDisposable + { + public ComponentPostProcessor(JpegPostProcessor jpegPostProcessor, IJpegComponent component) + { + this.Component = component; + this.JpegPostProcessor = jpegPostProcessor; + } + + public JpegPostProcessor JpegPostProcessor { get; } + + public IJpegComponent Component { get; } + + public int NumberOfRowGroupSteps { get; } + + public Buffer2D ColorBuffer { get; } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/ComponentUtils.cs b/src/ImageSharp/Formats/Jpeg/Common/ComponentUtils.cs index 78405a313..7d38d1b50 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/ComponentUtils.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/ComponentUtils.cs @@ -13,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common { public static Size SizeInBlocks(this IJpegComponent component) => new Size(component.WidthInBlocks, component.HeightInBlocks); + public static ref Block8x8 GetBlockReference(this IJpegComponent component, int bx, int by) + { + return ref component.SpectralBlocks[bx, by]; + } + public static SubsampleRatio GetSubsampleRatio(int horizontalRatio, int verticalRatio) { switch ((horizontalRatio << 4) | verticalRatio) @@ -107,19 +112,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common return sizes; } - - //public static Size CalculateJpegChannelSize(this IJpegComponent component, SubsampleRatio ratio = SubsampleRatio.Undefined) - //{ - // Size size = new Size(component.WidthInBlocks, component.HeightInBlocks) * 8; - - // if (component.IsChromaComponent()) - // { - // return ratio.CalculateChrominanceSize(size.Width, size.Height); - // } - // else - // { - // return size; - // } - //} } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/Common/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Common/IRawJpegData.cs new file mode 100644 index 000000000..7b3318f56 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/IRawJpegData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Common +{ + internal interface IRawJpegData + { + Size ImageSize { get; } + + Size ImageSizeInBlocks { get; } + + int ComponentCount { get; } + + IEnumerable Components { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/ComponentPostprocessor.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/ComponentPostprocessor.cs deleted file mode 100644 index 6fe07df02..000000000 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/ComponentPostprocessor.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using SixLabors.ImageSharp.Memory; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder -{ - internal class ComponentPostProcessor : IDisposable - { - public Size ImageSizeInBlocks { get; } - - public int NumberOfRowGroupScans - { - get; - - } - - class RowGroupProcessor : IDisposable - { - public Buffer2D ColorBuffer { get; } - - public void Dispose() - { - } - } - - - - public void Dispose() - { - - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/JpegBlockPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/JpegBlockPostProcessor.cs index ea55f0b93..95ac196d4 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/JpegBlockPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/JpegBlockPostProcessor.cs @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder /// The /// The x index of the block in /// The y index of the block in - private void ProcessBlockColors(OrigJpegDecoderCore decoder, OrigComponent component, int bx, int by) + private void ProcessBlockColors(OrigJpegDecoderCore decoder, IJpegComponent component, int bx, int by) { ref Block8x8 sourceBlock = ref component.GetBlockReference(bx, by); diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs index 391708441..7f0037cb0 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs @@ -52,12 +52,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder /// public int HeightInBlocks { get; private set; } - - public ref Block8x8 GetBlockReference(int bx, int by) - { - return ref this.SpectralBlocks[bx, by]; - } - + /// /// Initializes /// diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs index 7b7bf000c..ad5141c3a 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs @@ -18,10 +18,13 @@ using Block8x8F = SixLabors.ImageSharp.Formats.Jpeg.Common.Block8x8F; namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort { + using System.Collections.Generic; + using System.Numerics; + /// /// Performs the jpeg decoding operation. /// - internal sealed unsafe class OrigJpegDecoderCore : IDisposable + internal sealed unsafe class OrigJpegDecoderCore : IDisposable, IRawJpegData { /// /// The maximum number of color components @@ -138,20 +141,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort /// public byte[] Temp { get; } + public Size ImageSize { get; private set; } + + public Size ImageSizeInBlocks { get; private set; } + /// /// Gets the number of color components within the image. /// public int ComponentCount { get; private set; } + IEnumerable IRawJpegData.Components => this.Components; + /// /// Gets the image height /// - public int ImageHeight { get; private set; } + public int ImageHeight => this.ImageSize.Height; /// /// Gets the image width /// - public int ImageWidth { get; private set; } + public int ImageWidth => this.ImageSize.Width; /// /// Gets the input stream. @@ -1167,8 +1176,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort throw new ImageFormatException("Only 8-Bit precision supported."); } - this.ImageHeight = (this.Temp[1] << 8) + this.Temp[2]; - this.ImageWidth = (this.Temp[3] << 8) + this.Temp[4]; + int height = (this.Temp[1] << 8) + this.Temp[2]; + int width = (this.Temp[3] << 8) + this.Temp[4]; + + this.InitSizes(width, height); + if (this.Temp[5] != this.ComponentCount) { throw new ImageFormatException("SOF has wrong length"); @@ -1197,5 +1209,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort this.SubsampleRatio = ComponentUtils.GetSubsampleRatio(this.Components); } + + private void InitSizes(int width, int height) + { + this.ImageSize = new Size(width, height); + + var sizeInBlocks = (Vector2)(SizeF)this.ImageSize; + sizeInBlocks /= 8; + sizeInBlocks.X = MathF.Ceiling(sizeInBlocks.X); + sizeInBlocks.Y = MathF.Ceiling(sizeInBlocks.Y); + this.ImageSizeInBlocks = new Size((int)sizeInBlocks.X, (int)sizeInBlocks.Y); + } } } diff --git a/src/ImageSharp/Memory/BufferArea.cs b/src/ImageSharp/Memory/BufferArea.cs index 542420d0c..12843e209 100644 --- a/src/ImageSharp/Memory/BufferArea.cs +++ b/src/ImageSharp/Memory/BufferArea.cs @@ -34,8 +34,13 @@ namespace SixLabors.ImageSharp.Memory public Size Size => this.Rectangle.Size; + public int Stride => this.DestinationBuffer.Width; + public ref T this[int x, int y] => ref this.DestinationBuffer.Span[this.GetIndexOf(x, y)]; + public ref T DangerousGetPinnableReference() => + ref this.DestinationBuffer.Span[(this.Rectangle.Y * this.DestinationBuffer.Width) + this.Rectangle.X]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span GetRowSpan(int y) { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs new file mode 100644 index 000000000..d8cb8af8c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + + + +// Uncomment this to turn unit tests into benchmarks: +//#define BENCHMARKING + +// ReSharper disable InconsistentNaming + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + using SixLabors.ImageSharp.Formats.Jpeg.Common; + using SixLabors.ImageSharp.Memory; + using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; + using SixLabors.Primitives; + + using Xunit; + using Xunit.Abstractions; + + public partial class Block8x8FTests : JpegFixture + { + public class CopyToBufferArea : JpegFixture + { + public CopyToBufferArea(ITestOutputHelper output) + : base(output) + { + } + + private static void VerifyAllZeroOutsideSubArea(Buffer2D buffer, int subX, int subY, int horizontalFactor = 1, int verticalFactor = 1) + { + for (int y = 0; y < 20; y++) + { + for (int x = 0; x < 20; x++) + { + if (x < subX || x >= subX + 8 * horizontalFactor || y < subY || y >= subY + 8 * verticalFactor) + { + Assert.Equal(0, buffer[x, y]); + } + } + } + } + + [Fact] + public void Unscaled() + { + Block8x8F block = CreateRandomFloatBlock(0, 100); + + using (var buffer = new Buffer2D(20, 20)) + { + BufferArea area = buffer.GetArea(5, 10, 8, 8); + block.CopyTo(area); + + Assert.Equal(block[0, 0], buffer[5, 10]); + Assert.Equal(block[1, 0], buffer[6, 10]); + Assert.Equal(block[0, 1], buffer[5, 11]); + Assert.Equal(block[0, 7], buffer[5, 17]); + Assert.Equal(block[63], buffer[12, 17]); + + VerifyAllZeroOutsideSubArea(buffer, 5, 10); + } + } + + [Theory] + [InlineData(1, 1)] + [InlineData(1, 2)] + [InlineData(2, 1)] + [InlineData(2, 2)] + [InlineData(4, 2)] + [InlineData(4, 4)] + public void Scaled(int horizontalFactor, int verticalFactor) + { + Block8x8F block = CreateRandomFloatBlock(0, 100); + + var start = new Point(50, 50); + + using (var buffer = new Buffer2D(100, 100)) + { + BufferArea area = buffer.GetArea(start.X, start.Y, 8 * horizontalFactor, 8 * verticalFactor); + block.CopyTo(area, horizontalFactor, verticalFactor); + + for (int y = 0; y < 8 * verticalFactor; y++) + { + for (int x = 0; x < 8 * horizontalFactor; x++) + { + int yy = y / verticalFactor; + int xx = x / horizontalFactor; + + float expected = block[xx, yy]; + float actual = area[x, y]; + + Assert.Equal(expected, actual); + } + } + + VerifyAllZeroOutsideSubArea(buffer, start.X, start.Y, horizontalFactor, verticalFactor); + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index 3f643344b..aa224fd70 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using Xunit; using Xunit.Abstractions; - public class Block8x8FTests : JpegFixture + public partial class Block8x8FTests : JpegFixture { #if BENCHMARKING public const int Times = 1000000; @@ -313,23 +313,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(expected, actual); } } - - //[Fact] - //public void AsInt16Block() - //{ - // float[] data = Create8x8FloatData(); - - // var source = default(Block8x8F); - // source.LoadFrom(data); - - // Block8x8 dest = source.AsInt16Block(); - - // for (int i = 0; i < Block8x8F.Size; i++) - // { - // Assert.Equal((short)data[i], dest[i]); - // } - //} - + [Fact] public void RoundInto() { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8Tests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8Tests.cs index d1a128b53..c2fa8c8d4 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8Tests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8Tests.cs @@ -115,12 +115,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } [Fact] - public void IndexerYX() + public void IndexerXY() { var block = default(Block8x8); block[8 * 3 + 5] = 42; - short value = block[3, 5]; + short value = block[5, 3]; Assert.Equal(42, value); } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 351d57bd7..4eb55ccfe 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -28,6 +28,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Baseline.Calliphora, TestImages.Jpeg.Baseline.Cmyk, TestImages.Jpeg.Baseline.Jpeg400, TestImages.Jpeg.Baseline.Jpeg444, TestImages.Jpeg.Baseline.Testorig420, + TestImages.Jpeg.Baseline.Jpeg420Small, TestImages.Jpeg.Baseline.Bad.BadEOF, TestImages.Jpeg.Baseline.Bad.ExifUndefType, }; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs index ab5d072a4..4404d2cfe 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs @@ -103,6 +103,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils return result; } + internal static Block8x8F CreateRandomFloatBlock(float minValue, float maxValue, int seed = 42) => + Block8x8F.Load(Create8x8RandomFloatData(minValue, maxValue, seed)); + internal void Print8x8Data(T[] data) => this.Print8x8Data(new Span(data)); internal void Print8x8Data(Span data) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.AccurateDCT.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.AccurateDCT.cs index f46155ac4..6a1e09a9b 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.AccurateDCT.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.AccurateDCT.cs @@ -66,7 +66,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils } tmp += CosLut[y, v] * tmp2; } - res[y, x] = (float)tmp; + res[y * 8 + x] = (float)tmp; } } return res; @@ -88,11 +88,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils tmp2 = 0.0; for (x = 0; x < 8; x++) { - tmp2 += (double)block[y,x] * CosLut[x,u]; + tmp2 += (double)block[y * 8 + x] * CosLut[x,u]; } - tmp += CosLut[y,v] * tmp2; + tmp += CosLut[y, v] * tmp2; } - res[v,u] = (float) tmp; + res[v * 8 + u] = (float) tmp; } } diff --git a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs index 961380033..226e49aec 100644 --- a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs +++ b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs @@ -117,5 +117,19 @@ namespace SixLabors.ImageSharp.Tests.Memory Assert.Equal(value00, area1[0, 0]); } } + + [Fact] + public void DangerousGetPinnableReference() + { + using (Buffer2D buffer = CreateTestBuffer(20, 30)) + { + BufferArea area0 = buffer.GetArea(6, 8, 10, 10); + + ref int r = ref area0.DangerousGetPinnableReference(); + + int expected = buffer[6, 8]; + Assert.Equal(expected, r); + } + } } } \ No newline at end of file