diff --git a/src/ImageSharp.Formats.Jpeg/Components/Block8x8F.cs b/src/ImageSharp.Formats.Jpeg/Components/Block8x8F.cs index 723ccd6b8..e21ba2d02 100644 --- a/src/ImageSharp.Formats.Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp.Formats.Jpeg/Components/Block8x8F.cs @@ -333,18 +333,19 @@ namespace ImageSharp.Formats.Jpg /// Destination block /// Quantization table /// Pointer to elements - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void UnZigDivRound(Block8x8F* src, Block8x8F* dest, Block8x8F* qt, int* unzigPtr) + // [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void UnZigDivRound(Block8x8F* src, int* dest, Block8x8F* qt, int* unzigPtr) { float* s = (float*)src; - float* d = (float*)dest; float* q = (float*)qt; for (int zig = 0; zig < ScalarCount; zig++) { - float val = s[unzigPtr[zig]] / q[zig]; - val = (int)val; - d[zig] = val; + int a = (int)s[unzigPtr[zig]]; + int b = (int)q[zig]; + + int val = DivideRound(a, b); + dest[zig] = val; } } @@ -373,5 +374,22 @@ namespace ImageSharp.Formats.Jpg } } } + + /// + /// Performs division and rounding of a rational number represented by a dividend and a divisior into an integer. + /// + /// The dividend + /// The divisor + /// The result integer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DivideRound(int dividend, int divisor) + { + if (dividend >= 0) + { + return (dividend + (divisor >> 1)) / divisor; + } + + return -((-dividend + (divisor >> 1)) / divisor); + } } } \ No newline at end of file diff --git a/src/ImageSharp.Formats.Jpeg/JpegEncoderCore.cs b/src/ImageSharp.Formats.Jpeg/JpegEncoderCore.cs index 59dc5ce39..b74d622ce 100644 --- a/src/ImageSharp.Formats.Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp.Formats.Jpeg/JpegEncoderCore.cs @@ -434,7 +434,9 @@ namespace ImageSharp.Formats UnzigData unzig = UnzigData.Create(); // ReSharper disable once InconsistentNaming - float prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + + int* unzigDest = stackalloc int[Block8x8F.ScalarCount]; using (PixelArea rgbBytes = new PixelArea(8, 8, ComponentOrder.Xyz)) { @@ -449,6 +451,7 @@ namespace ImageSharp.Formats prevDCY, &b, &temp1, + unzigDest, &temp2, &onStackLuminanceQuantTable, unzig.Data); @@ -457,6 +460,7 @@ namespace ImageSharp.Formats prevDCCb, &cb, &temp1, + unzigDest, &temp2, &onStackChrominanceQuantTable, unzig.Data); @@ -465,6 +469,7 @@ namespace ImageSharp.Formats prevDCCr, &cr, &temp1, + unzigDest, &temp2, &onStackChrominanceQuantTable, unzig.Data); @@ -522,31 +527,31 @@ namespace ImageSharp.Formats /// The previous DC value. /// Source block /// Temporal block to be used as FDCT Destination - /// Temporal block 2 + /// Working buffer for unzigged stuff + /// Temporal block 2 /// Quantization table /// The 8x8 Unzig block pointer /// /// The /// - private float WriteBlock( + private int WriteBlock( QuantIndex index, - float prevDC, + int prevDC, Block8x8F* src, Block8x8F* tempDest, - Block8x8F* temp2, + int* d, + Block8x8F* tempWorker, Block8x8F* quant, int* unzigPtr) { - DCT.TransformFDCT(ref *src, ref *tempDest, ref *temp2); - - Block8x8F.UnZigDivRound(tempDest, temp2, quant, unzigPtr); + DCT.TransformFDCT(ref *src, ref *tempDest, ref *tempWorker); - float* d = (float*)temp2; + Block8x8F.UnZigDivRound(tempDest, d, quant, unzigPtr); // Emit the DC delta. - float dc = d[0]; + int dc = d[0]; - this.EmitHuffRLE((HuffIndex)((2 * (int)index) + 0), 0, (int)(dc - prevDC)); + this.EmitHuffRLE((HuffIndex)((2 * (int)index) + 0), 0, dc - prevDC); // Emit the AC components. HuffIndex h = (HuffIndex)((2 * (int)index) + 1); @@ -554,7 +559,7 @@ namespace ImageSharp.Formats for (int zig = 1; zig < Block8x8F.ScalarCount; zig++) { - float ac = d[zig]; + int ac = d[zig]; if (ac == 0) { @@ -568,7 +573,7 @@ namespace ImageSharp.Formats runLength -= 16; } - this.EmitHuffRLE(h, runLength, (int)ac); + this.EmitHuffRLE(h, runLength, ac); runLength = 0; } } @@ -802,8 +807,10 @@ namespace ImageSharp.Formats UnzigData unzig = UnzigData.Create(); + int* unzigDest = stackalloc int[Block8x8F.ScalarCount]; + // ReSharper disable once InconsistentNaming - float prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; using (PixelArea rgbBytes = new PixelArea(8, 8, ComponentOrder.Xyz)) { @@ -823,6 +830,7 @@ namespace ImageSharp.Formats prevDCY, &b, &temp1, + unzigDest, &temp2, &onStackLuminanceQuantTable, unzig.Data); @@ -834,6 +842,7 @@ namespace ImageSharp.Formats prevDCCb, &b, &temp1, + unzigDest, &temp2, &onStackChrominanceQuantTable, unzig.Data); @@ -844,6 +853,7 @@ namespace ImageSharp.Formats prevDCCr, &b, &temp1, + unzigDest, &temp2, &onStackChrominanceQuantTable, unzig.Data); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs index 53c44d836..c81165ffc 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegTests.cs @@ -17,6 +17,7 @@ namespace ImageSharp.Tests using System.Numerics; using ImageSharp.Formats.Jpg; + using ImageSharp.Processing; public class JpegTests : MeasureFixture { @@ -25,6 +26,63 @@ namespace ImageSharp.Tests { } + [Theory] + [WithFile(TestImages.Jpeg.Snake, PixelTypes.StandardImageClass, 75, JpegSubsample.Ratio420)] + [WithFile(TestImages.Jpeg.Lake, PixelTypes.StandardImageClass, 75, JpegSubsample.Ratio420)] + [WithFile(TestImages.Jpeg.Snake, PixelTypes.StandardImageClass, 75, JpegSubsample.Ratio444)] + [WithFile(TestImages.Jpeg.Lake, PixelTypes.StandardImageClass, 75, JpegSubsample.Ratio444)] + public void LoadResizeSave(TestImageProvider provider, int quality, JpegSubsample subsample) + where TColor : struct, IPackedPixel, IEquatable + { + var image = provider.GetImage() + .Resize(new ResizeOptions + { + Size = new Size(150, 150), + Mode = ResizeMode.Max + }); + image.Quality = 75; + JpegEncoder encoder = new JpegEncoder { Subsample = subsample, Quality = quality }; + + provider.Utility.TestName += $"{subsample}_Q{quality}"; + provider.Utility.SaveTestOutputFile(image, "png"); + provider.Utility.SaveTestOutputFile(image, "jpg", encoder); + } + + // Benchmark, enable manually! + // [Theory] + [InlineData(1, 75, JpegSubsample.Ratio420)] + [InlineData(30, 75, JpegSubsample.Ratio420)] + [InlineData(30, 75, JpegSubsample.Ratio444)] + [InlineData(30, 100, JpegSubsample.Ratio444)] + public void Encoder_Benchmark(int executionCount, int quality, JpegSubsample subsample) + { + string[] testFiles = TestImages.Bmp.All + .Concat(new[] { TestImages.Jpeg.Calliphora, TestImages.Jpeg.Cmyk }) + .ToArray(); + + var testImages = + testFiles.Select( + tf => TestImageProvider.File(tf, pixelTypeOverride: PixelTypes.StandardImageClass).GetImage()) + .ToArray(); + + using (MemoryStream ms = new MemoryStream()) + { + this.Measure(executionCount, + () => + { + foreach (Image img in testImages) + { + JpegEncoder encoder = new JpegEncoder() { Quality = quality, Subsample = subsample }; + img.Save(ms, encoder); + ms.Seek(0, SeekOrigin.Begin); + } + }, + // ReSharper disable once ExplicitCallerInfoArgument + $@"Encode {testFiles.Length} images" + ); + } + } + public static IEnumerable AllJpegFiles => TestImages.Jpeg.All; [Theory] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 0ce8fb05a..89b3c0f0d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -51,6 +51,9 @@ namespace ImageSharp.Tests public const string Festzug = "Jpg/Festzug.jpg"; public const string Hiyamugi = "Jpg/Hiyamugi.jpg"; + public const string Snake = "Jpg/Snake.jpg"; + public const string Lake = "Jpg/Lake.jpg"; + public const string Jpeg400 = "Jpg/baseline/jpeg400jfif.jpg"; public const string Jpeg420 = "Jpg/baseline/jpeg420exif.jpg"; public const string Jpeg422 = "Jpg/baseline/jpeg422jfif.jpg"; diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/Lake.jpg b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/Lake.jpg new file mode 100644 index 000000000..c54f2fd88 Binary files /dev/null and b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/Lake.jpg differ diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Jpg/Snake.jpg b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/Snake.jpg new file mode 100644 index 000000000..222754844 Binary files /dev/null and b/tests/ImageSharp.Tests/TestImages/Formats/Jpg/Snake.jpg differ diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index ea3645874..8eb658073 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -74,16 +74,19 @@ namespace ImageSharp.Tests /// The pixel format of the image /// The image instance /// The requested extension - public void SaveTestOutputFile(Image image, string extension = null) + /// Optional encoder + public void SaveTestOutputFile(Image image, string extension = null, IImageEncoder encoder = null) where TColor : struct, IPackedPixel, IEquatable { string path = this.GetTestOutputFileName(extension); var format = GetImageFormatByExtension(extension); + encoder = encoder ?? format.Encoder; + using (var stream = File.OpenWrite(path)) { - image.Save(stream, format); + image.Save(stream, encoder); } }