From dd3c3ec601dd7a9f5ca1399ee5258626783b346e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Mon, 9 May 2022 18:06:04 +0300 Subject: [PATCH] Fixed sampling factors (hopefully) --- .../Jpeg/Components/Block8x8F.ScaledCopy.cs | 162 ++++++++++++++++ .../Jpeg/Components/Block8x8F.ScaledCopyTo.cs | 179 ------------------ .../Components/Encoder/HuffmanScanEncoder.cs | 1 - .../Encoder/JpegComponentPostProcessor.cs | 80 ++++++-- .../Jpeg/Components/Encoder/JpegFrame.cs | 36 ++-- .../Formats/Jpeg/Components/Quantization.cs | 9 +- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 7 + .../Formats/Jpeg/JpegEncoderCore.cs | 19 +- .../Jpg/Block8x8FTests.CopyToBufferArea.cs | 97 ---------- 9 files changed, 270 insertions(+), 320 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs delete mode 100644 src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs delete mode 100644 tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs new file mode 100644 index 0000000000..1ba4dc6b0f --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs @@ -0,0 +1,162 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +// ReSharper disable UseObjectOrCollectionInitializer +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Jpeg.Components +{ + public partial struct Block8x8F + { + [MethodImpl(InliningOptions.ShortMethod)] + public void ScaledCopyFrom(ref float areaOrigin, int areaStride) => + CopyFrom1x1Scale(ref Unsafe.As(ref areaOrigin), ref Unsafe.As(ref this), areaStride); + + [MethodImpl(InliningOptions.ShortMethod)] + public void ScaledCopyTo(ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) + { + if (horizontalScale == 1 && verticalScale == 1) + { + CopyTo1x1Scale(ref Unsafe.As(ref this), ref Unsafe.As(ref areaOrigin), areaStride); + return; + } + + if (horizontalScale == 2 && verticalScale == 2) + { + this.CopyTo2x2Scale(ref areaOrigin, areaStride); + return; + } + + // TODO: Optimize: implement all cases with scale-specific, loopless code! + this.CopyArbitraryScale(ref areaOrigin, areaStride, horizontalScale, verticalScale); + } + + private void CopyTo2x2Scale(ref float areaOrigin, int areaStride) + { + ref Vector2 destBase = ref Unsafe.As(ref areaOrigin); + int destStride = areaStride / 2; + + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 0, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 1, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 2, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 3, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 4, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 5, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 6, destStride); + WidenCopyRowImpl2x2(ref this.V0L, ref destBase, 7, destStride); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void WidenCopyRowImpl2x2(ref Vector4 selfBase, ref Vector2 destBase, int row, int destStride) + { + ref Vector4 sLeft = ref Unsafe.Add(ref selfBase, 2 * row); + ref Vector4 sRight = ref Unsafe.Add(ref sLeft, 1); + + int offset = 2 * row * destStride; + ref Vector4 dTopLeft = ref Unsafe.As(ref Unsafe.Add(ref destBase, offset)); + ref Vector4 dBottomLeft = ref Unsafe.As(ref Unsafe.Add(ref destBase, offset + destStride)); + + var xyLeft = new Vector4(sLeft.X); + xyLeft.Z = sLeft.Y; + xyLeft.W = sLeft.Y; + + var zwLeft = new Vector4(sLeft.Z); + zwLeft.Z = sLeft.W; + zwLeft.W = sLeft.W; + + var xyRight = new Vector4(sRight.X); + xyRight.Z = sRight.Y; + xyRight.W = sRight.Y; + + var zwRight = new Vector4(sRight.Z); + zwRight.Z = sRight.W; + zwRight.W = sRight.W; + + dTopLeft = xyLeft; + Unsafe.Add(ref dTopLeft, 1) = zwLeft; + Unsafe.Add(ref dTopLeft, 2) = xyRight; + Unsafe.Add(ref dTopLeft, 3) = zwRight; + + dBottomLeft = xyLeft; + Unsafe.Add(ref dBottomLeft, 1) = zwLeft; + Unsafe.Add(ref dBottomLeft, 2) = xyRight; + Unsafe.Add(ref dBottomLeft, 3) = zwRight; + } + } + + [MethodImpl(InliningOptions.ColdPath)] + private void CopyArbitraryScale(ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) + { + for (int y = 0; y < 8; y++) + { + int yy = y * verticalScale; + int y8 = y * 8; + + for (int x = 0; x < 8; x++) + { + int xx = x * horizontalScale; + + float value = this[y8 + x]; + + for (int i = 0; i < verticalScale; i++) + { + int baseIdx = ((yy + i) * areaStride) + xx; + + for (int j = 0; j < horizontalScale; j++) + { + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, baseIdx + j) = value; + } + } + } + } + } + + private static void CopyTo1x1Scale(ref byte origin, ref byte dest, int areaStride) + { + int destStride = areaStride * sizeof(float); + + CopyRowImpl(ref origin, ref dest, destStride, 0); + CopyRowImpl(ref origin, ref dest, destStride, 1); + CopyRowImpl(ref origin, ref dest, destStride, 2); + CopyRowImpl(ref origin, ref dest, destStride, 3); + CopyRowImpl(ref origin, ref dest, destStride, 4); + CopyRowImpl(ref origin, ref dest, destStride, 5); + CopyRowImpl(ref origin, ref dest, destStride, 6); + CopyRowImpl(ref origin, ref dest, destStride, 7); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void CopyRowImpl(ref byte origin, ref byte dest, int destStride, int row) + { + origin = ref Unsafe.Add(ref origin, row * 8 * sizeof(float)); + dest = ref Unsafe.Add(ref dest, row * destStride); + Unsafe.CopyBlock(ref dest, ref origin, 8 * sizeof(float)); + } + } + + private static void CopyFrom1x1Scale(ref byte origin, ref byte dest, int areaStride) + { + int destStride = areaStride * sizeof(float); + + CopyRowImpl(ref origin, ref dest, destStride, 0); + CopyRowImpl(ref origin, ref dest, destStride, 1); + CopyRowImpl(ref origin, ref dest, destStride, 2); + CopyRowImpl(ref origin, ref dest, destStride, 3); + CopyRowImpl(ref origin, ref dest, destStride, 4); + CopyRowImpl(ref origin, ref dest, destStride, 5); + CopyRowImpl(ref origin, ref dest, destStride, 6); + CopyRowImpl(ref origin, ref dest, destStride, 7); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void CopyRowImpl(ref byte origin, ref byte dest, int sourceStride, int row) + { + origin = ref Unsafe.Add(ref origin, row * sourceStride); + dest = ref Unsafe.Add(ref dest, row * 8 * sizeof(float)); + Unsafe.CopyBlock(ref dest, ref origin, 8 * sizeof(float)); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs deleted file mode 100644 index fa9aa24498..0000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; - -// ReSharper disable UseObjectOrCollectionInitializer -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Formats.Jpeg.Components -{ - public partial struct Block8x8F - { - /// - /// Copy block data into the destination color buffer pixel area with the provided horizontal and vertical scale factors. - /// - [MethodImpl(InliningOptions.ShortMethod)] - public void ScaledCopyTo(in Buffer2DRegion region, int horizontalScale, int verticalScale) - { - ref float areaOrigin = ref region.GetReferenceToOrigin(); - this.ScaledCopyTo(ref areaOrigin, region.Stride, horizontalScale, verticalScale); - } - - [MethodImpl(InliningOptions.ShortMethod)] - public void ScaledCopyFrom(ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) - { - if (horizontalScale == 1 && verticalScale == 1) - { - ref byte selfBase = ref Unsafe.As(ref this); - ref byte destBase = ref Unsafe.As(ref areaOrigin); - int destStride = areaStride * sizeof(float); - - CopyRowImplFromStrides(ref selfBase, ref destBase, destStride, 0); - CopyRowImplFromStrides(ref selfBase, ref destBase, destStride, 1); - CopyRowImplFromStrides(ref selfBase, ref destBase, destStride, 2); - CopyRowImplFromStrides(ref selfBase, ref destBase, destStride, 3); - CopyRowImplFromStrides(ref selfBase, ref destBase, destStride, 4); - CopyRowImplFromStrides(ref selfBase, ref destBase, destStride, 5); - CopyRowImplFromStrides(ref selfBase, ref destBase, destStride, 6); - CopyRowImplFromStrides(ref selfBase, ref destBase, destStride, 7); - return; - } - - throw new NotImplementedException("This is a test setup!"); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void CopyRowImplFromStrides(ref byte src, ref byte dst, int srcStride, int row) - { - ref byte s = ref Unsafe.Add(ref dst, row * srcStride); - ref byte d = ref Unsafe.Add(ref src, row * 8 * sizeof(float)); - Unsafe.CopyBlock(ref d, ref s, 8 * sizeof(float)); - } - } - - [MethodImpl(InliningOptions.ShortMethod)] - public void ScaledCopyTo(ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) - { - if (horizontalScale == 1 && verticalScale == 1) - { - this.Copy1x1Scale(ref areaOrigin, areaStride); - return; - } - - if (horizontalScale == 2 && verticalScale == 2) - { - this.Copy2x2Scale(ref areaOrigin, areaStride); - return; - } - - // TODO: Optimize: implement all cases with scale-specific, loopless code! - this.CopyArbitraryScale(ref areaOrigin, areaStride, horizontalScale, verticalScale); - } - - public void Copy1x1Scale(ref float areaOrigin, int areaStride) - { - ref byte selfBase = ref Unsafe.As(ref this); - ref byte destBase = ref Unsafe.As(ref areaOrigin); - int destStride = areaStride * 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); - } - - [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)); - } - - private void Copy2x2Scale(ref float areaOrigin, int areaStride) - { - ref Vector2 destBase = ref Unsafe.As(ref areaOrigin); - int destStride = areaStride / 2; - - this.WidenCopyRowImpl2x2(ref destBase, 0, destStride); - this.WidenCopyRowImpl2x2(ref destBase, 1, destStride); - this.WidenCopyRowImpl2x2(ref destBase, 2, destStride); - this.WidenCopyRowImpl2x2(ref destBase, 3, destStride); - this.WidenCopyRowImpl2x2(ref destBase, 4, destStride); - this.WidenCopyRowImpl2x2(ref destBase, 5, destStride); - this.WidenCopyRowImpl2x2(ref destBase, 6, destStride); - this.WidenCopyRowImpl2x2(ref destBase, 7, destStride); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WidenCopyRowImpl2x2(ref Vector2 destBase, int row, int destStride) - { - ref Vector4 sLeft = ref Unsafe.Add(ref this.V0L, 2 * row); - ref Vector4 sRight = ref Unsafe.Add(ref sLeft, 1); - - int offset = 2 * row * destStride; - ref Vector4 dTopLeft = ref Unsafe.As(ref Unsafe.Add(ref destBase, offset)); - ref Vector4 dBottomLeft = ref Unsafe.As(ref Unsafe.Add(ref destBase, offset + destStride)); - - var xyLeft = new Vector4(sLeft.X); - xyLeft.Z = sLeft.Y; - xyLeft.W = sLeft.Y; - - var zwLeft = new Vector4(sLeft.Z); - zwLeft.Z = sLeft.W; - zwLeft.W = sLeft.W; - - var xyRight = new Vector4(sRight.X); - xyRight.Z = sRight.Y; - xyRight.W = sRight.Y; - - var zwRight = new Vector4(sRight.Z); - zwRight.Z = sRight.W; - zwRight.W = sRight.W; - - dTopLeft = xyLeft; - Unsafe.Add(ref dTopLeft, 1) = zwLeft; - Unsafe.Add(ref dTopLeft, 2) = xyRight; - Unsafe.Add(ref dTopLeft, 3) = zwRight; - - dBottomLeft = xyLeft; - Unsafe.Add(ref dBottomLeft, 1) = zwLeft; - Unsafe.Add(ref dBottomLeft, 2) = xyRight; - Unsafe.Add(ref dBottomLeft, 3) = zwRight; - } - - [MethodImpl(InliningOptions.ColdPath)] - private void CopyArbitraryScale(ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) - { - for (int y = 0; y < 8; y++) - { - int yy = y * verticalScale; - int y8 = y * 8; - - for (int x = 0; x < 8; x++) - { - int xx = x * horizontalScale; - - float value = this[y8 + x]; - - for (int i = 0; i < verticalScale; i++) - { - int baseIdx = ((yy + i) * areaStride) + xx; - - for (int j = 0; j < horizontalScale; j++) - { - // area[xx + j, yy + i] = value; - Unsafe.Add(ref areaOrigin, baseIdx + j) = value; - } - } - } - } - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 3d61967ef3..4c1071a286 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -142,7 +142,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder where TPixel : unmanaged, IPixel { // DEBUG INITIALIZATION SETUP - frame.Init(1, 1); frame.AllocateComponents(fullScan: false); var spectralConverter = new SpectralConverter(configuration); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs index ad7bb5f0ff..9ef54d83f2 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs @@ -21,11 +21,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder FastFloatingPointDCT.AdjustToFDCT(ref this.quantTable); this.component = component; - this.blockAreaSize = this.component.SubSamplingDivisors * 8; + this.blockAreaSize = component.SubSamplingDivisors * 8; this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( postProcessorBufferSize.Width, postProcessorBufferSize.Height, - this.blockAreaSize.Height, + 8 * component.SubSamplingDivisors.Height, AllocationOptions.Clean); } @@ -42,32 +42,28 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder // but 12-bit jpegs are not supported currently float normalizationValue = -128f; - int blocksRowsPerStep = this.component.SamplingFactors.Height; + int destAreaStride = this.ColorBuffer.Width * this.component.SubSamplingDivisors.Height; - int destAreaStride = this.ColorBuffer.Width; - - int yBlockStart = spectralStep * blocksRowsPerStep; - - Size subSamplingDivisors = this.component.SubSamplingDivisors; + int yBlockStart = spectralStep * this.component.SamplingFactors.Height; Block8x8F workspaceBlock = default; - for (int y = 0; y < blocksRowsPerStep; y++) + // handle subsampling + this.PackColorBuffer(); + + for (int y = 0; y < spectralBuffer.Height; y++) { int yBuffer = y * this.blockAreaSize.Height; - for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) { Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); // load 8x8 block from 8 pixel strides - int xColorBufferStart = xBlock * this.blockAreaSize.Width; + int xColorBufferStart = xBlock * 8; workspaceBlock.ScaledCopyFrom( ref colorBufferRow[xColorBufferStart], - destAreaStride, - subSamplingDivisors.Width, - subSamplingDivisors.Height); + destAreaStride); // level shift via -128f workspaceBlock.AddInPlace(normalizationValue); @@ -86,5 +82,61 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public void Dispose() => this.ColorBuffer.Dispose(); + + private void PackColorBuffer() + { + Size factors = this.component.SubSamplingDivisors; + + if (factors.Width == 1 && factors.Height == 1) + { + return; + } + + for (int i = 0; i < this.ColorBuffer.Height; i += factors.Height) + { + Span targetBufferRow = this.ColorBuffer.DangerousGetRowSpan(i); + + // vertical sum + for (int j = 1; j < factors.Height; j++) + { + SumVertical(targetBufferRow, this.ColorBuffer.DangerousGetRowSpan(i + j)); + } + + // horizontal sum + SumHorizontal(targetBufferRow, factors.Width); + + // calculate average + float multiplier = 1f / (factors.Width * factors.Height); + MultiplyToAverage(targetBufferRow, multiplier); + } + + static void SumVertical(Span target, Span source) + { + for (int i = 0; i < target.Length; i++) + { + target[i] += source[i]; + } + } + + static void SumHorizontal(Span target, int factor) + { + for (int i = 0; i < target.Length / factor; i++) + { + target[i] = target[i * factor]; + for (int j = 1; j < factor; j++) + { + target[i] += target[(i * factor) + j]; + } + } + } + + static void MultiplyToAverage(Span target, float multiplier) + { + for (int i = 0; i < target.Length; i++) + { + target[i] *= multiplier; + } + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs index 4213e7e434..675cdaca6b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs @@ -18,17 +18,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder this.PixelWidth = image.Width; this.PixelHeight = image.Height; - // int componentCount = 3; - var componentConfigs = frameConfig.Components; + JpegComponentConfig[] componentConfigs = frameConfig.Components; this.Components = new JpegComponent[componentConfigs.Length]; for (int i = 0; i < this.Components.Length; i++) { - var componentConfig = componentConfigs[i]; + JpegComponentConfig componentConfig = componentConfigs[i]; this.Components[i] = new JpegComponent(allocator, componentConfig.HorizontalSampleFactor, componentConfig.VerticalSampleFactor, componentConfig.QuantizatioTableIndex) { DcTableId = componentConfig.dcTableSelector, AcTableId = componentConfig.acTableSelector, }; + + this.BlocksPerMcu += componentConfig.HorizontalSampleFactor * componentConfig.VerticalSampleFactor; + } + + int maxSubFactorH = frameConfig.MaxHorizontalSamplingFactor; + int maxSubFactorV = frameConfig.MaxVerticalSamplingFactor; + this.McusPerLine = (int)Numerics.DivideCeil((uint)image.Width, (uint)maxSubFactorH * 8); + this.McusPerColumn = (int)Numerics.DivideCeil((uint)image.Height, (uint)maxSubFactorV * 8); + + for (int i = 0; i < this.ComponentCount; i++) + { + JpegComponent component = this.Components[i]; + component.Init(this, maxSubFactorH, maxSubFactorV); } } @@ -42,9 +54,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public JpegComponent[] Components { get; } - public int McusPerLine { get; set; } + public int McusPerLine { get; } + + public int McusPerColumn { get; } - public int McusPerColumn { get; set; } + public int BlocksPerMcu { get; } public void Dispose() { @@ -54,18 +68,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder } } - public void Init(int maxSubFactorH, int maxSubFactorV) - { - this.McusPerLine = (int)Numerics.DivideCeil((uint)this.PixelWidth, (uint)maxSubFactorH * 8); - this.McusPerColumn = (int)Numerics.DivideCeil((uint)this.PixelHeight, (uint)maxSubFactorV * 8); - - for (int i = 0; i < this.ComponentCount; i++) - { - JpegComponent component = this.Components[i]; - component.Init(this, maxSubFactorH, maxSubFactorV); - } - } - public void AllocateComponents(bool fullScan) { for (int i = 0; i < this.ComponentCount; i++) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index b85503ee15..a5e823ddbd 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -188,13 +188,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components return table; } - public static Block8x8F ScaleQuantizationTable(int scale, Block8x8 unscaledTable) + public static Block8x8 ScaleQuantizationTable(int quality, Block8x8 unscaledTable) { - Block8x8F table = default; - for (int j = 0; j < Block8x8F.Size; j++) + int scale = QualityToScale(quality); + Block8x8 table = default; + for (int j = 0; j < Block8x8.Size; j++) { int x = ((unscaledTable[j] * scale) + 50) / 100; - table[j] = Numerics.Clamp(x, 1, 255); + table[j] = (short)(uint)Numerics.Clamp(x, 1, 255); } return table; diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 371f543cf9..5e1bde3097 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -89,6 +89,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public JpegComponentConfig[] Components { get; } + public int MaxHorizontalSamplingFactor { get; set; } = 1; + + public int MaxVerticalSamplingFactor { get; set; } = 1; + public JpegFrameConfig PopulateComponent(int index, byte id, int hsf, int vsf, int quantIndex, int dcIndex, int acIndex) { this.Components[index] = new JpegComponentConfig @@ -101,6 +105,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg acTableSelector = acIndex, }; + this.MaxHorizontalSamplingFactor = Math.Max(this.MaxHorizontalSamplingFactor, hsf); + this.MaxVerticalSamplingFactor = Math.Max(this.MaxVerticalSamplingFactor, vsf); + return this; } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 066e79bc0c..2adec881d0 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -91,7 +91,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg cancellationToken.ThrowIfCancellationRequested(); - this.scanEncoder = new HuffmanScanEncoder(3, stream); + var frame = new Components.Encoder.JpegFrame(this.frameConfig, Configuration.Default.MemoryAllocator, image, GetTargetColorSpace(this.frameConfig.ColorType)); + this.scanEncoder = new HuffmanScanEncoder(frame.BlocksPerMcu, stream); this.outputStream = stream; ImageMetadata metadata = image.Metadata; @@ -127,7 +128,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Write the scan header. this.WriteStartOfScan(this.frameConfig.Components.Length, this.frameConfig.Components); - var frame = new Components.Encoder.JpegFrame(this.frameConfig, Configuration.Default.MemoryAllocator, image, GetTargetColorSpace(this.frameConfig.ColorType)); this.scanEncoder.EncodeInterleavedScan(frame, image, this.QuantizationTables, Configuration.Default, cancellationToken); // Write the End Of Image marker. @@ -649,7 +649,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Sampling factors // 4 bits - int samplingFactors = components[i].HorizontalSampleFactor | (components[i].VerticalSampleFactor << 4); + int samplingFactors = (components[i].HorizontalSampleFactor << 4) | components[i].VerticalSampleFactor; bufferSpan[1] = (byte)samplingFactors; // Id @@ -748,7 +748,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Output chrominance quantization table. private void InitQuantizationTables(JpegQuantizationTableConfig[] configs, JpegMetadata metadata) { - int dataLen = configs.Length * (1 + Block8x8F.Size); + int dataLen = configs.Length * (1 + Block8x8.Size); // Marker + quantization table lengths. int markerlen = 2 + dataLen; @@ -761,16 +761,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { JpegQuantizationTableConfig config = configs[i]; + int quality = GetQualityForTable(config.DestinationIndex, this.quality, metadata); + Block8x8 scaledTable = Quantization.ScaleQuantizationTable(quality, config.Table); + // write to the output stream buffer[offset++] = (byte)config.DestinationIndex; - for (int j = 0; j < Block8x8F.Size; j++) + + for (int j = 0; j < Block8x8.Size; j++) { - buffer[offset++] = (byte)(uint)config.Table[ZigZag.ZigZagOrder[j]]; + buffer[offset++] = (byte)(uint)scaledTable[ZigZag.ZigZagOrder[j]]; } // apply scaling and save into buffer - int quality = GetQualityForTable(config.DestinationIndex, this.quality, metadata); - this.QuantizationTables[config.DestinationIndex] = Quantization.ScaleQuantizationTable(quality, config.Table); + this.QuantizationTables[config.DestinationIndex].LoadFromInt16Scalar(ref scaledTable); } // write filled buffer to the stream diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs deleted file mode 100644 index cba042fcbd..0000000000 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -// Uncomment this to turn unit tests into benchmarks: -// #define BENCHMARKING -using SixLabors.ImageSharp.Formats.Jpeg.Components; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; - -using Xunit; -using Xunit.Abstractions; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Tests.Formats.Jpg -{ - [Trait("Format", "Jpg")] - public partial class Block8x8FTests - { - 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 Copy1x1Scale() - { - Block8x8F block = CreateRandomFloatBlock(0, 100); - - using (Buffer2D buffer = Configuration.Default.MemoryAllocator.Allocate2D(20, 20, AllocationOptions.Clean)) - { - Buffer2DRegion region = buffer.GetRegion(5, 10, 8, 8); - block.Copy1x1Scale(ref region.GetReferenceToOrigin(), region.Stride); - - 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 CopyTo(int horizontalFactor, int verticalFactor) - { - Block8x8F block = CreateRandomFloatBlock(0, 100); - - var start = new Point(50, 50); - - using (Buffer2D buffer = Configuration.Default.MemoryAllocator.Allocate2D(100, 100, AllocationOptions.Clean)) - { - Buffer2DRegion region = buffer.GetRegion(start.X, start.Y, 8 * horizontalFactor, 8 * verticalFactor); - block.ScaledCopyTo(region, 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 = region[x, y]; - - Assert.Equal(expected, actual); - } - } - - VerifyAllZeroOutsideSubArea(buffer, start.X, start.Y, horizontalFactor, verticalFactor); - } - } - } - } -}