mirror of https://github.com/SixLabors/ImageSharp
32 changed files with 1272 additions and 488 deletions
@ -0,0 +1,65 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for processing component spectral data and converting it to raw color data.
|
|||
/// </summary>
|
|||
internal abstract class ComponentProcessor : IDisposable |
|||
{ |
|||
public ComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, Size postProcessorBufferSize, IJpegComponent component, int blockSize) |
|||
{ |
|||
this.Frame = frame; |
|||
this.Component = component; |
|||
|
|||
this.BlockAreaSize = component.SubSamplingDivisors * blockSize; |
|||
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>( |
|||
postProcessorBufferSize.Width, |
|||
postProcessorBufferSize.Height, |
|||
this.BlockAreaSize.Height); |
|||
} |
|||
|
|||
protected JpegFrame Frame { get; } |
|||
|
|||
protected IJpegComponent Component { get; } |
|||
|
|||
protected Buffer2D<float> ColorBuffer { get; } |
|||
|
|||
protected Size BlockAreaSize { get; } |
|||
|
|||
/// <summary>
|
|||
/// Converts spectral data to color data accessible via <see cref="GetColorBufferRowSpan(int)"/>.
|
|||
/// </summary>
|
|||
/// <param name="row">Spectral row index to convert.</param>
|
|||
public abstract void CopyBlocksToColorBuffer(int row); |
|||
|
|||
/// <summary>
|
|||
/// Clears spectral buffers.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Should only be called during baseline interleaved decoding.
|
|||
/// </remarks>
|
|||
public void ClearSpectralBuffers() |
|||
{ |
|||
Buffer2D<Block8x8> spectralBlocks = this.Component.SpectralBlocks; |
|||
for (int i = 0; i < spectralBlocks.Height; i++) |
|||
{ |
|||
spectralBlocks.DangerousGetRowSpan(i).Clear(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets converted color buffer row.
|
|||
/// </summary>
|
|||
/// <param name="row">Row index.</param>
|
|||
/// <returns>Color buffer row.</returns>
|
|||
public Span<float> GetColorBufferRowSpan(int row) => |
|||
this.ColorBuffer.DangerousGetRowSpan(row); |
|||
|
|||
public void Dispose() => this.ColorBuffer.Dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Processes component spectral data and converts it to color data in 1-to-1 scale.
|
|||
/// </summary>
|
|||
internal sealed class DirectComponentProcessor : ComponentProcessor |
|||
{ |
|||
private Block8x8F dequantizationTable; |
|||
|
|||
public DirectComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) |
|||
: base(memoryAllocator, frame, postProcessorBufferSize, component, blockSize: 8) |
|||
{ |
|||
this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; |
|||
FloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); |
|||
} |
|||
|
|||
public override void CopyBlocksToColorBuffer(int spectralStep) |
|||
{ |
|||
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks; |
|||
|
|||
float maximumValue = this.Frame.MaxColorChannelValue; |
|||
|
|||
int destAreaStride = this.ColorBuffer.Width; |
|||
|
|||
int blocksRowsPerStep = this.Component.SamplingFactors.Height; |
|||
|
|||
int yBlockStart = spectralStep * blocksRowsPerStep; |
|||
|
|||
Size subSamplingDivisors = this.Component.SubSamplingDivisors; |
|||
|
|||
Block8x8F workspaceBlock = default; |
|||
|
|||
for (int y = 0; y < blocksRowsPerStep; y++) |
|||
{ |
|||
int yBuffer = y * this.BlockAreaSize.Height; |
|||
|
|||
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); |
|||
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); |
|||
|
|||
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) |
|||
{ |
|||
// Integer to float
|
|||
workspaceBlock.LoadFrom(ref blockRow[xBlock]); |
|||
|
|||
// Dequantize
|
|||
workspaceBlock.MultiplyInPlace(ref this.dequantizationTable); |
|||
|
|||
// Convert from spectral to color
|
|||
FloatingPointDCT.TransformIDCT(ref workspaceBlock); |
|||
|
|||
// To conform better to libjpeg we actually NEED TO loose precision here.
|
|||
// This is because they store blocks as Int16 between all the operations.
|
|||
// To be "more accurate", we need to emulate this by rounding!
|
|||
workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue); |
|||
|
|||
// Write to color buffer acording to sampling factors
|
|||
int xColorBufferStart = xBlock * this.BlockAreaSize.Width; |
|||
workspaceBlock.ScaledCopyTo( |
|||
ref colorBufferRow[xColorBufferStart], |
|||
destAreaStride, |
|||
subSamplingDivisors.Width, |
|||
subSamplingDivisors.Height); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Processes component spectral data and converts it to color data in 2-to-1 scale.
|
|||
/// </summary>
|
|||
internal sealed class DownScalingComponentProcessor2 : ComponentProcessor |
|||
{ |
|||
private Block8x8F dequantizationTable; |
|||
|
|||
public DownScalingComponentProcessor2(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) |
|||
: base(memoryAllocator, frame, postProcessorBufferSize, component, 4) |
|||
{ |
|||
this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; |
|||
ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); |
|||
} |
|||
|
|||
public override void CopyBlocksToColorBuffer(int spectralStep) |
|||
{ |
|||
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks; |
|||
|
|||
float maximumValue = this.Frame.MaxColorChannelValue; |
|||
float normalizationValue = MathF.Ceiling(maximumValue / 2); |
|||
|
|||
int destAreaStride = this.ColorBuffer.Width; |
|||
|
|||
int blocksRowsPerStep = this.Component.SamplingFactors.Height; |
|||
Size subSamplingDivisors = this.Component.SubSamplingDivisors; |
|||
|
|||
Block8x8F workspaceBlock = default; |
|||
|
|||
int yBlockStart = spectralStep * blocksRowsPerStep; |
|||
|
|||
for (int y = 0; y < blocksRowsPerStep; y++) |
|||
{ |
|||
int yBuffer = y * this.BlockAreaSize.Height; |
|||
|
|||
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); |
|||
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); |
|||
|
|||
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) |
|||
{ |
|||
// Integer to float
|
|||
workspaceBlock.LoadFrom(ref blockRow[xBlock]); |
|||
|
|||
// IDCT/Normalization/Range
|
|||
ScaledFloatingPointDCT.TransformIDCT_4x4(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue); |
|||
|
|||
// Save to the intermediate buffer
|
|||
int xColorBufferStart = xBlock * this.BlockAreaSize.Width; |
|||
ScaledCopyTo( |
|||
ref workspaceBlock, |
|||
ref colorBufferRow[xColorBufferStart], |
|||
destAreaStride, |
|||
subSamplingDivisors.Width, |
|||
subSamplingDivisors.Height); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) |
|||
{ |
|||
// TODO: Optimize: implement all cases with scale-specific, loopless code!
|
|||
CopyArbitraryScale(ref block, ref destRef, destStrideWidth, horizontalScale, verticalScale); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) |
|||
{ |
|||
for (int y = 0; y < 4; y++) |
|||
{ |
|||
int yy = y * verticalScale; |
|||
int y8 = y * 8; |
|||
|
|||
for (int x = 0; x < 4; x++) |
|||
{ |
|||
int xx = x * horizontalScale; |
|||
|
|||
float value = block[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, (nint)(uint)(baseIdx + j)) = value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Processes component spectral data and converts it to color data in 4-to-1 scale.
|
|||
/// </summary>
|
|||
internal sealed class DownScalingComponentProcessor4 : ComponentProcessor |
|||
{ |
|||
private Block8x8F dequantizationTable; |
|||
|
|||
public DownScalingComponentProcessor4(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) |
|||
: base(memoryAllocator, frame, postProcessorBufferSize, component, 2) |
|||
{ |
|||
this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; |
|||
ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); |
|||
} |
|||
|
|||
public override void CopyBlocksToColorBuffer(int spectralStep) |
|||
{ |
|||
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks; |
|||
|
|||
float maximumValue = this.Frame.MaxColorChannelValue; |
|||
float normalizationValue = MathF.Ceiling(maximumValue / 2); |
|||
|
|||
int destAreaStride = this.ColorBuffer.Width; |
|||
|
|||
int blocksRowsPerStep = this.Component.SamplingFactors.Height; |
|||
Size subSamplingDivisors = this.Component.SubSamplingDivisors; |
|||
|
|||
Block8x8F workspaceBlock = default; |
|||
|
|||
int yBlockStart = spectralStep * blocksRowsPerStep; |
|||
|
|||
for (int y = 0; y < blocksRowsPerStep; y++) |
|||
{ |
|||
int yBuffer = y * this.BlockAreaSize.Height; |
|||
|
|||
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); |
|||
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); |
|||
|
|||
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) |
|||
{ |
|||
// Integer to float
|
|||
workspaceBlock.LoadFrom(ref blockRow[xBlock]); |
|||
|
|||
// IDCT/Normalization/Range
|
|||
ScaledFloatingPointDCT.TransformIDCT_2x2(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue); |
|||
|
|||
// Save to the intermediate buffer
|
|||
int xColorBufferStart = xBlock * this.BlockAreaSize.Width; |
|||
ScaledCopyTo( |
|||
ref workspaceBlock, |
|||
ref colorBufferRow[xColorBufferStart], |
|||
destAreaStride, |
|||
subSamplingDivisors.Width, |
|||
subSamplingDivisors.Height); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) |
|||
{ |
|||
// TODO: Optimize: implement all cases with scale-specific, loopless code!
|
|||
CopyArbitraryScale(ref block, ref destRef, destStrideWidth, horizontalScale, verticalScale); |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) |
|||
{ |
|||
for (int y = 0; y < 2; y++) |
|||
{ |
|||
int yy = y * verticalScale; |
|||
int y8 = y * 8; |
|||
|
|||
for (int x = 0; x < 2; x++) |
|||
{ |
|||
int xx = x * horizontalScale; |
|||
|
|||
float value = block[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, (nint)(uint)(baseIdx + j)) = value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Processes component spectral data and converts it to color data in 8-to-1 scale.
|
|||
/// </summary>
|
|||
internal sealed class DownScalingComponentProcessor8 : ComponentProcessor |
|||
{ |
|||
private readonly float dcDequantizatizer; |
|||
|
|||
public DownScalingComponentProcessor8(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) |
|||
: base(memoryAllocator, frame, postProcessorBufferSize, component, 1) |
|||
=> this.dcDequantizatizer = 0.125f * rawJpeg.QuantizationTables[component.QuantizationTableIndex][0]; |
|||
|
|||
public override void CopyBlocksToColorBuffer(int spectralStep) |
|||
{ |
|||
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks; |
|||
|
|||
float maximumValue = this.Frame.MaxColorChannelValue; |
|||
float normalizationValue = MathF.Ceiling(maximumValue / 2); |
|||
|
|||
int destAreaStride = this.ColorBuffer.Width; |
|||
|
|||
int blocksRowsPerStep = this.Component.SamplingFactors.Height; |
|||
Size subSamplingDivisors = this.Component.SubSamplingDivisors; |
|||
|
|||
int yBlockStart = spectralStep * blocksRowsPerStep; |
|||
|
|||
for (int y = 0; y < blocksRowsPerStep; y++) |
|||
{ |
|||
int yBuffer = y * this.BlockAreaSize.Height; |
|||
|
|||
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); |
|||
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); |
|||
|
|||
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) |
|||
{ |
|||
float dc = ScaledFloatingPointDCT.TransformIDCT_1x1(blockRow[xBlock][0], this.dcDequantizatizer, normalizationValue, maximumValue); |
|||
|
|||
// Save to the intermediate buffer
|
|||
int xColorBufferStart = xBlock * this.BlockAreaSize.Width; |
|||
ScaledCopyTo( |
|||
dc, |
|||
ref colorBufferRow[xColorBufferStart], |
|||
destAreaStride, |
|||
subSamplingDivisors.Width, |
|||
subSamplingDivisors.Height); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static void ScaledCopyTo(float value, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) |
|||
{ |
|||
if (horizontalScale == 1 && verticalScale == 1) |
|||
{ |
|||
destRef = value; |
|||
return; |
|||
} |
|||
|
|||
if (horizontalScale == 2 && verticalScale == 2) |
|||
{ |
|||
destRef = value; |
|||
Unsafe.Add(ref destRef, 1) = value; |
|||
Unsafe.Add(ref destRef, 0 + (nint)(uint)destStrideWidth) = value; |
|||
Unsafe.Add(ref destRef, 1 + (nint)(uint)destStrideWidth) = value; |
|||
return; |
|||
} |
|||
|
|||
// TODO: Optimize: implement all cases with scale-specific, loopless code!
|
|||
for (int y = 0; y < verticalScale; y++) |
|||
{ |
|||
for (int x = 0; x < horizontalScale; x++) |
|||
{ |
|||
Unsafe.Add(ref destRef, (nint)(uint)x) = value; |
|||
} |
|||
|
|||
destRef = ref Unsafe.Add(ref destRef, (nint)(uint)destStrideWidth); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,129 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Encapsulates spectral data to rgba32 processing for one component.
|
|||
/// </summary>
|
|||
internal class JpegComponentPostProcessor : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// The size of the area in <see cref="ColorBuffer"/> corresponding to one 8x8 Jpeg block
|
|||
/// </summary>
|
|||
private readonly Size blockAreaSize; |
|||
|
|||
/// <summary>
|
|||
/// Jpeg frame instance containing required decoding metadata.
|
|||
/// </summary>
|
|||
private readonly JpegFrame frame; |
|||
|
|||
/// <summary>
|
|||
/// Gets the maximal number of block rows being processed in one step.
|
|||
/// </summary>
|
|||
private readonly int blockRowsPerStep; |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="IJpegComponent"/> component containing decoding meta information.
|
|||
/// </summary>
|
|||
private readonly IJpegComponent component; |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="IRawJpegData"/> instance containing decoding meta information.
|
|||
/// </summary>
|
|||
private readonly IRawJpegData rawJpeg; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="JpegComponentPostProcessor"/> class.
|
|||
/// </summary>
|
|||
public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) |
|||
{ |
|||
this.frame = frame; |
|||
|
|||
this.component = component; |
|||
this.rawJpeg = rawJpeg; |
|||
this.blockAreaSize = this.component.SubSamplingDivisors * 8; |
|||
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>( |
|||
postProcessorBufferSize.Width, |
|||
postProcessorBufferSize.Height, |
|||
this.blockAreaSize.Height); |
|||
|
|||
this.blockRowsPerStep = postProcessorBufferSize.Height / 8 / this.component.SubSamplingDivisors.Height; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the temporary working buffer of color values.
|
|||
/// </summary>
|
|||
public Buffer2D<float> ColorBuffer { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() => this.ColorBuffer.Dispose(); |
|||
|
|||
/// <summary>
|
|||
/// Convert raw spectral DCT data to color data and copy it to the color buffer <see cref="ColorBuffer"/>.
|
|||
/// </summary>
|
|||
public void CopyBlocksToColorBuffer(int spectralStep) |
|||
{ |
|||
Buffer2D<Block8x8> spectralBuffer = this.component.SpectralBlocks; |
|||
|
|||
float maximumValue = this.frame.MaxColorChannelValue; |
|||
|
|||
int destAreaStride = this.ColorBuffer.Width; |
|||
|
|||
int yBlockStart = spectralStep * this.blockRowsPerStep; |
|||
|
|||
Size subSamplingDivisors = this.component.SubSamplingDivisors; |
|||
|
|||
Block8x8F dequantTable = this.rawJpeg.QuantizationTables[this.component.QuantizationTableIndex]; |
|||
Block8x8F workspaceBlock = default; |
|||
|
|||
for (int y = 0; y < this.blockRowsPerStep; y++) |
|||
{ |
|||
int yBuffer = y * this.blockAreaSize.Height; |
|||
|
|||
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); |
|||
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); |
|||
|
|||
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) |
|||
{ |
|||
// Integer to float
|
|||
workspaceBlock.LoadFrom(ref blockRow[xBlock]); |
|||
|
|||
// Dequantize
|
|||
workspaceBlock.MultiplyInPlace(ref dequantTable); |
|||
|
|||
// Convert from spectral to color
|
|||
FastFloatingPointDCT.TransformIDCT(ref workspaceBlock); |
|||
|
|||
// To conform better to libjpeg we actually NEED TO loose precision here.
|
|||
// This is because they store blocks as Int16 between all the operations.
|
|||
// To be "more accurate", we need to emulate this by rounding!
|
|||
workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue); |
|||
|
|||
// Write to color buffer according to sampling factors
|
|||
int xColorBufferStart = xBlock * this.blockAreaSize.Width; |
|||
workspaceBlock.ScaledCopyTo( |
|||
ref colorBufferRow[xColorBufferStart], |
|||
destAreaStride, |
|||
subSamplingDivisors.Width, |
|||
subSamplingDivisors.Height); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void ClearSpectralBuffers() |
|||
{ |
|||
Buffer2D<Block8x8> spectralBlocks = this.component.SpectralBlocks; |
|||
for (int i = 0; i < spectralBlocks.Height; i++) |
|||
{ |
|||
spectralBlocks.DangerousGetRowSpan(i).Clear(); |
|||
} |
|||
} |
|||
|
|||
public Span<float> GetColorBufferRowSpan(int row) => |
|||
this.ColorBuffer.DangerousGetRowSpan(row); |
|||
} |
|||
} |
|||
@ -0,0 +1,220 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
#pragma warning disable IDE0078
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components |
|||
{ |
|||
/// <summary>
|
|||
/// Contains floating point forward DCT implementations with built-in scaling.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Based on "Loeffler, Ligtenberg, and Moschytz" algorithm.
|
|||
/// </remarks>
|
|||
internal static class ScaledFloatingPointDCT |
|||
{ |
|||
#pragma warning disable SA1310
|
|||
private const float FP32_0_541196100 = 0.541196100f; |
|||
private const float FP32_0_765366865 = 0.765366865f; |
|||
private const float FP32_1_847759065 = 1.847759065f; |
|||
private const float FP32_0_211164243 = 0.211164243f; |
|||
private const float FP32_1_451774981 = 1.451774981f; |
|||
private const float FP32_2_172734803 = 2.172734803f; |
|||
private const float FP32_1_061594337 = 1.061594337f; |
|||
private const float FP32_0_509795579 = 0.509795579f; |
|||
private const float FP32_0_601344887 = 0.601344887f; |
|||
private const float FP32_0_899976223 = 0.899976223f; |
|||
private const float FP32_2_562915447 = 2.562915447f; |
|||
private const float FP32_0_720959822 = 0.720959822f; |
|||
private const float FP32_0_850430095 = 0.850430095f; |
|||
private const float FP32_1_272758580 = 1.272758580f; |
|||
private const float FP32_3_624509785 = 3.624509785f; |
|||
#pragma warning restore SA1310
|
|||
|
|||
/// <summary>
|
|||
/// Adjusts given quantization table for usage with IDCT algorithms
|
|||
/// from <see cref="ScaledFloatingPointDCT"/>.
|
|||
/// </summary>
|
|||
/// <param name="quantTable">Quantization table to adjust.</param>
|
|||
public static void AdjustToIDCT(ref Block8x8F quantTable) |
|||
{ |
|||
ref float tableRef = ref Unsafe.As<Block8x8F, float>(ref quantTable); |
|||
for (nint i = 0; i < Block8x8F.Size; i++) |
|||
{ |
|||
ref float elemRef = ref Unsafe.Add(ref tableRef, i); |
|||
elemRef = 0.125f * elemRef; |
|||
} |
|||
|
|||
// Spectral macroblocks are transposed before quantization
|
|||
// so we must transpose quantization table
|
|||
quantTable.TransposeInplace(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Apply 2D floating point 'donwscaling' IDCT inplace producing
|
|||
/// 8x8 -> 4x4 result.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Resulting matrix is stored in the top left 4x4 part of the
|
|||
/// <paramref name="block"/>.
|
|||
/// </remarks>
|
|||
/// <param name="block">Input block.</param>
|
|||
/// <param name="dequantTable">Dequantization table adjusted by <see cref="AdjustToIDCT(ref Block8x8F)"/>.</param>
|
|||
/// <param name="normalizationValue">Output range normalization value, 1/2 of the <paramref name="maxValue"/>.</param>
|
|||
/// <param name="maxValue">Maximum value of the output range.</param>
|
|||
public static void TransformIDCT_4x4(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue) |
|||
{ |
|||
for (int ctr = 0; ctr < 8; ctr++) |
|||
{ |
|||
// Don't process row 4, second pass doesn't use it
|
|||
if (ctr == 4) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Even part
|
|||
float tmp0 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0] * 2; |
|||
|
|||
float z2 = block[(ctr * 8) + 2] * dequantTable[(ctr * 8) + 2]; |
|||
float z3 = block[(ctr * 8) + 6] * dequantTable[(ctr * 8) + 6]; |
|||
|
|||
float tmp2 = (z2 * FP32_1_847759065) + (z3 * -FP32_0_765366865); |
|||
|
|||
float tmp10 = tmp0 + tmp2; |
|||
float tmp12 = tmp0 - tmp2; |
|||
|
|||
// Odd part
|
|||
float z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7]; |
|||
z2 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5]; |
|||
z3 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3]; |
|||
float z4 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1]; |
|||
|
|||
tmp0 = (z1 * -FP32_0_211164243) + |
|||
(z2 * FP32_1_451774981) + |
|||
(z3 * -FP32_2_172734803) + |
|||
(z4 * FP32_1_061594337); |
|||
|
|||
tmp2 = (z1 * -FP32_0_509795579) + |
|||
(z2 * -FP32_0_601344887) + |
|||
(z3 * FP32_0_899976223) + |
|||
(z4 * FP32_2_562915447); |
|||
|
|||
// temporal result is saved to +4 shifted indices
|
|||
// because result is saved into the top left 2x2 region of the
|
|||
// input block
|
|||
block[(ctr * 8) + 0 + 4] = (tmp10 + tmp2) / 2; |
|||
block[(ctr * 8) + 3 + 4] = (tmp10 - tmp2) / 2; |
|||
block[(ctr * 8) + 1 + 4] = (tmp12 + tmp0) / 2; |
|||
block[(ctr * 8) + 2 + 4] = (tmp12 - tmp0) / 2; |
|||
} |
|||
|
|||
for (int ctr = 0; ctr < 4; ctr++) |
|||
{ |
|||
// Even part
|
|||
float tmp0 = block[ctr + (8 * 0) + 4] * 2; |
|||
|
|||
float tmp2 = (block[ctr + (8 * 2) + 4] * FP32_1_847759065) + (block[ctr + (8 * 6) + 4] * -FP32_0_765366865); |
|||
|
|||
float tmp10 = tmp0 + tmp2; |
|||
float tmp12 = tmp0 - tmp2; |
|||
|
|||
// Odd part
|
|||
float z1 = block[ctr + (8 * 7) + 4]; |
|||
float z2 = block[ctr + (8 * 5) + 4]; |
|||
float z3 = block[ctr + (8 * 3) + 4]; |
|||
float z4 = block[ctr + (8 * 1) + 4]; |
|||
|
|||
tmp0 = (z1 * -FP32_0_211164243) + |
|||
(z2 * FP32_1_451774981) + |
|||
(z3 * -FP32_2_172734803) + |
|||
(z4 * FP32_1_061594337); |
|||
|
|||
tmp2 = (z1 * -FP32_0_509795579) + |
|||
(z2 * -FP32_0_601344887) + |
|||
(z3 * FP32_0_899976223) + |
|||
(z4 * FP32_2_562915447); |
|||
|
|||
// Save results to the top left 4x4 subregion
|
|||
block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp2) / 2) + normalizationValue, 0, maxValue)); |
|||
block[(ctr * 8) + 3] = MathF.Round(Numerics.Clamp(((tmp10 - tmp2) / 2) + normalizationValue, 0, maxValue)); |
|||
block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp12 + tmp0) / 2) + normalizationValue, 0, maxValue)); |
|||
block[(ctr * 8) + 2] = MathF.Round(Numerics.Clamp(((tmp12 - tmp0) / 2) + normalizationValue, 0, maxValue)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Apply 2D floating point 'donwscaling' IDCT inplace producing
|
|||
/// 8x8 -> 2x2 result.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Resulting matrix is stored in the top left 2x2 part of the
|
|||
/// <paramref name="block"/>.
|
|||
/// </remarks>
|
|||
/// <param name="block">Input block.</param>
|
|||
/// <param name="dequantTable">Dequantization table adjusted by <see cref="AdjustToIDCT(ref Block8x8F)"/>.</param>
|
|||
/// <param name="normalizationValue">Output range normalization value, 1/2 of the <paramref name="maxValue"/>.</param>
|
|||
/// <param name="maxValue">Maximum value of the output range.</param>
|
|||
public static void TransformIDCT_2x2(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue) |
|||
{ |
|||
for (int ctr = 0; ctr < 8; ctr++) |
|||
{ |
|||
// Don't process rows 2/4/6, second pass doesn't use it
|
|||
if (ctr == 2 || ctr == 4 || ctr == 6) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Even part
|
|||
float tmp0; |
|||
float z1 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0]; |
|||
float tmp10 = z1 * 4; |
|||
|
|||
// Odd part
|
|||
z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7]; |
|||
tmp0 = z1 * -FP32_0_720959822; |
|||
z1 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5]; |
|||
tmp0 += z1 * FP32_0_850430095; |
|||
z1 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3]; |
|||
tmp0 += z1 * -FP32_1_272758580; |
|||
z1 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1]; |
|||
tmp0 += z1 * FP32_3_624509785; |
|||
|
|||
// temporal result is saved to +2 shifted indices
|
|||
// because result is saved into the top left 2x2 region of the
|
|||
// input block
|
|||
block[(ctr * 8) + 2] = (tmp10 + tmp0) / 4; |
|||
block[(ctr * 8) + 3] = (tmp10 - tmp0) / 4; |
|||
} |
|||
|
|||
for (int ctr = 0; ctr < 2; ctr++) |
|||
{ |
|||
// Even part
|
|||
float tmp10 = block[ctr + (8 * 0) + 2] * 4; |
|||
|
|||
// Odd part
|
|||
float tmp0 = (block[ctr + (8 * 7) + 2] * -FP32_0_720959822) + |
|||
(block[ctr + (8 * 5) + 2] * FP32_0_850430095) + |
|||
(block[ctr + (8 * 3) + 2] * -FP32_1_272758580) + |
|||
(block[ctr + (8 * 1) + 2] * FP32_3_624509785); |
|||
|
|||
// Save results to the top left 2x2 subregion
|
|||
block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp0) / 4) + normalizationValue, 0, maxValue)); |
|||
block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp10 - tmp0) / 4) + normalizationValue, 0, maxValue)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Apply 2D floating point 'donwscaling' IDCT inplace producing
|
|||
/// 8x8 -> 1x1 result.
|
|||
/// </summary>
|
|||
/// <param name="dc">Direct current term value from input block.</param>
|
|||
/// <param name="dequantizer">Dequantization value.</param>
|
|||
/// <param name="normalizationValue">Output range normalization value, 1/2 of the <paramref name="maxValue"/>.</param>
|
|||
/// <param name="maxValue">Maximum value of the output range.</param>
|
|||
public static float TransformIDCT_1x1(float dc, float dequantizer, float normalizationValue, float maxValue) |
|||
=> MathF.Round(Numerics.Clamp((dc * dequantizer) + normalizationValue, 0, maxValue)); |
|||
} |
|||
} |
|||
#pragma warning restore IDE0078
|
|||
@ -1,88 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Drawing; |
|||
using System.Drawing.Drawing2D; |
|||
using System.Drawing.Imaging; |
|||
using System.IO; |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Tests; |
|||
|
|||
// ReSharper disable InconsistentNaming
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg |
|||
{ |
|||
[Config(typeof(Config.ShortMultiFramework))] |
|||
public class LoadResizeSave_Aggregate : MultiImageBenchmarkBase |
|||
{ |
|||
protected override IEnumerable<string> InputImageSubfoldersOrFiles => |
|||
new[] |
|||
{ |
|||
TestImages.Jpeg.BenchmarkSuite.Jpeg400_SmallMonochrome, |
|||
TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr, |
|||
TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr, |
|||
TestImages.Jpeg.BenchmarkSuite.MissingFF00ProgressiveBedroom159_MidSize420YCbCr, |
|||
TestImages.Jpeg.BenchmarkSuite.ExifGetString750Transform_Huge420YCbCr, |
|||
}; |
|||
|
|||
[Params(InputImageCategory.AllImages)] |
|||
public override InputImageCategory InputCategory { get; set; } |
|||
|
|||
private readonly Configuration configuration = new Configuration(new JpegConfigurationModule()); |
|||
|
|||
private byte[] destBytes; |
|||
|
|||
public override void Setup() |
|||
{ |
|||
base.Setup(); |
|||
|
|||
this.configuration.MaxDegreeOfParallelism = 1; |
|||
const int MaxOutputSizeInBytes = 2 * 1024 * 1024; // ~2 MB
|
|||
this.destBytes = new byte[MaxOutputSizeInBytes]; |
|||
} |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public void SystemDrawing() |
|||
=> this.ForEachStream( |
|||
sourceStream => |
|||
{ |
|||
using (var destStream = new MemoryStream(this.destBytes)) |
|||
using (var source = System.Drawing.Image.FromStream(sourceStream)) |
|||
using (var destination = new Bitmap(source.Width / 4, source.Height / 4)) |
|||
{ |
|||
using (var g = Graphics.FromImage(destination)) |
|||
{ |
|||
g.InterpolationMode = InterpolationMode.HighQualityBicubic; |
|||
g.PixelOffsetMode = PixelOffsetMode.HighQuality; |
|||
g.CompositingQuality = CompositingQuality.HighQuality; |
|||
g.DrawImage(source, 0, 0, 400, 400); |
|||
} |
|||
|
|||
destination.Save(destStream, ImageFormat.Jpeg); |
|||
} |
|||
|
|||
return null; |
|||
}); |
|||
|
|||
[Benchmark] |
|||
public void ImageSharp() |
|||
=> this.ForEachStream( |
|||
sourceStream => |
|||
{ |
|||
using (var source = Image.Load<Rgba32>( |
|||
this.configuration, |
|||
sourceStream, |
|||
new JpegDecoder { IgnoreMetadata = true })) |
|||
{ |
|||
using var destStream = new MemoryStream(this.destBytes); |
|||
source.Mutate(c => c.Resize(source.Width / 4, source.Height / 4)); |
|||
source.SaveAsJpeg(destStream); |
|||
} |
|||
|
|||
return null; |
|||
}); |
|||
} |
|||
} |
|||
@ -1,103 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System; |
|||
using System.Drawing; |
|||
using System.Drawing.Drawing2D; |
|||
using System.Drawing.Imaging; |
|||
using System.IO; |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Tests; |
|||
using SDImage = System.Drawing.Image; |
|||
|
|||
// ReSharper disable InconsistentNaming
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg |
|||
{ |
|||
[Config(typeof(Config.ShortMultiFramework))] |
|||
public class LoadResizeSave_ImageSpecific |
|||
{ |
|||
private readonly Configuration configuration = new Configuration(new JpegConfigurationModule()); |
|||
|
|||
private byte[] sourceBytes; |
|||
|
|||
private byte[] destBytes; |
|||
|
|||
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); |
|||
|
|||
[Params( |
|||
TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr, |
|||
TestImages.Jpeg.BenchmarkSuite.BadRstProgressive518_Large444YCbCr, |
|||
TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr)] |
|||
|
|||
public string TestImage { get; set; } |
|||
|
|||
[Params(false, true)] |
|||
public bool ParallelExec { get; set; } |
|||
|
|||
[GlobalSetup] |
|||
public void Setup() |
|||
{ |
|||
this.configuration.MaxDegreeOfParallelism = |
|||
this.ParallelExec ? Environment.ProcessorCount : 1; |
|||
|
|||
this.sourceBytes = File.ReadAllBytes(this.TestImageFullPath); |
|||
|
|||
this.destBytes = new byte[this.sourceBytes.Length * 2]; |
|||
} |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public void SystemDrawing() |
|||
{ |
|||
using var sourceStream = new MemoryStream(this.sourceBytes); |
|||
using var destStream = new MemoryStream(this.destBytes); |
|||
using var source = SDImage.FromStream(sourceStream); |
|||
using var destination = new Bitmap(source.Width / 4, source.Height / 4); |
|||
using (var g = Graphics.FromImage(destination)) |
|||
{ |
|||
g.InterpolationMode = InterpolationMode.HighQualityBicubic; |
|||
g.PixelOffsetMode = PixelOffsetMode.HighQuality; |
|||
g.CompositingQuality = CompositingQuality.HighQuality; |
|||
g.DrawImage(source, 0, 0, 400, 400); |
|||
} |
|||
|
|||
destination.Save(destStream, ImageFormat.Jpeg); |
|||
} |
|||
|
|||
[Benchmark] |
|||
public void ImageSharp() |
|||
{ |
|||
using (var source = Image.Load(this.configuration, this.sourceBytes, new JpegDecoder { IgnoreMetadata = true })) |
|||
using (var destStream = new MemoryStream(this.destBytes)) |
|||
{ |
|||
source.Mutate(c => c.Resize(source.Width / 4, source.Height / 4)); |
|||
source.SaveAsJpeg(destStream); |
|||
} |
|||
} |
|||
|
|||
// RESULTS (2018 October 31):
|
|||
//
|
|||
// BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
|
|||
// Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
|
|||
// Frequency=2742191 Hz, Resolution=364.6719 ns, Timer=TSC
|
|||
// .NET Core SDK=2.1.403
|
|||
// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
|
|||
// Job-ZPEZGV : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0
|
|||
// Job-SGOCJT : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
|
|||
//
|
|||
// Method | Runtime | TestImage | ParallelExec | Mean | Error | StdDev | Scaled | ScaledSD | Gen 0 | Allocated |
|
|||
// -------------- |-------- |----------------------------- |------------- |----------:|----------:|----------:|-------:|---------:|---------:|----------:|
|
|||
// SystemDrawing | Clr | Jpg/baseline/jpeg420exif.jpg | False | 64.88 ms | 3.735 ms | 0.2110 ms | 1.00 | 0.00 | 250.0000 | 791.07 KB |
|
|||
// ImageSharp | Clr | Jpg/baseline/jpeg420exif.jpg | False | 129.53 ms | 23.423 ms | 1.3234 ms | 2.00 | 0.02 | - | 50.09 KB |
|
|||
// | | | | | | | | | | |
|
|||
// SystemDrawing | Core | Jpg/baseline/jpeg420exif.jpg | False | 65.87 ms | 10.488 ms | 0.5926 ms | 1.00 | 0.00 | 250.0000 | 789.79 KB |
|
|||
// ImageSharp | Core | Jpg/baseline/jpeg420exif.jpg | False | 92.00 ms | 7.241 ms | 0.4091 ms | 1.40 | 0.01 | - | 46.36 KB |
|
|||
// | | | | | | | | | | |
|
|||
// SystemDrawing | Clr | Jpg/baseline/jpeg420exif.jpg | True | 64.23 ms | 5.998 ms | 0.3389 ms | 1.00 | 0.00 | 250.0000 | 791.07 KB |
|
|||
// ImageSharp | Clr | Jpg/baseline/jpeg420exif.jpg | True | 82.63 ms | 29.320 ms | 1.6566 ms | 1.29 | 0.02 | - | 57.59 KB |
|
|||
// | | | | | | | | | | |
|
|||
// SystemDrawing | Core | Jpg/baseline/jpeg420exif.jpg | True | 64.20 ms | 6.560 ms | 0.3707 ms | 1.00 | 0.00 | 250.0000 | 789.79 KB |
|
|||
// ImageSharp | Core | Jpg/baseline/jpeg420exif.jpg | True | 68.08 ms | 18.376 ms | 1.0383 ms | 1.06 | 0.01 | - | 50.49 KB |
|
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Jpg |
|||
{ |
|||
[Trait("Format", "Jpg")] |
|||
public class SpectralConverterTests |
|||
{ |
|||
// Test for null target size, i.e. when no scaling is needed
|
|||
[Theory] |
|||
[InlineData(1, 1)] |
|||
[InlineData(800, 400)] |
|||
[InlineData(2354, 4847)] |
|||
public void CalculateResultingImageSize_Null_TargetSize(int width, int height) |
|||
{ |
|||
Size inputSize = new(width, height); |
|||
|
|||
Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, null, out int blockPixelSize); |
|||
|
|||
Assert.Equal(expected: 8, blockPixelSize); |
|||
Assert.Equal(inputSize, outputSize); |
|||
} |
|||
|
|||
// Test for 'perfect' dimensions, i.e. dimensions divisible by 8, with exact scaled size match
|
|||
[Theory] |
|||
[InlineData(800, 400, 800, 400, 8)] |
|||
[InlineData(800, 400, 400, 200, 4)] |
|||
[InlineData(800, 400, 200, 100, 2)] |
|||
[InlineData(800, 400, 100, 50, 1)] |
|||
public void CalculateResultingImageSize_Perfect_Dimensions_Exact_Match(int inW, int inH, int tW, int tH, int expectedBlockSize) |
|||
{ |
|||
Size inputSize = new(inW, inH); |
|||
Size targetSize = new(tW, tH); |
|||
|
|||
Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize); |
|||
|
|||
Assert.Equal(expectedBlockSize, blockPixelSize); |
|||
Assert.Equal(outputSize, targetSize); |
|||
} |
|||
|
|||
// Test for 'imperfect' dimensions, i.e. dimensions NOT divisible by 8, with exact scaled size match
|
|||
[Theory] |
|||
[InlineData(7, 14, 7, 14, 8)] |
|||
[InlineData(7, 14, 4, 7, 4)] |
|||
[InlineData(7, 14, 2, 4, 2)] |
|||
[InlineData(7, 14, 1, 2, 1)] |
|||
public void CalculateResultingImageSize_Imperfect_Dimensions_Exact_Match(int inW, int inH, int tW, int tH, int expectedBlockSize) |
|||
{ |
|||
Size inputSize = new(inW, inH); |
|||
Size targetSize = new(tW, tH); |
|||
|
|||
Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize); |
|||
|
|||
Assert.Equal(expectedBlockSize, blockPixelSize); |
|||
Assert.Equal(outputSize, targetSize); |
|||
} |
|||
|
|||
// Test for inexact target and output sizes match
|
|||
[Theory] |
|||
[InlineData(7, 14, 4, 6, 4, 7, 4)] |
|||
[InlineData(7, 14, 1, 1, 1, 2, 1)] |
|||
[InlineData(800, 400, 999, 600, 800, 400, 8)] |
|||
[InlineData(800, 400, 390, 150, 400, 200, 4)] |
|||
[InlineData(804, 1198, 500, 800, 804, 1198, 8)] |
|||
public void CalculateResultingImageSize_Inexact_Target_Size(int inW, int inH, int tW, int tH, int exW, int exH, int expectedBlockSize) |
|||
{ |
|||
Size inputSize = new(inW, inH); |
|||
Size targetSize = new(tW, tH); |
|||
Size expectedSize = new(exW, exH); |
|||
|
|||
Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize); |
|||
|
|||
Assert.Equal(expectedBlockSize, blockPixelSize); |
|||
Assert.Equal(expectedSize, outputSize); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue