diff --git a/src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.cs b/src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.cs new file mode 100644 index 0000000000..1bb37a7d32 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using System.Runtime.CompilerServices; + +// +namespace SixLabors.ImageSharp.Formats.Jpeg.Common +{ + internal unsafe partial struct GenericBlock8x8 + { + #pragma warning disable 169 + + // It's not allowed use fix-sized buffers with generics, need to place all the fields manually: + private T _y0_x0, _y0_x1, _y0_x2, _y0_x3, _y0_x4, _y0_x5, _y0_x6, _y0_x7; + private T _y1_x0, _y1_x1, _y1_x2, _y1_x3, _y1_x4, _y1_x5, _y1_x6, _y1_x7; + private T _y2_x0, _y2_x1, _y2_x2, _y2_x3, _y2_x4, _y2_x5, _y2_x6, _y2_x7; + private T _y3_x0, _y3_x1, _y3_x2, _y3_x3, _y3_x4, _y3_x5, _y3_x6, _y3_x7; + private T _y4_x0, _y4_x1, _y4_x2, _y4_x3, _y4_x4, _y4_x5, _y4_x6, _y4_x7; + private T _y5_x0, _y5_x1, _y5_x2, _y5_x3, _y5_x4, _y5_x5, _y5_x6, _y5_x7; + private T _y6_x0, _y6_x1, _y6_x2, _y6_x3, _y6_x4, _y6_x5, _y6_x6, _y6_x7; + private T _y7_x0, _y7_x1, _y7_x2, _y7_x3, _y7_x4, _y7_x5, _y7_x6, _y7_x7; + + #pragma warning restore 169 + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.tt b/src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.tt new file mode 100644 index 0000000000..d9b15b34fa --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.Generated.tt @@ -0,0 +1,43 @@ +<# +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. +#> +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".cs" #> +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using System.Runtime.CompilerServices; + +// +namespace SixLabors.ImageSharp.Formats.Jpeg.Common +{ + internal unsafe partial struct GenericBlock8x8 + { + #pragma warning disable 169 + + // It's not allowed use fix-sized buffers with generics, need to place all the fields manually: + <# + PushIndent(" "); + Write(" "); + for (int y = 0; y < 8; y++) + { + Write("private T "); + for (int x = 0; x < 8; x++) + { + Write($"_y{y}_x{x}"); + if (x < 7) Write(", "); + } + WriteLine(";"); + } + PopIndent(); + #> + + #pragma warning restore 169 + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.cs b/src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.cs new file mode 100644 index 0000000000..bc5838e9b5 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/GenericBlock8x8.cs @@ -0,0 +1,128 @@ +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Common +{ + /// + /// A generic 8x8 block implementation, useful for manipulating custom 8x8 pixel data. + /// + // ReSharper disable once InconsistentNaming + internal unsafe partial struct GenericBlock8x8 + where T : struct + { + public const int Size = 64; + + public const int SizeInBytes = Size * 3; + + public void LoadAndStretchEdges(IPixelSource source, int sourceX, int sourceY) + where TPixel : struct, IPixel + { + var buffer = source.PixelBuffer as Buffer2D; + if (buffer == null) + { + throw new InvalidOperationException("LoadAndStretchEdges() is only valid for TPixel == T !"); + } + + this.LoadAndStretchEdges(buffer, sourceX, sourceY); + } + + /// + /// Load a 8x8 region of an image into the block. + /// The "outlying" area of the block will be stretched out with pixels on the right and bottom edge of the image. + /// + public void LoadAndStretchEdges(Buffer2D source, int sourceX, int sourceY) + { + int width = Math.Min(8, source.Width - sourceX); + int height = Math.Min(8, source.Height - sourceY); + + if (width <= 0 || height <= 0) + { + return; + } + + uint byteWidth = (uint)width * (uint)Unsafe.SizeOf(); + int remainderXCount = 8 - width; + + ref byte blockStart = ref Unsafe.As, byte>(ref this); + ref byte imageStart = ref Unsafe.As( + ref Unsafe.Add(ref source.GetRowSpan(sourceY).DangerousGetPinnableReference(), sourceX) + ); + + int blockRowSizeInBytes = 8 * Unsafe.SizeOf(); + int imageRowSizeInBytes = source.Width * Unsafe.SizeOf(); + + for (int y = 0; y < height; y++) + { + ref byte s = ref Unsafe.Add(ref imageStart, y * imageRowSizeInBytes); + ref byte d = ref Unsafe.Add(ref blockStart, y * blockRowSizeInBytes); + + Unsafe.CopyBlock(ref d, ref s, byteWidth); + + ref T last = ref Unsafe.Add(ref Unsafe.As(ref d), width - 1); + + for (int x = 1; x <= remainderXCount; x++) + { + Unsafe.Add(ref last, x) = last; + } + } + + int remainderYCount = 8 - height; + + if (remainderYCount == 0) + { + return; + } + + ref byte lastRowStart = ref Unsafe.Add(ref blockStart, (height - 1) * blockRowSizeInBytes); + + for (int y = 1; y <= remainderYCount; y++) + { + ref byte remStart = ref Unsafe.Add(ref lastRowStart, blockRowSizeInBytes * y); + Unsafe.CopyBlock(ref remStart, ref lastRowStart, (uint)blockRowSizeInBytes); + } + } + + /// + /// ONLY FOR GenericBlock instances living on the stack! + /// + public Span AsSpanUnsafe() => new Span(Unsafe.AsPointer(ref this), Size); + + /// + /// FOR TESTING ONLY! + /// Gets or sets a value at the given index + /// + /// The index + /// The value + public T this[int idx] + { + get + { + ref T selfRef = ref Unsafe.As, T>(ref this); + return Unsafe.Add(ref selfRef, idx); + } + + set + { + ref T selfRef = ref Unsafe.As, T>(ref this); + Unsafe.Add(ref selfRef, idx) = value; + } + } + + /// + /// FOR TESTING ONLY! + /// Gets or sets a value in a row+coulumn of the 8x8 block + /// + /// The x position index in the row + /// The column index + /// The value + public T this[int x, int y] + { + get => this[(y * 8) + x]; + set => this[(y * 8) + x] = value; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Encoder/RgbToYCbCrTables.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Encoder/RgbToYCbCrTables.cs index 02bd451b94..3c1d666850 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Encoder/RgbToYCbCrTables.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Encoder/RgbToYCbCrTables.cs @@ -8,6 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Encoder /// /// Provides 8-bit lookup tables for converting from Rgb to YCbCr colorspace. /// Methods to build the tables are based on libjpeg implementation. + /// TODO: Replace this logic with SIMD conversion (similar to the one in the decoder)! /// internal unsafe struct RgbToYCbCrTables { @@ -91,6 +92,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Encoder } /// + /// TODO: Replace this logic with SIMD conversion (similar to the one in the decoder)! /// Optimized method to allocates the correct y, cb, and cr values to the DCT blocks from the given r, g, b values. /// /// The The luminance block. @@ -102,7 +104,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Encoder /// The green value. /// The blue value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Allocate(ref float* yBlockRaw, ref float* cbBlockRaw, ref float* crBlockRaw, ref RgbToYCbCrTables* tables, int index, int r, int g, int b) + public static void Rgb2YCbCr(float* yBlockRaw, float* cbBlockRaw, float* crBlockRaw, ref RgbToYCbCrTables* tables, int index, int r, int g, int b) { // float y = (0.299F * r) + (0.587F * g) + (0.114F * b); yBlockRaw[index] = (tables->YRTable[r] + tables->YGTable[g] + tables->YBTable[b]) >> ScaleBits; diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs index 4a9ddf3536..b03b0b5341 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs @@ -331,7 +331,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort int index = j8 + i; - RgbToYCbCrTables.Allocate(ref yBlockRaw, ref cbBlockRaw, ref crBlockRaw, ref tables, index, r, g, b); + RgbToYCbCrTables.Rgb2YCbCr(yBlockRaw, cbBlockRaw, crBlockRaw, ref tables, index, r, g, b); dataIdx += 3; } diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/Utils/OrigJpegUtils.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/Utils/OrigJpegUtils.cs index 01ed5063ba..42a7d56e3b 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/Utils/OrigJpegUtils.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/Utils/OrigJpegUtils.cs @@ -8,7 +8,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils { /// - /// Jpeg specific utilities and extension methods + /// Jpeg specific utilities and extension methods /// internal static class OrigJpegUtils { diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index b812b0b227..8b89499ea7 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -62,6 +62,10 @@ TextTemplatingFileGenerator Block8x8F.Generated.cs + + TextTemplatingFileGenerator + GenericBlock8x8.Generated.cs + TextTemplatingFileGenerator Block8x8F.Generated.cs @@ -92,6 +96,11 @@ True Block8x8F.Generated.tt + + True + True + GenericBlock8x8.Generated.tt + True True diff --git a/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs b/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs new file mode 100644 index 0000000000..193e26fcbd --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs @@ -0,0 +1,126 @@ +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + using System; + using System.Numerics; + + using SixLabors.ImageSharp.Formats.Jpeg.Common; + using SixLabors.ImageSharp.Helpers; + using SixLabors.ImageSharp.PixelFormats; + using SixLabors.Primitives; + + using Xunit; + + public class GenericBlock8x8Tests + { + public static Image CreateTestImage() + where TPixel : struct, IPixel + { + var image = new Image(10, 10); + using (PixelAccessor pixels = image.Lock()) + { + for (int i = 0; i < 10; i++) + { + for (int j = 0; j < 10; j++) + { + var rgba = new Rgba32((byte)(i+1), (byte)(j+1), (byte)200, (byte)255); + var color = default(TPixel); + color.PackFromRgba32(rgba); + + pixels[i, j] = color; + } + } + } + + return image; + } + + [Theory] + [WithMemberFactory(nameof(CreateTestImage), PixelTypes.Rgb24 | PixelTypes.Rgba32 /* | PixelTypes.Rgba32 | PixelTypes.Argb32*/)] + public void LoadAndStretchCorners_FromOrigo(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image s = provider.GetImage()) + { + var d = default(GenericBlock8x8); + d.LoadAndStretchEdges(s.Frames.RootFrame, 0, 0); + + TPixel a = s.Frames.RootFrame[0, 0]; + TPixel b = d[0, 0]; + + Assert.Equal(s[0, 0], d[0, 0]); + Assert.Equal(s[1, 0], d[1, 0]); + Assert.Equal(s[7, 0], d[7, 0]); + Assert.Equal(s[0, 1], d[0, 1]); + Assert.Equal(s[1, 1], d[1, 1]); + Assert.Equal(s[7, 0], d[7, 0]); + Assert.Equal(s[0, 7], d[0, 7]); + Assert.Equal(s[7, 7], d[7, 7]); + } + } + + [Theory] + [WithMemberFactory(nameof(CreateTestImage), PixelTypes.Rgb24 | PixelTypes.Rgba32)] + public unsafe void LoadAndStretchCorners_WithOffset(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image s = provider.GetImage()) + { + var d = default(GenericBlock8x8); + d.LoadAndStretchEdges(s.Frames.RootFrame, 6, 7); + + Assert.Equal(s[6, 7], d[0, 0]); + Assert.Equal(s[6, 8], d[0, 1]); + Assert.Equal(s[7, 8], d[1, 1]); + + Assert.Equal(s[6, 9], d[0, 2]); + Assert.Equal(s[6, 9], d[0, 3]); + Assert.Equal(s[6, 9], d[0, 7]); + + Assert.Equal(s[7, 9], d[1, 2]); + Assert.Equal(s[7, 9], d[1, 3]); + Assert.Equal(s[7, 9], d[1, 7]); + + Assert.Equal(s[9, 9], d[3, 2]); + Assert.Equal(s[9, 9], d[3, 3]); + Assert.Equal(s[9, 9], d[3, 7]); + + Assert.Equal(s[9, 7], d[3, 0]); + Assert.Equal(s[9, 7], d[4, 0]); + Assert.Equal(s[9, 7], d[7, 0]); + + Assert.Equal(s[9, 9], d[3, 2]); + Assert.Equal(s[9, 9], d[4, 2]); + Assert.Equal(s[9, 9], d[7, 2]); + + Assert.Equal(s[9, 9], d[4, 3]); + Assert.Equal(s[9, 9], d[7, 7]); + } + } + + [Fact] + public void Indexer() + { + var block = default(GenericBlock8x8); + Span span = block.AsSpanUnsafe(); + Assert.Equal(64, span.Length); + + for (int i = 0; i < 64; i++) + { + span[i] = new Rgb24((byte)i, (byte)(2 * i), (byte)(3 * i)); + } + + Rgb24 expected00 = new Rgb24(0, 0, 0); + Rgb24 expected07 = new Rgb24(7, 14, 21); + Rgb24 expected11 = new Rgb24(9, 18, 27); + Rgb24 expected77 = new Rgb24(63, 126, 189); + Rgb24 expected67 = new Rgb24(62, 124, 186); + + Assert.Equal(expected00, block[0, 0]); + Assert.Equal(expected07, block[7, 0]); + Assert.Equal(expected11, block[1, 1]); + Assert.Equal(expected67, block[6, 7]); + Assert.Equal(expected77, block[7, 7]); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegUtilsTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegUtilsTests.cs index 887e9d7e95..6fe6a9bfde 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegUtilsTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegUtilsTests.cs @@ -1,18 +1,14 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. - +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Formats.Jpg { - using System.Numerics; - - using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Utils; - using SixLabors.ImageSharp.PixelFormats; - - using Xunit; - public class JpegUtilsTests { public static Image CreateTestImage()