diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs index 92482de2a..9619a78fc 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs @@ -55,9 +55,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// Converts a 8x8 image area inside 'pixels' at position (x,y) placing the result members of the structure (, , ) /// - public void Convert(ImageFrame frame, int x, int y) + public void Convert(ImageFrame frame, int x, int y, in RowOctet currentRows) { - this.pixelBlock.LoadAndStretchEdges(frame, x, y); + this.pixelBlock.LoadAndStretchEdges(frame.PixelBuffer, x, y, currentRows); Span rgbSpan = this.rgbBlock.AsSpanUnsafe(); PixelOperations.Instance.ToRgb24(frame.GetConfiguration(), this.pixelBlock.AsSpanUnsafe(), rgbSpan); diff --git a/src/ImageSharp/Formats/Jpeg/Components/GenericBlock8x8.cs b/src/ImageSharp/Formats/Jpeg/Components/GenericBlock8x8.cs index 3d1e22a99..ebc071494 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/GenericBlock8x8.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/GenericBlock8x8.cs @@ -54,24 +54,24 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components set => this[(y * 8) + x] = value; } - public void LoadAndStretchEdges(IPixelSource source, int sourceX, int sourceY) - where TPixel : struct, IPixel - { - if (source.PixelBuffer is Buffer2D buffer) - { - this.LoadAndStretchEdges(buffer, sourceX, sourceY); - } - else - { - throw new InvalidOperationException("LoadAndStretchEdges() is only valid for TPixel == T !"); - } - } + // public void LoadAndStretchEdges(IPixelSource source, int sourceX, RowOctet currentRows) + // where TPixel : struct, IPixel + // { + // if (source.PixelBuffer is Buffer2D buffer) + // { + // this.LoadAndStretchEdges(buffer, sourceX, sourceY); + // } + // else + // { + // throw new InvalidOperationException("LoadAndStretchEdges() is only valid for TPixel == T !"); + // } + // } /// /// 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) + public void LoadAndStretchEdges(Buffer2D source, int sourceX, int sourceY, in RowOctet currentRows) { int width = Math.Min(8, source.Width - sourceX); int height = Math.Min(8, source.Height - sourceY); @@ -85,15 +85,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components int remainderXCount = 8 - width; ref byte blockStart = ref Unsafe.As, byte>(ref this); - ref byte imageStart = ref Unsafe.As( - ref Unsafe.Add(ref MemoryMarshal.GetReference(source.GetRowSpan(sourceY)), 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); + Span row = currentRows[y]; + + ref byte s = ref Unsafe.As(ref row[sourceX]); ref byte d = ref Unsafe.Add(ref blockStart, y * blockRowSizeInBytes); Unsafe.CopyBlock(ref d, ref s, byteWidth); @@ -127,4 +125,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// public Span AsSpanUnsafe() => new Span(Unsafe.AsPointer(ref this), Size); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs b/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs new file mode 100644 index 000000000..57a134703 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components +{ + /// + /// Cache 8 pixel rows on the stack, which may originate from different buffers of a . + /// + [StructLayout(LayoutKind.Sequential)] + internal readonly ref struct RowOctet + where T : struct + { + private readonly Span row0; + private readonly Span row1; + private readonly Span row2; + private readonly Span row3; + private readonly Span row4; + private readonly Span row5; + private readonly Span row6; + private readonly Span row7; + + public RowOctet(Buffer2D buffer, int startY) + { + int y = startY; + int height = buffer.Height; + this.row0 = y < height ? buffer.GetRowSpan(y++) : default; + this.row1 = y < height ? buffer.GetRowSpan(y++) : default; + this.row2 = y < height ? buffer.GetRowSpan(y++) : default; + this.row3 = y < height ? buffer.GetRowSpan(y++) : default; + this.row4 = y < height ? buffer.GetRowSpan(y++) : default; + this.row5 = y < height ? buffer.GetRowSpan(y++) : default; + this.row6 = y < height ? buffer.GetRowSpan(y++) : default; + this.row7 = y < height ? buffer.GetRowSpan(y) : default; + } + + public Span this[int y] + { + get + { + // No unsafe tricks, since Span can't be used as a generic argument + return y switch + { + 0 => this.row0, + 1 => this.row1, + 2 => this.row2, + 3 => this.row3, + 4 => this.row4, + 5 => this.row5, + 6 => this.row6, + 7 => this.row7, + _ => throw new IndexOutOfRangeException() + }; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index cd3c19aa3..dcf2d72a5 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; @@ -409,12 +410,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; var pixelConverter = YCbCrForwardConverter.Create(); + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; for (int y = 0; y < pixels.Height; y += 8) { + var currentRows = new RowOctet(pixelBuffer, y); + for (int x = 0; x < pixels.Width; x += 8) { - pixelConverter.Convert(pixels.Frames.RootFrame, x, y); + pixelConverter.Convert(frame, x, y, currentRows); prevDCY = this.WriteBlock( QuantIndex.Luminance, @@ -935,6 +940,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // ReSharper disable once InconsistentNaming int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; for (int y = 0; y < pixels.Height; y += 16) { @@ -945,7 +952,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int xOff = (i & 1) * 8; int yOff = (i & 2) * 4; - pixelConverter.Convert(pixels.Frames.RootFrame, x + xOff, y + yOff); + // TODO: Try pushing this to the outer loop! + var currentRows = new RowOctet(pixelBuffer, y + yOff); + + pixelConverter.Convert(frame, x + xOff, y + yOff, currentRows); cbPtr[i] = pixelConverter.Cb; crPtr[i] = pixelConverter.Cr; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs b/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs index 7c42af596..38b33e842 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs @@ -41,7 +41,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (Image s = provider.GetImage()) { var d = default(GenericBlock8x8); - d.LoadAndStretchEdges(s.Frames.RootFrame, 0, 0); + var rowOctet = new RowOctet(s.GetRootFramePixelBuffer(), 0); + d.LoadAndStretchEdges(s.Frames.RootFrame.PixelBuffer, 0, 0, rowOctet); TPixel a = s.Frames.RootFrame[0, 0]; TPixel b = d[0, 0]; @@ -65,7 +66,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (Image s = provider.GetImage()) { var d = default(GenericBlock8x8); - d.LoadAndStretchEdges(s.Frames.RootFrame, 6, 7); + var rowOctet = new RowOctet(s.GetRootFramePixelBuffer(), 7); + d.LoadAndStretchEdges(s.Frames.RootFrame.PixelBuffer, 6, 7, rowOctet); Assert.Equal(s[6, 7], d[0, 0]); Assert.Equal(s[6, 8], d[0, 1]); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 0000ef13f..49ef7f8f8 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -3,6 +3,7 @@ using System.IO; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -81,9 +82,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg where TPixel : struct, IPixel => TestJpegEncoderCore(provider, subsample, quality); [Theory] - [WithTestPatternImages(nameof(BitsPerPixel_Quality), 600, 400, PixelTypes.Rgba32)] - public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvider provider, JpegSubsample subsample, int quality) - where TPixel : struct, IPixel => TestJpegEncoderCore(provider, subsample, quality, true, ImageComparer.TolerantPercentage(0.1f)); + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, JpegSubsample.Ratio444)] + [WithTestPatternImages(587, 821, PixelTypes.Rgba32, JpegSubsample.Ratio444)] + [WithTestPatternImages(677, 683, PixelTypes.Bgra32, JpegSubsample.Ratio420)] + [WithSolidFilledImages(400, 400, "Red", PixelTypes.Bgr24, JpegSubsample.Ratio420)] + public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvider provider, JpegSubsample subsample) + where TPixel : struct, IPixel + { + ImageComparer comparer = subsample == JpegSubsample.Ratio444 + ? ImageComparer.TolerantPercentage(0.1f) + : ImageComparer.TolerantPercentage(5f); + + provider.LimitAllocatorBufferCapacity(); + TestJpegEncoderCore(provider, subsample, 100, comparer); + } /// /// Anton's SUPER-SCIENTIFIC tolerance threshold calculation @@ -112,15 +124,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImageProvider provider, JpegSubsample subsample, int quality = 100, - bool enforceDiscontiguousBuffers = false, ImageComparer comparer = null) where TPixel : struct, IPixel { - if (enforceDiscontiguousBuffers) - { - provider.LimitAllocatorBufferCapacity(); - } - using Image image = provider.GetImage(); // There is no alpha in Jpeg! @@ -132,10 +138,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Quality = quality }; string info = $"{subsample}-Q{quality}"; - if (enforceDiscontiguousBuffers) - { - info += "-Disco"; - } comparer ??= GetComparer(quality, subsample); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs index 85506a9de..179680e1a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using Xunit.Abstractions; @@ -53,7 +54,7 @@ namespace SixLabors.ImageSharp.Tests Image image = base.GetImage(); Color color = new Rgba32(this.r, this.g, this.b, this.a); - image.GetPixelSpan().Fill(color.ToPixel()); + image.GetRootFramePixelBuffer().MemoryGroup.Fill(color.ToPixel()); return image; } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 58afd48a7..e492efb25 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -3,12 +3,14 @@ using System; using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using ImageMagick; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs @@ -17,45 +19,64 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs { public static MagickReferenceDecoder Instance { get; } = new MagickReferenceDecoder(); + private static void FromRgba32Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) + where TPixel : struct, IPixel + { + foreach (Memory m in destinationGroup) + { + Span destBuffer = m.Span; + PixelOperations.Instance.FromRgba32Bytes( + configuration, + rgbaBytes, + destBuffer, + destBuffer.Length); + rgbaBytes = rgbaBytes.Slice(destBuffer.Length * 4); + } + } + + private static void FromRgba64Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) + where TPixel : struct, IPixel + { + foreach (Memory m in destinationGroup) + { + Span destBuffer = m.Span; + PixelOperations.Instance.FromRgba64Bytes( + configuration, + rgbaBytes, + destBuffer, + destBuffer.Length); + rgbaBytes = rgbaBytes.Slice(destBuffer.Length * 8); + } + } + public Image Decode(Configuration configuration, Stream stream) where TPixel : struct, IPixel { - using (var magickImage = new MagickImage(stream)) + using var magickImage = new MagickImage(stream); + var result = new Image(configuration, magickImage.Width, magickImage.Height); + MemoryGroup resultPixels = result.GetRootFramePixelBuffer().MemoryGroup; + + using (IPixelCollection pixels = magickImage.GetPixelsUnsafe()) { - var result = new Image(configuration, magickImage.Width, magickImage.Height); - Span resultPixels = result.GetPixelSpan(); + if (magickImage.Depth == 8) + { + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - using (IPixelCollection pixels = magickImage.GetPixelsUnsafe()) + FromRgba32Bytes(configuration, data, resultPixels); + } + else if (magickImage.Depth == 16) { - if (magickImage.Depth == 8) - { - byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - - PixelOperations.Instance.FromRgba32Bytes( - configuration, - data, - resultPixels, - resultPixels.Length); - } - else if (magickImage.Depth == 16) - { - ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); - Span bytes = MemoryMarshal.Cast(data.AsSpan()); - - PixelOperations.Instance.FromRgba64Bytes( - configuration, - bytes, - resultPixels, - resultPixels.Length); - } - else - { - throw new InvalidOperationException(); - } + ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); + Span bytes = MemoryMarshal.Cast(data.AsSpan()); + FromRgba64Bytes(configuration, bytes, resultPixels); + } + else + { + throw new InvalidOperationException(); } - - return result; } + + return result; } public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index d4c2dc307..fa5eab20a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -657,12 +657,12 @@ namespace SixLabors.ImageSharp.Tests testOutputDetails, appendPixelTypeToFileName); - referenceDecoder = referenceDecoder ?? TestEnvironment.GetReferenceDecoder(actualOutputFile); + referenceDecoder ??= TestEnvironment.GetReferenceDecoder(actualOutputFile); - using (var actualImage = Image.Load(actualOutputFile, referenceDecoder)) + using (var encodedImage = Image.Load(actualOutputFile, referenceDecoder)) { ImageComparer comparer = customComparer ?? ImageComparer.Exact; - comparer.VerifySimilarity(actualImage, image); + comparer.VerifySimilarity(encodedImage, image); } }