Browse Source

Merge branch 'main' into js/decoder-options

pull/2180/head
James Jackson-South 4 years ago
parent
commit
75b0e07794
  1. 17
      src/ImageSharp/Common/Helpers/EnumUtils.cs
  2. 47
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  3. 43
      src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs
  4. 15
      src/ImageSharp/Formats/Jpeg/Components/Block8x8.cs
  5. 159
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs
  6. 147
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs
  7. 13
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs
  8. 99
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykAvx.cs
  9. 82
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs
  10. 106
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector.cs
  11. 72
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleAvx.cs
  12. 51
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs
  13. 74
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector.cs
  14. 16
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbAvx.cs
  15. 40
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs
  16. 27
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector.cs
  17. 125
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrAvx.cs
  18. 76
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs
  19. 128
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector.cs
  20. 136
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKAvx.cs
  21. 80
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs
  22. 138
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector.cs
  23. 6
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterAvx.cs
  24. 150
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs
  25. 2
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.cs
  26. 131
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.cs
  27. 52
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmykAvx.cs
  28. 43
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmykScalar.cs
  29. 51
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmykVector.cs
  30. 39
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScaleAvx.cs
  31. 34
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScaleScalar.cs
  32. 38
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScaleVector.cs
  33. 26
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgbScalar.cs
  34. 74
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrAvx.cs
  35. 50
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrScalar.cs
  36. 74
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrVector.cs
  37. 82
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccKAvx.cs
  38. 43
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccKScalar.cs
  39. 75
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccKVector.cs
  40. 61
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs
  41. 8
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
  42. 4
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
  43. 1
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  44. 116
      src/ImageSharp/Formats/Jpeg/Components/Encoder/Component.cs
  45. 231
      src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs
  46. 30
      src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegComponentConfig.cs
  47. 44
      src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs
  48. 21
      src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegHuffmanTableConfig.cs
  49. 20
      src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegQuantizationTableConfig.cs
  50. 35
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffIndex.cs
  51. 17
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs
  52. 366
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  53. 53
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs
  54. 85
      src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs
  55. 127
      src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs
  56. 21
      src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs
  57. 165
      src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs
  58. 237
      src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterLut.cs
  59. 259
      src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterVectorized.cs
  60. 12
      src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs
  61. 149
      src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs
  62. 121
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs
  63. 122
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs
  64. 61
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs
  65. 4
      src/ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs
  66. 15
      src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
  67. 13
      src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
  68. 1
      src/ImageSharp/Formats/Jpeg/JpegDecoder.cs
  69. 38
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  70. 25
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  71. 196
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs
  72. 556
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  73. 15
      src/ImageSharp/Formats/Jpeg/JpegEncodingColor.cs
  74. 80
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  75. 2
      src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs
  76. 2
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs
  77. 31
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
  78. 2
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs
  79. 7
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  80. 8
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb24.PixelOperations.cs
  81. 31
      src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs
  82. 8
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs
  83. 6
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/GrayscaleColorConversion.cs
  84. 8
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/RgbColorConversion.cs
  85. 8
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrColorConversion.cs
  86. 56
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrForwardConverterBenchmark.cs
  87. 8
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YccKColorConverter.cs
  88. 2
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegComparison.cs
  89. 48
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegFeatures.cs
  90. 240
      tests/ImageSharp.Benchmarks/Color/RgbToYCbCr.LookupTables.cs
  91. 359
      tests/ImageSharp.Benchmarks/Color/RgbToYCbCr.cs
  92. 2
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  93. 20
      tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
  94. 97
      tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs
  95. 57
      tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
  96. 1
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs
  97. 29
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
  98. 148
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs
  99. 457
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  100. 6
      tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs

17
src/ImageSharp/Common/Helpers/EnumUtils.cs

@ -19,15 +19,14 @@ namespace SixLabors.ImageSharp
/// <param name="defaultValue">The default value to return.</param> /// <param name="defaultValue">The default value to return.</param>
/// <returns>The <typeparamref name="TEnum"/>.</returns> /// <returns>The <typeparamref name="TEnum"/>.</returns>
public static TEnum Parse<TEnum>(int value, TEnum defaultValue) public static TEnum Parse<TEnum>(int value, TEnum defaultValue)
where TEnum : Enum where TEnum : struct, Enum
{ {
foreach (TEnum enumValue in Enum.GetValues(typeof(TEnum))) DebugGuard.IsTrue(Unsafe.SizeOf<TEnum>() == sizeof(int), "Only int-sized enums are supported.");
{
TEnum current = enumValue; TEnum valueEnum = Unsafe.As<int, TEnum>(ref value);
if (value == Unsafe.As<TEnum, int>(ref current)) if (Enum.IsDefined(valueEnum))
{ {
return enumValue; return valueEnum;
}
} }
return defaultValue; return defaultValue;
@ -41,8 +40,10 @@ namespace SixLabors.ImageSharp
/// <param name="flag">The flag.</param> /// <param name="flag">The flag.</param>
/// <returns>The <see cref="bool"/>.</returns> /// <returns>The <see cref="bool"/>.</returns>
public static bool HasFlag<TEnum>(TEnum value, TEnum flag) public static bool HasFlag<TEnum>(TEnum value, TEnum flag)
where TEnum : Enum where TEnum : struct, Enum
{ {
DebugGuard.IsTrue(Unsafe.SizeOf<TEnum>() == sizeof(int), "Only int-sized enums are supported.");
uint flagValue = Unsafe.As<TEnum, uint>(ref flag); uint flagValue = Unsafe.As<TEnum, uint>(ref flag);
return (Unsafe.As<TEnum, uint>(ref value) & flagValue) == flagValue; return (Unsafe.As<TEnum, uint>(ref value) & flagValue) == flagValue;
} }

47
src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs

@ -21,6 +21,10 @@ namespace SixLabors.ImageSharp
public static ReadOnlySpan<byte> PermuteMaskSwitchInnerDWords8x32 => new byte[] { 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0 }; public static ReadOnlySpan<byte> PermuteMaskSwitchInnerDWords8x32 => new byte[] { 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0 };
private static ReadOnlySpan<byte> MoveFirst24BytesToSeparateLanes => new byte[] { 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0 };
internal static ReadOnlySpan<byte> ExtractRgb => new byte[] { 0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF, 0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF };
private static ReadOnlySpan<byte> ShuffleMaskPad4Nx16 => new byte[] { 0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80 }; private static ReadOnlySpan<byte> ShuffleMaskPad4Nx16 => new byte[] { 0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80 };
private static ReadOnlySpan<byte> ShuffleMaskSlice4Nx16 => new byte[] { 0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80 }; private static ReadOnlySpan<byte> ShuffleMaskSlice4Nx16 => new byte[] { 0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80 };
@ -962,6 +966,49 @@ namespace SixLabors.ImageSharp
blueChannel = blueChannel.Slice(slice); blueChannel = blueChannel.Slice(slice);
destination = destination.Slice(slice); destination = destination.Slice(slice);
} }
internal static void UnpackToRgbPlanesAvx2Reduce(
ref Span<float> redChannel,
ref Span<float> greenChannel,
ref Span<float> blueChannel,
ref ReadOnlySpan<Rgb24> source)
{
ref Vector256<byte> rgbByteSpan = ref Unsafe.As<Rgb24, Vector256<byte>>(ref MemoryMarshal.GetReference(source));
ref Vector256<float> destRRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(redChannel));
ref Vector256<float> destGRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(greenChannel));
ref Vector256<float> destBRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(blueChannel));
Vector256<uint> extractToLanesMask = Unsafe.As<byte, Vector256<uint>>(ref MemoryMarshal.GetReference(MoveFirst24BytesToSeparateLanes));
Vector256<byte> extractRgbMask = Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(ExtractRgb));
Vector256<byte> rgb, rg, bx;
Vector256<float> r, g, b;
const int bytesPerRgbStride = 24;
int count = (int)((uint)source.Length / 8);
for (int i = 0; i < count; i++)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (IntPtr)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.Shuffle(rgb, extractRgbMask);
rg = Avx2.UnpackLow(rgb, Vector256<byte>.Zero);
bx = Avx2.UnpackHigh(rgb, Vector256<byte>.Zero);
r = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, Vector256<byte>.Zero).AsInt32());
g = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, Vector256<byte>.Zero).AsInt32());
b = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, Vector256<byte>.Zero).AsInt32());
Unsafe.Add(ref destRRef, i) = r;
Unsafe.Add(ref destGRef, i) = g;
Unsafe.Add(ref destBRef, i) = b;
}
int sliceCount = count * 8;
redChannel = redChannel.Slice(sliceCount);
greenChannel = greenChannel.Slice(sliceCount);
blueChannel = blueChannel.Slice(sliceCount);
source = source.Slice(sliceCount);
}
} }
} }
} }

43
src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs

@ -65,6 +65,25 @@ namespace SixLabors.ImageSharp
PackFromRgbPlanesRemainder(redChannel, greenChannel, blueChannel, destination); PackFromRgbPlanesRemainder(redChannel, greenChannel, blueChannel, destination);
} }
[MethodImpl(InliningOptions.ShortMethod)]
internal static void UnpackToRgbPlanes(
Span<float> redChannel,
Span<float> greenChannel,
Span<float> blueChannel,
ReadOnlySpan<Rgb24> source)
{
DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!");
DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!");
DebugGuard.IsTrue(source.Length <= redChannel.Length, nameof(source), "'source' span should not be bigger than the destination channels!");
if (Avx2.IsSupported)
{
HwIntrinsics.UnpackToRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref source);
}
UnpackToRgbPlanesScalar(redChannel, greenChannel, blueChannel, source);
}
private static void PackFromRgbPlanesScalarBatchedReduce( private static void PackFromRgbPlanesScalarBatchedReduce(
ref ReadOnlySpan<byte> redChannel, ref ReadOnlySpan<byte> redChannel,
ref ReadOnlySpan<byte> greenChannel, ref ReadOnlySpan<byte> greenChannel,
@ -200,5 +219,29 @@ namespace SixLabors.ImageSharp
d.A = 255; d.A = 255;
} }
} }
private static void UnpackToRgbPlanesScalar(
Span<float> redChannel,
Span<float> greenChannel,
Span<float> blueChannel,
ReadOnlySpan<Rgb24> source)
{
DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!");
DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!");
DebugGuard.IsTrue(source.Length <= redChannel.Length, nameof(source), "'source' span should not be bigger than the destination channels!");
ref float r = ref MemoryMarshal.GetReference(redChannel);
ref float g = ref MemoryMarshal.GetReference(greenChannel);
ref float b = ref MemoryMarshal.GetReference(blueChannel);
ref Rgb24 rgb = ref MemoryMarshal.GetReference(source);
for (int i = 0; i < source.Length; i++)
{
ref Rgb24 src = ref Unsafe.Add(ref rgb, i);
Unsafe.Add(ref r, i) = src.R;
Unsafe.Add(ref g, i) = src.G;
Unsafe.Add(ref b, i) = src.B;
}
}
} }
} }

15
src/ImageSharp/Formats/Jpeg/Components/Block8x8.cs

@ -122,6 +122,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
} }
} }
public static Block8x8 Load(ReadOnlySpan<byte> data)
{
Unsafe.SkipInit(out Block8x8 result);
result.LoadFrom(data);
return result;
}
public void LoadFrom(ReadOnlySpan<byte> source)
{
for (int i = 0; i < Size; i++)
{
this[i] = source[i];
}
}
/// <summary> /// <summary>
/// Load raw 16bit integers from source. /// Load raw 16bit integers from source.
/// </summary> /// </summary>

159
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs

@ -0,0 +1,159 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
// ReSharper disable UseObjectOrCollectionInitializer
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal partial struct Block8x8F
{
[MethodImpl(InliningOptions.ShortMethod)]
public void ScaledCopyFrom(ref float areaOrigin, int areaStride) =>
CopyFrom1x1Scale(ref Unsafe.As<float, byte>(ref areaOrigin), ref Unsafe.As<Block8x8F, byte>(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<Block8x8F, byte>(ref this), ref Unsafe.As<float, byte>(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<float, Vector2>(ref areaOrigin);
int destStride = (int)((uint)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, nint row, nint destStride)
{
ref Vector4 sLeft = ref Unsafe.Add(ref selfBase, 2 * row);
ref Vector4 sRight = ref Unsafe.Add(ref sLeft, 1);
nint offset = 2 * row * destStride;
ref Vector4 dTopLeft = ref Unsafe.As<Vector2, Vector4>(ref Unsafe.Add(ref destBase, offset));
ref Vector4 dBottomLeft = ref Unsafe.As<Vector2, Vector4>(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];
nint baseIdx = (yy * areaStride) + xx;
for (nint i = 0; i < verticalScale; i++, baseIdx += areaStride)
{
for (nint 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));
}
}
}
}

147
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopyTo.cs

@ -1,147 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
// ReSharper disable UseObjectOrCollectionInitializer
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal partial struct Block8x8F
{
/// <summary>
/// Copy block data into the destination color buffer pixel area with the provided horizontal and vertical scale factors.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void ScaledCopyTo(in Buffer2DRegion<float> region, int horizontalScale, int verticalScale)
{
ref float areaOrigin = ref region.GetReferenceToOrigin();
this.ScaledCopyTo(ref areaOrigin, region.Stride, horizontalScale, verticalScale);
}
[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<Block8x8F, byte>(ref this);
ref byte destBase = ref Unsafe.As<float, byte>(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<float, Vector2>(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<Vector2, Vector4>(ref Unsafe.Add(ref destBase, offset));
ref Vector4 dBottomLeft = ref Unsafe.As<Vector2, Vector4>(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;
}
}
}
}
}
}
}

13
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs

@ -352,6 +352,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
} }
} }
public void DE_NormalizeColors(float maximum)
{
if (SimdUtils.HasVector8)
{
this.NormalizeColorsAndRoundInPlaceVector8(maximum);
}
else
{
this.NormalizeColorsInPlace(maximum);
this.RoundInPlace();
}
}
/// <summary> /// <summary>
/// Rounds all values in the block. /// Rounds all values in the block.
/// </summary> /// </summary>

99
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykAvx.cs

@ -0,0 +1,99 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class CmykAvx : JpegColorConverterAvx
{
public CmykAvx(int precision)
: base(JpegColorSpace.Cmyk, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector256<float> c0Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> c1Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector256<float> c2Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector256<float> c3Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component3));
// Used for the color conversion
var scale = Vector256.Create(1 / (this.MaximumValue * this.MaximumValue));
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector256<float> c = ref Unsafe.Add(ref c0Base, i);
ref Vector256<float> m = ref Unsafe.Add(ref c1Base, i);
ref Vector256<float> y = ref Unsafe.Add(ref c2Base, i);
Vector256<float> k = Unsafe.Add(ref c3Base, i);
k = Avx.Multiply(k, scale);
c = Avx.Multiply(c, k);
m = Avx.Multiply(m, k);
y = Avx.Multiply(y, k);
}
}
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
=> ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane);
public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
ref Vector256<float> destC =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> destM =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector256<float> destY =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector256<float> destK =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component3));
ref Vector256<float> srcR =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(rLane));
ref Vector256<float> srcG =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(gLane));
ref Vector256<float> srcB =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(bLane));
var scale = Vector256.Create(maxValue);
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
Vector256<float> ctmp = Avx.Subtract(scale, Unsafe.Add(ref srcR, i));
Vector256<float> mtmp = Avx.Subtract(scale, Unsafe.Add(ref srcG, i));
Vector256<float> ytmp = Avx.Subtract(scale, Unsafe.Add(ref srcB, i));
Vector256<float> ktmp = Avx.Min(ctmp, Avx.Min(mtmp, ytmp));
Vector256<float> kMask = Avx.CompareNotEqual(ktmp, scale);
ctmp = Avx.And(Avx.Divide(Avx.Subtract(ctmp, ktmp), Avx.Subtract(scale, ktmp)), kMask);
mtmp = Avx.And(Avx.Divide(Avx.Subtract(mtmp, ktmp), Avx.Subtract(scale, ktmp)), kMask);
ytmp = Avx.And(Avx.Divide(Avx.Subtract(ytmp, ktmp), Avx.Subtract(scale, ktmp)), kMask);
Unsafe.Add(ref destC, i) = Avx.Subtract(scale, Avx.Multiply(ctmp, scale));
Unsafe.Add(ref destM, i) = Avx.Subtract(scale, Avx.Multiply(mtmp, scale));
Unsafe.Add(ref destY, i) = Avx.Subtract(scale, Avx.Multiply(ytmp, scale));
Unsafe.Add(ref destK, i) = Avx.Subtract(scale, ktmp);
}
}
}
}
}
#endif

82
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.cs

@ -0,0 +1,82 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class CmykScalar : JpegColorConverterScalar
{
public CmykScalar(int precision)
: base(JpegColorSpace.Cmyk, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values) =>
ConvertToRgbInplace(values, this.MaximumValue);
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> ConvertFromRgb(values, this.MaximumValue, r, g, b);
public static void ConvertToRgbInplace(in ComponentValues values, float maxValue)
{
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
Span<float> c3 = values.Component3;
float scale = 1 / (maxValue * maxValue);
for (int i = 0; i < c0.Length; i++)
{
float c = c0[i];
float m = c1[i];
float y = c2[i];
float k = c3[i];
k *= scale;
c0[i] = c * k;
c1[i] = m * k;
c2[i] = y * k;
}
}
public static void ConvertFromRgb(in ComponentValues values, float maxValue, Span<float> r, Span<float> g, Span<float> b)
{
Span<float> c = values.Component0;
Span<float> m = values.Component1;
Span<float> y = values.Component2;
Span<float> k = values.Component3;
for (int i = 0; i < c.Length; i++)
{
float ctmp = 255f - r[i];
float mtmp = 255f - g[i];
float ytmp = 255f - b[i];
float ktmp = MathF.Min(MathF.Min(ctmp, mtmp), ytmp);
if (ktmp >= 255f)
{
ctmp = 0f;
mtmp = 0f;
ytmp = 0f;
}
else
{
ctmp = (ctmp - ktmp) / (255f - ktmp);
mtmp = (mtmp - ktmp) / (255f - ktmp);
ytmp = (ytmp - ktmp) / (255f - ktmp);
}
c[i] = maxValue - (ctmp * maxValue);
m[i] = maxValue - (mtmp * maxValue);
y[i] = maxValue - (ytmp * maxValue);
k[i] = maxValue - ktmp;
}
}
}
}
}

106
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector.cs

@ -0,0 +1,106 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class CmykVector : JpegColorConverterVector
{
public CmykVector(int precision)
: base(JpegColorSpace.Cmyk, precision)
{
}
/// <inheritdoc/>
protected override void ConvertToRgbInplaceVectorized(in ComponentValues values)
{
ref Vector<float> cBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> mBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector<float> yBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector<float> kBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component3));
var scale = new Vector<float>(1 / (this.MaximumValue * this.MaximumValue));
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector<float> c = ref Unsafe.Add(ref cBase, i);
ref Vector<float> m = ref Unsafe.Add(ref mBase, i);
ref Vector<float> y = ref Unsafe.Add(ref yBase, i);
Vector<float> k = Unsafe.Add(ref kBase, i);
k *= scale;
c *= k;
m *= k;
y *= k;
}
}
/// <inheritdoc/>
protected override void ConvertToRgbInplaceScalarRemainder(in ComponentValues values)
=> CmykScalar.ConvertToRgbInplace(values, this.MaximumValue);
/// <inheritdoc/>
protected override void ConvertFromRgbVectorized(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> ConvertFromRgbInplaceVectorized(in values, this.MaximumValue, r, g, b);
/// <inheritdoc/>
protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> ConvertFromRgbInplaceRemainder(values, this.MaximumValue, r, g, b);
public static void ConvertFromRgbInplaceVectorized(in ComponentValues values, float maxValue, Span<float> r, Span<float> g, Span<float> b)
{
ref Vector<float> destC =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> destM =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector<float> destY =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector<float> destK =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component3));
ref Vector<float> srcR =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(r));
ref Vector<float> srcG =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(g));
ref Vector<float> srcB =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(b));
// Used for the color conversion
var scale = new Vector<float>(maxValue);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
Vector<float> ctmp = scale - Unsafe.Add(ref srcR, i);
Vector<float> mtmp = scale - Unsafe.Add(ref srcG, i);
Vector<float> ytmp = scale - Unsafe.Add(ref srcB, i);
Vector<float> ktmp = Vector.Min(ctmp, Vector.Min(mtmp, ytmp));
var kMask = Vector.Equals(ktmp, scale);
ctmp = Vector.AndNot((ctmp - ktmp) / (scale - ktmp), kMask.As<int, float>());
mtmp = Vector.AndNot((mtmp - ktmp) / (scale - ktmp), kMask.As<int, float>());
ytmp = Vector.AndNot((ytmp - ktmp) / (scale - ktmp), kMask.As<int, float>());
Unsafe.Add(ref destC, i) = scale - (ctmp * scale);
Unsafe.Add(ref destM, i) = scale - (mtmp * scale);
Unsafe.Add(ref destY, i) = scale - (ytmp * scale);
Unsafe.Add(ref destK, i) = scale - ktmp;
}
}
public static void ConvertFromRgbInplaceRemainder(in ComponentValues values, float maxValue, Span<float> r, Span<float> g, Span<float> b)
=> CmykScalar.ConvertFromRgb(values, maxValue, r, g, b);
}
}
}

72
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleAvx.cs

@ -0,0 +1,72 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using static SixLabors.ImageSharp.SimdUtils;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class GrayscaleAvx : JpegColorConverterAvx
{
public GrayscaleAvx(int precision)
: base(JpegColorSpace.Grayscale, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector256<float> c0Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
// Used for the color conversion
var scale = Vector256.Create(1 / this.MaximumValue);
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector256<float> c0 = ref Unsafe.Add(ref c0Base, i);
c0 = Avx.Multiply(c0, scale);
}
}
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
ref Vector256<float> destLuminance =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> srcRed =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(rLane));
ref Vector256<float> srcGreen =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(gLane));
ref Vector256<float> srcBlue =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(bLane));
// Used for the color conversion
var f0299 = Vector256.Create(0.299f);
var f0587 = Vector256.Create(0.587f);
var f0114 = Vector256.Create(0.114f);
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector256<float> r = ref Unsafe.Add(ref srcRed, i);
ref Vector256<float> g = ref Unsafe.Add(ref srcGreen, i);
ref Vector256<float> b = ref Unsafe.Add(ref srcBlue, i);
// luminocity = (0.299 * r) + (0.587 * g) + (0.114 * b)
Unsafe.Add(ref destLuminance, i) = HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(Avx.Multiply(f0114, b), f0587, g), f0299, r);
}
}
}
}
}
#endif

51
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.cs

@ -0,0 +1,51 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class GrayscaleScalar : JpegColorConverterScalar
{
public GrayscaleScalar(int precision)
: base(JpegColorSpace.Grayscale, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
=> ConvertToRgbInplace(values.Component0, this.MaximumValue);
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> ConvertCoreInplaceFromRgb(values, r, g, b);
internal static void ConvertToRgbInplace(Span<float> values, float maxValue)
{
ref float valuesRef = ref MemoryMarshal.GetReference(values);
float scale = 1 / maxValue;
for (nint i = 0; i < values.Length; i++)
{
Unsafe.Add(ref valuesRef, i) *= scale;
}
}
internal static void ConvertCoreInplaceFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
Span<float> c0 = values.Component0;
for (int i = 0; i < c0.Length; i++)
{
// luminocity = (0.299 * r) + (0.587 * g) + (0.114 * b)
float luma = (0.299f * rLane[i]) + (0.587f * gLane[i]) + (0.114f * bLane[i]);
c0[i] = luma;
}
}
}
}
}

74
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector.cs

@ -0,0 +1,74 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class GrayScaleVector : JpegColorConverterVector
{
public GrayScaleVector(int precision)
: base(JpegColorSpace.Grayscale, precision)
{
}
/// <inheritdoc/>
protected override void ConvertToRgbInplaceVectorized(in ComponentValues values)
{
ref Vector<float> cBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
var scale = new Vector<float>(1 / this.MaximumValue);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector<float> c0 = ref Unsafe.Add(ref cBase, i);
c0 *= scale;
}
}
/// <inheritdoc/>
protected override void ConvertToRgbInplaceScalarRemainder(in ComponentValues values)
=> GrayscaleScalar.ConvertToRgbInplace(values.Component0, this.MaximumValue);
/// <inheritdoc/>
protected override void ConvertFromRgbVectorized(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
ref Vector<float> destLuma =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> srcR =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(rLane));
ref Vector<float> srcG =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(gLane));
ref Vector<float> srcB =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(bLane));
var rMult = new Vector<float>(0.299f);
var gMult = new Vector<float>(0.587f);
var bMult = new Vector<float>(0.114f);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
Vector<float> r = Unsafe.Add(ref srcR, i);
Vector<float> g = Unsafe.Add(ref srcR, i);
Vector<float> b = Unsafe.Add(ref srcR, i);
// luminocity = (0.299 * r) + (0.587 * g) + (0.114 * b)
Unsafe.Add(ref destLuma, i) = (rMult * r) + (gMult * g) + (bMult * b);
}
}
/// <inheritdoc/>
protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> GrayscaleScalar.ConvertCoreInplaceFromRgb(values, r, g, b);
}
}
}

16
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgbAvx.cs → src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbAvx.cs

@ -2,22 +2,24 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS #if SUPPORTS_RUNTIME_INTRINSICS
using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{ {
internal abstract partial class JpegColorConverterBase internal abstract partial class JpegColorConverterBase
{ {
internal sealed class FromRgbAvx : JpegColorConverterAvx internal sealed class RgbAvx : JpegColorConverterAvx
{ {
public FromRgbAvx(int precision) public RgbAvx(int precision)
: base(JpegColorSpace.RGB, precision) : base(JpegColorSpace.RGB, precision)
{ {
} }
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values) public override void ConvertToRgbInplace(in ComponentValues values)
{ {
ref Vector256<float> rBase = ref Vector256<float> rBase =
@ -40,6 +42,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
b = Avx.Multiply(b, scale); b = Avx.Multiply(b, scale);
} }
} }
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
rLane.CopyTo(values.Component0);
gLane.CopyTo(values.Component1);
bLane.CopyTo(values.Component2);
}
} }
} }
} }

40
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class RgbScalar : JpegColorConverterScalar
{
public RgbScalar(int precision)
: base(JpegColorSpace.RGB, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
=> ConvertToRgbInplace(values, this.MaximumValue);
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> ConvertFromRgb(values, r, g, b);
internal static void ConvertToRgbInplace(ComponentValues values, float maxValue)
{
GrayscaleScalar.ConvertToRgbInplace(values.Component0, maxValue);
GrayscaleScalar.ConvertToRgbInplace(values.Component1, maxValue);
GrayscaleScalar.ConvertToRgbInplace(values.Component2, maxValue);
}
internal static void ConvertFromRgb(ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
{
r.CopyTo(values.Component0);
g.CopyTo(values.Component1);
b.CopyTo(values.Component2);
}
}
}
}

27
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgbVector.cs → src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector.cs

@ -1,22 +1,24 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{ {
internal abstract partial class JpegColorConverterBase internal abstract partial class JpegColorConverterBase
{ {
internal sealed class FromRgbVector : JpegColorConverterVector internal sealed class RgbVector : JpegColorConverterVector
{ {
public FromRgbVector(int precision) public RgbVector(int precision)
: base(JpegColorSpace.RGB, precision) : base(JpegColorSpace.RGB, precision)
{ {
} }
protected override void ConvertCoreVectorizedInplace(in ComponentValues values) /// <inheritdoc/>
protected override void ConvertToRgbInplaceVectorized(in ComponentValues values)
{ {
ref Vector<float> rBase = ref Vector<float> rBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0)); ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
@ -39,8 +41,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
} }
} }
protected override void ConvertCoreInplace(in ComponentValues values) => /// <inheritdoc/>
FromRgbScalar.ConvertCoreInplace(values, this.MaximumValue); protected override void ConvertToRgbInplaceScalarRemainder(in ComponentValues values)
=> RgbScalar.ConvertToRgbInplace(values, this.MaximumValue);
/// <inheritdoc/>
protected override void ConvertFromRgbVectorized(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
{
r.CopyTo(values.Component0);
g.CopyTo(values.Component1);
b.CopyTo(values.Component2);
}
/// <inheritdoc/>
protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> RgbScalar.ConvertFromRgb(values, r, g, b);
} }
} }
} }

125
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrAvx.cs

@ -0,0 +1,125 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using static SixLabors.ImageSharp.SimdUtils;
// ReSharper disable ImpureMethodCallOnReadonlyValueField
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class YCbCrAvx : JpegColorConverterAvx
{
public YCbCrAvx(int precision)
: base(JpegColorSpace.YCbCr, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector256<float> c0Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> c1Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector256<float> c2Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component2));
// Used for the color conversion
var chromaOffset = Vector256.Create(-this.HalfValue);
var scale = Vector256.Create(1 / this.MaximumValue);
var rCrMult = Vector256.Create(YCbCrScalar.RCrMult);
var gCbMult = Vector256.Create(-YCbCrScalar.GCbMult);
var gCrMult = Vector256.Create(-YCbCrScalar.GCrMult);
var bCbMult = Vector256.Create(YCbCrScalar.BCbMult);
// Walking 8 elements at one step:
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
ref Vector256<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector256<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector256<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector256<float> y = c0;
Vector256<float> cb = Avx.Add(c1, chromaOffset);
Vector256<float> cr = Avx.Add(c2, chromaOffset);
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector256<float> r = HwIntrinsics.MultiplyAdd(y, cr, rCrMult);
Vector256<float> g = HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(y, cb, gCbMult), cr, gCrMult);
Vector256<float> b = HwIntrinsics.MultiplyAdd(y, cb, bCbMult);
r = Avx.Multiply(Avx.RoundToNearestInteger(r), scale);
g = Avx.Multiply(Avx.RoundToNearestInteger(g), scale);
b = Avx.Multiply(Avx.RoundToNearestInteger(b), scale);
c0 = r;
c1 = g;
c2 = b;
}
}
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
ref Vector256<float> destY =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> destCb =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector256<float> destCr =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector256<float> srcR =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(rLane));
ref Vector256<float> srcG =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(gLane));
ref Vector256<float> srcB =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(bLane));
// Used for the color conversion
var chromaOffset = Vector256.Create(this.HalfValue);
var f0299 = Vector256.Create(0.299f);
var f0587 = Vector256.Create(0.587f);
var f0114 = Vector256.Create(0.114f);
var fn0168736 = Vector256.Create(-0.168736f);
var fn0331264 = Vector256.Create(-0.331264f);
var fn0418688 = Vector256.Create(-0.418688f);
var fn0081312F = Vector256.Create(-0.081312F);
var f05 = Vector256.Create(0.5f);
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
Vector256<float> r = Unsafe.Add(ref srcR, i);
Vector256<float> g = Unsafe.Add(ref srcG, i);
Vector256<float> b = Unsafe.Add(ref srcB, i);
// y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b)
// cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b)
// cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b)
Vector256<float> y = HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(Avx.Multiply(f0114, b), f0587, g), f0299, r);
Vector256<float> cb = Avx.Add(chromaOffset, HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(Avx.Multiply(f05, b), fn0331264, g), fn0168736, r));
Vector256<float> cr = Avx.Add(chromaOffset, HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(Avx.Multiply(fn0081312F, b), fn0418688, g), f05, r));
Unsafe.Add(ref destY, i) = y;
Unsafe.Add(ref destCb, i) = cb;
Unsafe.Add(ref destCr, i) = cr;
}
}
}
}
}
#endif

76
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs

@ -0,0 +1,76 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class YCbCrScalar : JpegColorConverterScalar
{
// derived from ITU-T Rec. T.871
internal const float RCrMult = 1.402f;
internal const float GCbMult = (float)(0.114 * 1.772 / 0.587);
internal const float GCrMult = (float)(0.299 * 1.402 / 0.587);
internal const float BCbMult = 1.772f;
public YCbCrScalar(int precision)
: base(JpegColorSpace.YCbCr, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
=> ConvertToRgbInplace(values, this.MaximumValue, this.HalfValue);
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> ConvertFromRgb(values, this.HalfValue, r, g, b);
public static void ConvertToRgbInplace(in ComponentValues values, float maxValue, float halfValue)
{
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
float scale = 1 / maxValue;
for (int i = 0; i < c0.Length; i++)
{
float y = c0[i];
float cb = c1[i] - halfValue;
float cr = c2[i] - halfValue;
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
c0[i] = MathF.Round(y + (RCrMult * cr), MidpointRounding.AwayFromZero) * scale;
c1[i] = MathF.Round(y - (GCbMult * cb) - (GCrMult * cr), MidpointRounding.AwayFromZero) * scale;
c2[i] = MathF.Round(y + (BCbMult * cb), MidpointRounding.AwayFromZero) * scale;
}
}
public static void ConvertFromRgb(in ComponentValues values, float halfValue, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
Span<float> y = values.Component0;
Span<float> cb = values.Component1;
Span<float> cr = values.Component2;
for (int i = 0; i < y.Length; i++)
{
float r = rLane[i];
float g = gLane[i];
float b = bLane[i];
// y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b)
// cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b)
// cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b)
y[i] = (0.299f * r) + (0.587f * g) + (0.114f * b);
cb[i] = halfValue - (0.168736f * r) - (0.331264f * g) + (0.5f * b);
cr[i] = halfValue + (0.5f * r) - (0.418688f * g) - (0.081312f * b);
}
}
}
}
}

128
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector.cs

@ -0,0 +1,128 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// ReSharper disable ImpureMethodCallOnReadonlyValueField
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class YCbCrVector : JpegColorConverterVector
{
public YCbCrVector(int precision)
: base(JpegColorSpace.YCbCr, precision)
{
}
/// <inheritdoc/>
protected override void ConvertToRgbInplaceVectorized(in ComponentValues values)
{
ref Vector<float> c0Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> c1Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector<float> c2Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component2));
var chromaOffset = new Vector<float>(-this.HalfValue);
var scale = new Vector<float>(1 / this.MaximumValue);
var rCrMult = new Vector<float>(YCbCrScalar.RCrMult);
var gCbMult = new Vector<float>(-YCbCrScalar.GCbMult);
var gCrMult = new Vector<float>(-YCbCrScalar.GCrMult);
var bCbMult = new Vector<float>(YCbCrScalar.BCbMult);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
ref Vector<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector<float> y = Unsafe.Add(ref c0Base, i);
Vector<float> cb = Unsafe.Add(ref c1Base, i) + chromaOffset;
Vector<float> cr = Unsafe.Add(ref c2Base, i) + chromaOffset;
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector<float> r = y + (cr * rCrMult);
Vector<float> g = y + (cb * gCbMult) + (cr * gCrMult);
Vector<float> b = y + (cb * bCbMult);
r = r.FastRound();
g = g.FastRound();
b = b.FastRound();
r *= scale;
g *= scale;
b *= scale;
c0 = r;
c1 = g;
c2 = b;
}
}
/// <inheritdoc/>
protected override void ConvertToRgbInplaceScalarRemainder(in ComponentValues values)
=> YCbCrScalar.ConvertToRgbInplace(values, this.MaximumValue, this.HalfValue);
/// <inheritdoc/>
protected override void ConvertFromRgbVectorized(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
ref Vector<float> destY =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> destCb =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector<float> destCr =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector<float> srcR =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(rLane));
ref Vector<float> srcG =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(gLane));
ref Vector<float> srcB =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(bLane));
var chromaOffset = new Vector<float>(this.HalfValue);
var rYMult = new Vector<float>(0.299f);
var gYMult = new Vector<float>(0.587f);
var bYMult = new Vector<float>(0.114f);
var rCbMult = new Vector<float>(0.168736f);
var gCbMult = new Vector<float>(0.331264f);
var bCbMult = new Vector<float>(0.5f);
var rCrMult = new Vector<float>(0.5f);
var gCrMult = new Vector<float>(0.418688f);
var bCrMult = new Vector<float>(0.081312f);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
Vector<float> r = Unsafe.Add(ref srcR, i);
Vector<float> g = Unsafe.Add(ref srcG, i);
Vector<float> b = Unsafe.Add(ref srcB, i);
// y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b)
// cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b)
// cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b)
Unsafe.Add(ref destY, i) = (rYMult * r) + (gYMult * g) + (bYMult * b);
Unsafe.Add(ref destCb, i) = chromaOffset - (rCbMult * r) - (gCbMult * g) + (bCbMult * b);
Unsafe.Add(ref destCr, i) = chromaOffset + (rCrMult * r) - (gCrMult * g) - (bCrMult * b);
}
}
/// <inheritdoc/>
protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> YCbCrScalar.ConvertFromRgb(values, this.HalfValue, r, g, b);
}
}
}

136
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKAvx.cs

@ -0,0 +1,136 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using static SixLabors.ImageSharp.SimdUtils;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class YccKAvx : JpegColorConverterAvx
{
public YccKAvx(int precision)
: base(JpegColorSpace.Ycck, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector256<float> c0Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> c1Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector256<float> c2Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector256<float> kBase =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component3));
// Used for the color conversion
var chromaOffset = Vector256.Create(-this.HalfValue);
var scale = Vector256.Create(1 / (this.MaximumValue * this.MaximumValue));
var max = Vector256.Create(this.MaximumValue);
var rCrMult = Vector256.Create(YCbCrScalar.RCrMult);
var gCbMult = Vector256.Create(-YCbCrScalar.GCbMult);
var gCrMult = Vector256.Create(-YCbCrScalar.GCrMult);
var bCbMult = Vector256.Create(YCbCrScalar.BCbMult);
// Walking 8 elements at one step:
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
// k = kVals[i] / 256F;
ref Vector256<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector256<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector256<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector256<float> y = c0;
Vector256<float> cb = Avx.Add(c1, chromaOffset);
Vector256<float> cr = Avx.Add(c2, chromaOffset);
Vector256<float> scaledK = Avx.Multiply(Unsafe.Add(ref kBase, i), scale);
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector256<float> r = HwIntrinsics.MultiplyAdd(y, cr, rCrMult);
Vector256<float> g =
HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(y, cb, gCbMult), cr, gCrMult);
Vector256<float> b = HwIntrinsics.MultiplyAdd(y, cb, bCbMult);
r = Avx.Subtract(max, Avx.RoundToNearestInteger(r));
g = Avx.Subtract(max, Avx.RoundToNearestInteger(g));
b = Avx.Subtract(max, Avx.RoundToNearestInteger(b));
r = Avx.Multiply(r, scaledK);
g = Avx.Multiply(g, scaledK);
b = Avx.Multiply(b, scaledK);
c0 = r;
c1 = g;
c2 = b;
}
}
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
// rgb -> cmyk
CmykAvx.ConvertFromRgb(in values, this.MaximumValue, rLane, gLane, bLane);
// cmyk -> ycck
ref Vector256<float> destY =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> destCb =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector256<float> destCr =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector256<float> srcR = ref destY;
ref Vector256<float> srcG = ref destCb;
ref Vector256<float> srcB = ref destCr;
// Used for the color conversion
var maxSampleValue = Vector256.Create(this.MaximumValue);
var chromaOffset = Vector256.Create(this.HalfValue);
var f0299 = Vector256.Create(0.299f);
var f0587 = Vector256.Create(0.587f);
var f0114 = Vector256.Create(0.114f);
var fn0168736 = Vector256.Create(-0.168736f);
var fn0331264 = Vector256.Create(-0.331264f);
var fn0418688 = Vector256.Create(-0.418688f);
var fn0081312F = Vector256.Create(-0.081312F);
var f05 = Vector256.Create(0.5f);
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
Vector256<float> r = Avx.Subtract(maxSampleValue, Unsafe.Add(ref srcR, i));
Vector256<float> g = Avx.Subtract(maxSampleValue, Unsafe.Add(ref srcG, i));
Vector256<float> b = Avx.Subtract(maxSampleValue, Unsafe.Add(ref srcB, i));
// y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b)
// cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b)
// cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b)
Vector256<float> y = HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(Avx.Multiply(f0114, b), f0587, g), f0299, r);
Vector256<float> cb = Avx.Add(chromaOffset, HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(Avx.Multiply(f05, b), fn0331264, g), fn0168736, r));
Vector256<float> cr = Avx.Add(chromaOffset, HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(Avx.Multiply(fn0081312F, b), fn0418688, g), f05, r));
Unsafe.Add(ref destY, i) = y;
Unsafe.Add(ref destCb, i) = cb;
Unsafe.Add(ref destCr, i) = cr;
}
}
}
}
}
#endif

80
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs

@ -0,0 +1,80 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class YccKScalar : JpegColorConverterScalar
{
// derived from ITU-T Rec. T.871
internal const float RCrMult = 1.402f;
internal const float GCbMult = (float)(0.114 * 1.772 / 0.587);
internal const float GCrMult = (float)(0.299 * 1.402 / 0.587);
internal const float BCbMult = 1.772f;
public YccKScalar(int precision)
: base(JpegColorSpace.Ycck, precision)
{
}
/// <inheritdoc/>
public override void ConvertToRgbInplace(in ComponentValues values)
=> ConvertToRgpInplace(values, this.MaximumValue, this.HalfValue);
/// <inheritdoc/>
public override void ConvertFromRgb(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
=> ConvertFromRgb(values, this.HalfValue, this.MaximumValue, r, g, b);
public static void ConvertToRgpInplace(in ComponentValues values, float maxValue, float halfValue)
{
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
Span<float> c3 = values.Component3;
float scale = 1 / (maxValue * maxValue);
for (int i = 0; i < values.Component0.Length; i++)
{
float y = c0[i];
float cb = c1[i] - halfValue;
float cr = c2[i] - halfValue;
float scaledK = c3[i] * scale;
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
c0[i] = (maxValue - MathF.Round(y + (RCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK;
c1[i] = (maxValue - MathF.Round(y - (GCbMult * cb) - (GCrMult * cr), MidpointRounding.AwayFromZero)) * scaledK;
c2[i] = (maxValue - MathF.Round(y + (BCbMult * cb), MidpointRounding.AwayFromZero)) * scaledK;
}
}
public static void ConvertFromRgb(in ComponentValues values, float halfValue, float maxValue, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
// rgb -> cmyk
CmykScalar.ConvertFromRgb(in values, maxValue, rLane, gLane, bLane);
// cmyk -> ycck
Span<float> c = values.Component0;
Span<float> m = values.Component1;
Span<float> y = values.Component2;
for (int i = 0; i < y.Length; i++)
{
float r = maxValue - c[i];
float g = maxValue - m[i];
float b = maxValue - y[i];
// k value is passed untouched from rgb -> cmyk conversion
c[i] = (0.299f * r) + (0.587f * g) + (0.114f * b);
m[i] = halfValue - (0.168736f * r) - (0.331264f * g) + (0.5f * b);
y[i] = halfValue + (0.5f * r) - (0.418688f * g) - (0.081312f * b);
}
}
}
}
}

138
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector.cs

@ -0,0 +1,138 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class YccKVector : JpegColorConverterVector
{
public YccKVector(int precision)
: base(JpegColorSpace.Ycck, precision)
{
}
/// <inheritdoc/>
protected override void ConvertToRgbInplaceVectorized(in ComponentValues values)
{
ref Vector<float> c0Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> c1Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector<float> c2Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector<float> kBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component3));
var chromaOffset = new Vector<float>(-this.HalfValue);
var scale = new Vector<float>(1 / (this.MaximumValue * this.MaximumValue));
var max = new Vector<float>(this.MaximumValue);
var rCrMult = new Vector<float>(YCbCrScalar.RCrMult);
var gCbMult = new Vector<float>(-YCbCrScalar.GCbMult);
var gCrMult = new Vector<float>(-YCbCrScalar.GCrMult);
var bCbMult = new Vector<float>(YCbCrScalar.BCbMult);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
// k = kVals[i] / 256F;
ref Vector<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector<float> y = c0;
Vector<float> cb = c1 + chromaOffset;
Vector<float> cr = c2 + chromaOffset;
Vector<float> scaledK = Unsafe.Add(ref kBase, i) * scale;
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector<float> r = y + (cr * rCrMult);
Vector<float> g = y + (cb * gCbMult) + (cr * gCrMult);
Vector<float> b = y + (cb * bCbMult);
r = (max - r.FastRound()) * scaledK;
g = (max - g.FastRound()) * scaledK;
b = (max - b.FastRound()) * scaledK;
c0 = r;
c1 = g;
c2 = b;
}
}
/// <inheritdoc/>
protected override void ConvertToRgbInplaceScalarRemainder(in ComponentValues values)
=> YccKScalar.ConvertToRgpInplace(values, this.MaximumValue, this.HalfValue);
/// <inheritdoc/>
protected override void ConvertFromRgbVectorized(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane)
{
// rgb -> cmyk
CmykVector.ConvertFromRgbInplaceVectorized(in values, this.MaximumValue, rLane, gLane, bLane);
// cmyk -> ycck
ref Vector<float> destY =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> destCb =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector<float> destCr =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector<float> srcR = ref destY;
ref Vector<float> srcG = ref destCb;
ref Vector<float> srcB = ref destCr;
var maxSampleValue = new Vector<float>(this.MaximumValue);
var chromaOffset = new Vector<float>(this.HalfValue);
var rYMult = new Vector<float>(0.299f);
var gYMult = new Vector<float>(0.587f);
var bYMult = new Vector<float>(0.114f);
var rCbMult = new Vector<float>(0.168736f);
var gCbMult = new Vector<float>(0.331264f);
var bCbMult = new Vector<float>(0.5f);
var rCrMult = new Vector<float>(0.5f);
var gCrMult = new Vector<float>(0.418688f);
var bCrMult = new Vector<float>(0.081312f);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
Vector<float> r = maxSampleValue - Unsafe.Add(ref srcR, i);
Vector<float> g = maxSampleValue - Unsafe.Add(ref srcG, i);
Vector<float> b = maxSampleValue - Unsafe.Add(ref srcB, i);
// y = 0 + (0.299 * r) + (0.587 * g) + (0.114 * b)
// cb = 128 - (0.168736 * r) - (0.331264 * g) + (0.5 * b)
// cr = 128 + (0.5 * r) - (0.418688 * g) - (0.081312 * b)
Unsafe.Add(ref destY, i) = (rYMult * r) + (gYMult * g) + (bYMult * b);
Unsafe.Add(ref destCb, i) = chromaOffset - (rCbMult * r) - (gCbMult * g) + (bCbMult * b);
Unsafe.Add(ref destCr, i) = chromaOffset + (rCrMult * r) - (gCrMult * g) - (bCrMult * b);
}
}
/// <inheritdoc/>
protected override void ConvertFromRgbScalarRemainder(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
{
// rgb -> cmyk
CmykScalar.ConvertFromRgb(in values, this.MaximumValue, r, g, b);
// cmyk -> ycck
YccKScalar.ConvertFromRgb(in values, this.HalfValue, this.MaximumValue, r, g, b);
}
}
}
}

6
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs → src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterAvx.cs

@ -4,7 +4,7 @@
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{ {
internal abstract partial class JpegColorConverterBase internal abstract partial class JpegColorConverterBase
{ {
@ -26,7 +26,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{ {
} }
public sealed override bool IsAvailable => Avx.IsSupported; public static bool IsSupported => Avx.IsSupported;
public sealed override bool IsAvailable => IsSupported;
public sealed override int ElementsPerBatch => Vector256<float>.Count; public sealed override int ElementsPerBatch => Vector256<float>.Count;
} }

150
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs → src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterBase.cs

@ -3,10 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{ {
/// <summary> /// <summary>
/// Encapsulates the conversion of color channels from jpeg image to RGB channels. /// Encapsulates the conversion of color channels from jpeg image to RGB channels.
@ -88,88 +87,130 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
/// <param name="values">The input/ouptut as a stack-only <see cref="ComponentValues"/> struct</param> /// <param name="values">The input/ouptut as a stack-only <see cref="ComponentValues"/> struct</param>
public abstract void ConvertToRgbInplace(in ComponentValues values); public abstract void ConvertToRgbInplace(in ComponentValues values);
/// <summary>
/// Converts RGB lanes to jpeg component values.
/// </summary>
/// <param name="values">Jpeg component values.</param>
/// <param name="rLane">Red colors lane.</param>
/// <param name="gLane">Green colors lane.</param>
/// <param name="bLane">Blue colors lane.</param>
public abstract void ConvertFromRgb(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane);
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for all supported colorspaces and precisions. /// Returns the <see cref="JpegColorConverterBase"/>s for all supported colorspaces and precisions.
/// </summary> /// </summary>
private static JpegColorConverterBase[] CreateConverters() private static JpegColorConverterBase[] CreateConverters()
{ {
var converters = new List<JpegColorConverterBase>(); // 5 color types with 2 supported precisions: 8 bit & 12 bit
const int colorConvertersCount = 5 * 2;
var converters = new JpegColorConverterBase[colorConvertersCount];
// 8-bit converters // 8-bit converters
converters.AddRange(GetYCbCrConverters(8)); converters[0] = GetYCbCrConverter(8);
converters.AddRange(GetYccKConverters(8)); converters[1] = GetYccKConverter(8);
converters.AddRange(GetCmykConverters(8)); converters[2] = GetCmykConverter(8);
converters.AddRange(GetGrayScaleConverters(8)); converters[3] = GetGrayScaleConverter(8);
converters.AddRange(GetRgbConverters(8)); converters[4] = GetRgbConverter(8);
// 12-bit converters // 12-bit converters
converters.AddRange(GetYCbCrConverters(12)); converters[5] = GetYCbCrConverter(12);
converters.AddRange(GetYccKConverters(12)); converters[6] = GetYccKConverter(12);
converters.AddRange(GetCmykConverters(12)); converters[7] = GetCmykConverter(12);
converters.AddRange(GetGrayScaleConverters(12)); converters[8] = GetGrayScaleConverter(12);
converters.AddRange(GetRgbConverters(12)); converters[9] = GetRgbConverter(12);
return converters.Where(x => x.IsAvailable).ToArray(); return converters;
} }
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for the YCbCr colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the YCbCr colorspace.
/// </summary> /// </summary>
private static IEnumerable<JpegColorConverterBase> GetYCbCrConverters(int precision) private static JpegColorConverterBase GetYCbCrConverter(int precision)
{ {
#if SUPPORTS_RUNTIME_INTRINSICS if (JpegColorConverterAvx.IsSupported)
yield return new FromYCbCrAvx(precision); {
#endif return new YCbCrAvx(precision);
yield return new FromYCbCrVector(precision); }
yield return new FromYCbCrScalar(precision);
if (JpegColorConverterVector.IsSupported)
{
return new YCbCrVector(precision);
}
return new YCbCrScalar(precision);
} }
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for the YccK colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the YccK colorspace.
/// </summary> /// </summary>
private static IEnumerable<JpegColorConverterBase> GetYccKConverters(int precision) private static JpegColorConverterBase GetYccKConverter(int precision)
{
if (JpegColorConverterAvx.IsSupported)
{
return new YccKAvx(precision);
}
if (JpegColorConverterVector.IsSupported)
{ {
#if SUPPORTS_RUNTIME_INTRINSICS return new YccKVector(precision);
yield return new FromYccKAvx(precision); }
#endif
yield return new FromYccKVector(precision); return new YccKScalar(precision);
yield return new FromYccKScalar(precision);
} }
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for the CMYK colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the CMYK colorspace.
/// </summary> /// </summary>
private static IEnumerable<JpegColorConverterBase> GetCmykConverters(int precision) private static JpegColorConverterBase GetCmykConverter(int precision)
{
if (JpegColorConverterAvx.IsSupported)
{
return new CmykAvx(precision);
}
if (JpegColorConverterVector.IsSupported)
{ {
#if SUPPORTS_RUNTIME_INTRINSICS return new CmykVector(precision);
yield return new FromCmykAvx(precision); }
#endif
yield return new FromCmykVector(precision); return new CmykScalar(precision);
yield return new FromCmykScalar(precision);
} }
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for the gray scale colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the gray scale colorspace.
/// </summary> /// </summary>
private static IEnumerable<JpegColorConverterBase> GetGrayScaleConverters(int precision) private static JpegColorConverterBase GetGrayScaleConverter(int precision)
{
if (JpegColorConverterAvx.IsSupported)
{
return new GrayscaleAvx(precision);
}
if (JpegColorConverterVector.IsSupported)
{ {
#if SUPPORTS_RUNTIME_INTRINSICS return new GrayScaleVector(precision);
yield return new FromGrayscaleAvx(precision); }
#endif
yield return new FromGrayScaleVector(precision); return new GrayscaleScalar(precision);
yield return new FromGrayscaleScalar(precision);
} }
/// <summary> /// <summary>
/// Returns the <see cref="JpegColorConverterBase"/>s for the RGB colorspace. /// Returns the <see cref="JpegColorConverterBase"/>s for the RGB colorspace.
/// </summary> /// </summary>
private static IEnumerable<JpegColorConverterBase> GetRgbConverters(int precision) private static JpegColorConverterBase GetRgbConverter(int precision)
{ {
#if SUPPORTS_RUNTIME_INTRINSICS if (JpegColorConverterAvx.IsSupported)
yield return new FromRgbAvx(precision); {
#endif return new RgbAvx(precision);
yield return new FromRgbVector(precision); }
yield return new FromRgbScalar(precision);
if (JpegColorConverterVector.IsSupported)
{
return new RgbScalar(precision);
}
return new GrayscaleScalar(precision);
} }
/// <summary> /// <summary>
@ -228,7 +269,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
/// </summary> /// </summary>
/// <param name="processors">List of component color processors.</param> /// <param name="processors">List of component color processors.</param>
/// <param name="row">Row to convert</param> /// <param name="row">Row to convert</param>
public ComponentValues(IReadOnlyList<ComponentProcessor> processors, int row) public ComponentValues(IReadOnlyList<Decoder.ComponentProcessor> processors, int row)
{
DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors));
this.ComponentCount = processors.Count;
this.Component0 = processors[0].GetColorBufferRowSpan(row);
// In case of grayscale, Component1 and Component2 point to Component0 memory area
this.Component1 = this.ComponentCount > 1 ? processors[1].GetColorBufferRowSpan(row) : this.Component0;
this.Component2 = this.ComponentCount > 2 ? processors[2].GetColorBufferRowSpan(row) : this.Component0;
this.Component3 = this.ComponentCount > 3 ? processors[3].GetColorBufferRowSpan(row) : Span<float>.Empty;
}
/// <summary>
/// Initializes a new instance of the <see cref="ComponentValues"/> struct.
/// </summary>
/// <param name="processors">List of component color processors.</param>
/// <param name="row">Row to convert</param>
public ComponentValues(IReadOnlyList<Encoder.ComponentProcessor> processors, int row)
{ {
DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors)); DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors));

2
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs → src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{ {
internal abstract partial class JpegColorConverterBase internal abstract partial class JpegColorConverterBase
{ {

131
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.cs

@ -0,0 +1,131 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
internal abstract partial class JpegColorConverterBase
{
/// <summary>
/// <see cref="JpegColorConverterBase"/> abstract base for implementations
/// based on <see cref="Vector"/> API.
/// </summary>
/// <remarks>
/// Converters of this family can work with data of any size.
/// Even though real life data is guaranteed to be of size
/// divisible by 8 newer SIMD instructions like AVX512 won't work with
/// such data out of the box. These converters have fallback code
/// for 'remainder' data.
/// </remarks>
internal abstract class JpegColorConverterVector : JpegColorConverterBase
{
protected JpegColorConverterVector(JpegColorSpace colorSpace, int precision)
: base(colorSpace, precision)
{
}
/// <summary>
/// Gets a value indicating whether this converter is supported on current hardware.
/// </summary>
public static bool IsSupported => Vector.IsHardwareAccelerated && Vector<float>.Count % 4 == 0;
/// <inheritdoc/>
public sealed override bool IsAvailable => IsSupported;
public override int ElementsPerBatch => Vector<float>.Count;
/// <inheritdoc/>
public sealed override void ConvertToRgbInplace(in ComponentValues values)
{
DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware.");
int length = values.Component0.Length;
int remainder = (int)((uint)length % (uint)Vector<float>.Count);
int simdCount = length - remainder;
if (simdCount > 0)
{
this.ConvertToRgbInplaceVectorized(values.Slice(0, simdCount));
}
// Jpeg images width is always divisible by 8 without a remainder
// so it's safe to say SSE/AVX1/AVX2 implementations would never have
// 'remainder' pixels
// But some exotic simd implementations e.g. AVX-512 can have
// remainder pixels
if (remainder > 0)
{
this.ConvertToRgbInplaceScalarRemainder(values.Slice(simdCount, remainder));
}
}
/// <inheritdoc/>
public sealed override void ConvertFromRgb(in ComponentValues values, Span<float> r, Span<float> g, Span<float> b)
{
DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware.");
int length = values.Component0.Length;
int remainder = (int)((uint)length % (uint)Vector<float>.Count);
int simdCount = length - remainder;
if (simdCount > 0)
{
this.ConvertFromRgbVectorized(
values.Slice(0, simdCount),
r.Slice(0, simdCount),
g.Slice(0, simdCount),
b.Slice(0, simdCount));
}
// Jpeg images width is always divisible by 8 without a remainder
// so it's safe to say SSE/AVX1/AVX2 implementations would never have
// 'remainder' pixels
// But some exotic simd implementations e.g. AVX-512 can have
// remainder pixels
if (remainder > 0)
{
this.ConvertFromRgbScalarRemainder(
values.Slice(simdCount, remainder),
r.Slice(simdCount, remainder),
g.Slice(simdCount, remainder),
b.Slice(simdCount, remainder));
}
}
/// <summary>
/// Converts planar jpeg component values in <paramref name="values"/>
/// to RGB color space inplace using <see cref="Vector"/> API.
/// </summary>
/// <param name="values">The input/ouptut as a stack-only <see cref="ComponentValues"/> struct</param>
protected abstract void ConvertToRgbInplaceVectorized(in ComponentValues values);
/// <summary>
/// Converts remainder of the planar jpeg component values after
/// conversion in <see cref="ConvertToRgbInplaceVectorized(in ComponentValues)"/>.
/// </summary>
/// <param name="values">The input/ouptut as a stack-only <see cref="ComponentValues"/> struct</param>
protected abstract void ConvertToRgbInplaceScalarRemainder(in ComponentValues values);
/// <summary>
/// Converts RGB lanes to jpeg component values using <see cref="Vector"/> API.
/// </summary>
/// <param name="values">Jpeg component values.</param>
/// <param name="rLane">Red colors lane.</param>
/// <param name="gLane">Green colors lane.</param>
/// <param name="bLane">Blue colors lane.</param>
protected abstract void ConvertFromRgbVectorized(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane);
/// <summary>
/// Converts remainder of RGB lanes to jpeg component values after
/// conversion in <see cref="ConvertFromRgbVectorized(in ComponentValues, Span{float}, Span{float}, Span{float})"/>.
/// </summary>
/// <param name="values">Jpeg component values.</param>
/// <param name="rLane">Red colors lane.</param>
/// <param name="gLane">Green colors lane.</param>
/// <param name="bLane">Blue colors lane.</param>
protected abstract void ConvertFromRgbScalarRemainder(in ComponentValues values, Span<float> rLane, Span<float> gLane, Span<float> bLane);
}
}
}

52
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmykAvx.cs

@ -1,52 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromCmykAvx : JpegColorConverterAvx
{
public FromCmykAvx(int precision)
: base(JpegColorSpace.Cmyk, precision)
{
}
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector256<float> c0Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> c1Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector256<float> c2Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector256<float> c3Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component3));
// Used for the color conversion
var scale = Vector256.Create(1 / (this.MaximumValue * this.MaximumValue));
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector256<float> c = ref Unsafe.Add(ref c0Base, i);
ref Vector256<float> m = ref Unsafe.Add(ref c1Base, i);
ref Vector256<float> y = ref Unsafe.Add(ref c2Base, i);
Vector256<float> k = Unsafe.Add(ref c3Base, i);
k = Avx.Multiply(k, scale);
c = Avx.Multiply(c, k);
m = Avx.Multiply(m, k);
y = Avx.Multiply(y, k);
}
}
}
}
}
#endif

43
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmykScalar.cs

@ -1,43 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromCmykScalar : JpegColorConverterScalar
{
public FromCmykScalar(int precision)
: base(JpegColorSpace.Cmyk, precision)
{
}
public override void ConvertToRgbInplace(in ComponentValues values) =>
ConvertCoreInplace(values, this.MaximumValue);
internal static void ConvertCoreInplace(in ComponentValues values, float maxValue)
{
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
Span<float> c3 = values.Component3;
float scale = 1 / (maxValue * maxValue);
for (int i = 0; i < c0.Length; i++)
{
float c = c0[i];
float m = c1[i];
float y = c2[i];
float k = c3[i];
k *= scale;
c0[i] = c * k;
c1[i] = m * k;
c2[i] = y * k;
}
}
}
}
}

51
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromCmykVector.cs

@ -1,51 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromCmykVector : JpegColorConverterVector
{
public FromCmykVector(int precision)
: base(JpegColorSpace.Cmyk, precision)
{
}
protected override void ConvertCoreVectorizedInplace(in ComponentValues values)
{
ref Vector<float> cBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> mBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector<float> yBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector<float> kBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component3));
var scale = new Vector<float>(1 / (this.MaximumValue * this.MaximumValue));
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector<float> c = ref Unsafe.Add(ref cBase, i);
ref Vector<float> m = ref Unsafe.Add(ref mBase, i);
ref Vector<float> y = ref Unsafe.Add(ref yBase, i);
Vector<float> k = Unsafe.Add(ref kBase, i);
k *= scale;
c *= k;
m *= k;
y *= k;
}
}
protected override void ConvertCoreInplace(in ComponentValues values) =>
FromCmykScalar.ConvertCoreInplace(values, this.MaximumValue);
}
}
}

39
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScaleAvx.cs

@ -1,39 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromGrayscaleAvx : JpegColorConverterAvx
{
public FromGrayscaleAvx(int precision)
: base(JpegColorSpace.Grayscale, precision)
{
}
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector256<float> c0Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
// Used for the color conversion
var scale = Vector256.Create(1 / this.MaximumValue);
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector256<float> c0 = ref Unsafe.Add(ref c0Base, i);
c0 = Avx.Multiply(c0, scale);
}
}
}
}
}
#endif

34
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScaleScalar.cs

@ -1,34 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromGrayscaleScalar : JpegColorConverterScalar
{
public FromGrayscaleScalar(int precision)
: base(JpegColorSpace.Grayscale, precision)
{
}
public override void ConvertToRgbInplace(in ComponentValues values) =>
ConvertCoreInplace(values.Component0, this.MaximumValue);
internal static void ConvertCoreInplace(Span<float> values, float maxValue)
{
ref float valuesRef = ref MemoryMarshal.GetReference(values);
float scale = 1 / maxValue;
for (nint i = 0; i < values.Length; i++)
{
Unsafe.Add(ref valuesRef, i) *= scale;
}
}
}
}
}

38
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromGrayScaleVector.cs

@ -1,38 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromGrayScaleVector : JpegColorConverterVector
{
public FromGrayScaleVector(int precision)
: base(JpegColorSpace.Grayscale, precision)
{
}
protected override void ConvertCoreVectorizedInplace(in ComponentValues values)
{
ref Vector<float> cBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
var scale = new Vector<float>(1 / this.MaximumValue);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
ref Vector<float> c0 = ref Unsafe.Add(ref cBase, i);
c0 *= scale;
}
}
protected override void ConvertCoreInplace(in ComponentValues values) =>
FromGrayscaleScalar.ConvertCoreInplace(values.Component0, this.MaximumValue);
}
}
}

26
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromRgbScalar.cs

@ -1,26 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromRgbScalar : JpegColorConverterScalar
{
public FromRgbScalar(int precision)
: base(JpegColorSpace.RGB, precision)
{
}
public override void ConvertToRgbInplace(in ComponentValues values) =>
ConvertCoreInplace(values, this.MaximumValue);
internal static void ConvertCoreInplace(ComponentValues values, float maxValue)
{
FromGrayscaleScalar.ConvertCoreInplace(values.Component0, maxValue);
FromGrayscaleScalar.ConvertCoreInplace(values.Component1, maxValue);
FromGrayscaleScalar.ConvertCoreInplace(values.Component2, maxValue);
}
}
}
}

74
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrAvx.cs

@ -1,74 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using static SixLabors.ImageSharp.SimdUtils;
// ReSharper disable ImpureMethodCallOnReadonlyValueField
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromYCbCrAvx : JpegColorConverterAvx
{
public FromYCbCrAvx(int precision)
: base(JpegColorSpace.YCbCr, precision)
{
}
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector256<float> c0Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> c1Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector256<float> c2Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component2));
// Used for the color conversion
var chromaOffset = Vector256.Create(-this.HalfValue);
var scale = Vector256.Create(1 / this.MaximumValue);
var rCrMult = Vector256.Create(FromYCbCrScalar.RCrMult);
var gCbMult = Vector256.Create(-FromYCbCrScalar.GCbMult);
var gCrMult = Vector256.Create(-FromYCbCrScalar.GCrMult);
var bCbMult = Vector256.Create(FromYCbCrScalar.BCbMult);
// Walking 8 elements at one step:
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
ref Vector256<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector256<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector256<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector256<float> y = c0;
Vector256<float> cb = Avx.Add(c1, chromaOffset);
Vector256<float> cr = Avx.Add(c2, chromaOffset);
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector256<float> r = HwIntrinsics.MultiplyAdd(y, cr, rCrMult);
Vector256<float> g = HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(y, cb, gCbMult), cr, gCrMult);
Vector256<float> b = HwIntrinsics.MultiplyAdd(y, cb, bCbMult);
r = Avx.Multiply(Avx.RoundToNearestInteger(r), scale);
g = Avx.Multiply(Avx.RoundToNearestInteger(g), scale);
b = Avx.Multiply(Avx.RoundToNearestInteger(b), scale);
c0 = r;
c1 = g;
c2 = b;
}
}
}
}
}
#endif

50
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrScalar.cs

@ -1,50 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromYCbCrScalar : JpegColorConverterScalar
{
// TODO: comments, derived from ITU-T Rec. T.871
internal const float RCrMult = 1.402f;
internal const float GCbMult = (float)(0.114 * 1.772 / 0.587);
internal const float GCrMult = (float)(0.299 * 1.402 / 0.587);
internal const float BCbMult = 1.772f;
public FromYCbCrScalar(int precision)
: base(JpegColorSpace.YCbCr, precision)
{
}
public override void ConvertToRgbInplace(in ComponentValues values)
=> ConvertCoreInplace(values, this.MaximumValue, this.HalfValue);
internal static void ConvertCoreInplace(in ComponentValues values, float maxValue, float halfValue)
{
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
float scale = 1 / maxValue;
for (int i = 0; i < c0.Length; i++)
{
float y = c0[i];
float cb = c1[i] - halfValue;
float cr = c2[i] - halfValue;
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
c0[i] = MathF.Round(y + (RCrMult * cr), MidpointRounding.AwayFromZero) * scale;
c1[i] = MathF.Round(y - (GCbMult * cb) - (GCrMult * cr), MidpointRounding.AwayFromZero) * scale;
c2[i] = MathF.Round(y + (BCbMult * cb), MidpointRounding.AwayFromZero) * scale;
}
}
}
}
}

74
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYCbCrVector.cs

@ -1,74 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// ReSharper disable ImpureMethodCallOnReadonlyValueField
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromYCbCrVector : JpegColorConverterVector
{
public FromYCbCrVector(int precision)
: base(JpegColorSpace.YCbCr, precision)
{
}
protected override void ConvertCoreVectorizedInplace(in ComponentValues values)
{
ref Vector<float> c0Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> c1Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector<float> c2Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component2));
var chromaOffset = new Vector<float>(-this.HalfValue);
var scale = new Vector<float>(1 / this.MaximumValue);
var rCrMult = new Vector<float>(FromYCbCrScalar.RCrMult);
var gCbMult = new Vector<float>(-FromYCbCrScalar.GCbMult);
var gCrMult = new Vector<float>(-FromYCbCrScalar.GCrMult);
var bCbMult = new Vector<float>(FromYCbCrScalar.BCbMult);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
ref Vector<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector<float> y = Unsafe.Add(ref c0Base, i);
Vector<float> cb = Unsafe.Add(ref c1Base, i) + chromaOffset;
Vector<float> cr = Unsafe.Add(ref c2Base, i) + chromaOffset;
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector<float> r = y + (cr * rCrMult);
Vector<float> g = y + (cb * gCbMult) + (cr * gCrMult);
Vector<float> b = y + (cb * bCbMult);
r = r.FastRound();
g = g.FastRound();
b = b.FastRound();
r *= scale;
g *= scale;
b *= scale;
c0 = r;
c1 = g;
c2 = b;
}
}
protected override void ConvertCoreInplace(in ComponentValues values) =>
FromYCbCrScalar.ConvertCoreInplace(values, this.MaximumValue, this.HalfValue);
}
}
}

82
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccKAvx.cs

@ -1,82 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using static SixLabors.ImageSharp.SimdUtils;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromYccKAvx : JpegColorConverterAvx
{
public FromYccKAvx(int precision)
: base(JpegColorSpace.Ycck, precision)
{
}
public override void ConvertToRgbInplace(in ComponentValues values)
{
ref Vector256<float> c0Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector256<float> c1Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector256<float> c2Base =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector256<float> kBase =
ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(values.Component3));
// Used for the color conversion
var chromaOffset = Vector256.Create(-this.HalfValue);
var scale = Vector256.Create(1 / (this.MaximumValue * this.MaximumValue));
var max = Vector256.Create(this.MaximumValue);
var rCrMult = Vector256.Create(FromYCbCrScalar.RCrMult);
var gCbMult = Vector256.Create(-FromYCbCrScalar.GCbMult);
var gCrMult = Vector256.Create(-FromYCbCrScalar.GCrMult);
var bCbMult = Vector256.Create(FromYCbCrScalar.BCbMult);
// Walking 8 elements at one step:
nint n = values.Component0.Length / Vector256<float>.Count;
for (nint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
// k = kVals[i] / 256F;
ref Vector256<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector256<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector256<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector256<float> y = c0;
Vector256<float> cb = Avx.Add(c1, chromaOffset);
Vector256<float> cr = Avx.Add(c2, chromaOffset);
Vector256<float> scaledK = Avx.Multiply(Unsafe.Add(ref kBase, i), scale);
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector256<float> r = HwIntrinsics.MultiplyAdd(y, cr, rCrMult);
Vector256<float> g =
HwIntrinsics.MultiplyAdd(HwIntrinsics.MultiplyAdd(y, cb, gCbMult), cr, gCrMult);
Vector256<float> b = HwIntrinsics.MultiplyAdd(y, cb, bCbMult);
r = Avx.Subtract(max, Avx.RoundToNearestInteger(r));
g = Avx.Subtract(max, Avx.RoundToNearestInteger(g));
b = Avx.Subtract(max, Avx.RoundToNearestInteger(b));
r = Avx.Multiply(r, scaledK);
g = Avx.Multiply(g, scaledK);
b = Avx.Multiply(b, scaledK);
c0 = r;
c1 = g;
c2 = b;
}
}
}
}
}
#endif

43
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccKScalar.cs

@ -1,43 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromYccKScalar : JpegColorConverterScalar
{
public FromYccKScalar(int precision)
: base(JpegColorSpace.Ycck, precision)
{
}
public override void ConvertToRgbInplace(in ComponentValues values) =>
ConvertCoreInplace(values, this.MaximumValue, this.HalfValue);
internal static void ConvertCoreInplace(in ComponentValues values, float maxValue, float halfValue)
{
Span<float> c0 = values.Component0;
Span<float> c1 = values.Component1;
Span<float> c2 = values.Component2;
Span<float> c3 = values.Component3;
float scale = 1 / (maxValue * maxValue);
for (int i = 0; i < values.Component0.Length; i++)
{
float y = c0[i];
float cb = c1[i] - halfValue;
float cr = c2[i] - halfValue;
float scaledK = c3[i] * scale;
c0[i] = (maxValue - MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero)) * scaledK;
c1[i] = (maxValue - MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero)) * scaledK;
c2[i] = (maxValue - MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero)) * scaledK;
}
}
}
}
}

75
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.FromYccKVector.cs

@ -1,75 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
internal sealed class FromYccKVector : JpegColorConverterVector
{
public FromYccKVector(int precision)
: base(JpegColorSpace.Ycck, precision)
{
}
protected override void ConvertCoreVectorizedInplace(in ComponentValues values)
{
ref Vector<float> c0Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component0));
ref Vector<float> c1Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component1));
ref Vector<float> c2Base =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component2));
ref Vector<float> kBase =
ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(values.Component3));
var chromaOffset = new Vector<float>(-this.HalfValue);
var scale = new Vector<float>(1 / (this.MaximumValue * this.MaximumValue));
var max = new Vector<float>(this.MaximumValue);
var rCrMult = new Vector<float>(FromYCbCrScalar.RCrMult);
var gCbMult = new Vector<float>(-FromYCbCrScalar.GCbMult);
var gCrMult = new Vector<float>(-FromYCbCrScalar.GCrMult);
var bCbMult = new Vector<float>(FromYCbCrScalar.BCbMult);
nint n = values.Component0.Length / Vector<float>.Count;
for (nint i = 0; i < n; i++)
{
// y = yVals[i];
// cb = cbVals[i] - 128F;
// cr = crVals[i] - 128F;
// k = kVals[i] / 256F;
ref Vector<float> c0 = ref Unsafe.Add(ref c0Base, i);
ref Vector<float> c1 = ref Unsafe.Add(ref c1Base, i);
ref Vector<float> c2 = ref Unsafe.Add(ref c2Base, i);
Vector<float> y = c0;
Vector<float> cb = c1 + chromaOffset;
Vector<float> cr = c2 + chromaOffset;
Vector<float> scaledK = Unsafe.Add(ref kBase, i) * scale;
// r = y + (1.402F * cr);
// g = y - (0.344136F * cb) - (0.714136F * cr);
// b = y + (1.772F * cb);
Vector<float> r = y + (cr * rCrMult);
Vector<float> g = y + (cb * gCbMult) + (cr * gCrMult);
Vector<float> b = y + (cb * bCbMult);
r = (max - r.FastRound()) * scaledK;
g = (max - g.FastRound()) * scaledK;
b = (max - b.FastRound()) * scaledK;
c0 = r;
c1 = g;
c2 = b;
}
}
protected override void ConvertCoreInplace(in ComponentValues values) =>
FromYccKScalar.ConvertCoreInplace(values, this.MaximumValue, this.HalfValue);
}
}
}

61
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs

@ -1,61 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
{
internal abstract partial class JpegColorConverterBase
{
/// <summary>
/// <see cref="JpegColorConverterBase"/> abstract base for implementations
/// based on <see cref="Vector"/> API.
/// </summary>
/// <remarks>
/// Converters of this family can work with data of any size.
/// Even though real life data is guaranteed to be of size
/// divisible by 8 newer SIMD instructions like AVX512 won't work with
/// such data out of the box. These converters have fallback code
/// for remainder data.
/// </remarks>
internal abstract class JpegColorConverterVector : JpegColorConverterBase
{
protected JpegColorConverterVector(JpegColorSpace colorSpace, int precision)
: base(colorSpace, precision)
{
}
public sealed override bool IsAvailable => Vector.IsHardwareAccelerated && Vector<float>.Count % 4 == 0;
public sealed override int ElementsPerBatch => Vector<float>.Count;
public sealed override void ConvertToRgbInplace(in ComponentValues values)
{
DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware.");
int length = values.Component0.Length;
int remainder = (int)((uint)length % (uint)Vector<float>.Count);
// Jpeg images are guaranteed to have pixel strides at least 8 pixels wide
// Thus there's no need to check whether simdCount is greater than zero
int simdCount = length - remainder;
this.ConvertCoreVectorizedInplace(values.Slice(0, simdCount));
// Jpeg images width is always divisible by 8 without a remainder
// so it's safe to say SSE/AVX implementations would never have
// 'remainder' pixels
// But some exotic simd implementations e.g. AVX-512 can have
// remainder pixels
if (remainder > 0)
{
this.ConvertCoreInplace(values.Slice(simdCount, remainder));
}
}
protected virtual void ConvertCoreVectorizedInplace(in ComponentValues values) => throw new NotImplementedException();
protected virtual void ConvertCoreInplace(in ComponentValues values) => throw new NotImplementedException();
}
}
}

8
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{ {
public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height, byte componentCount) public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height, byte componentCount)
{ {
this.Extended = sofMarker.Marker is JpegConstants.Markers.SOF1 or JpegConstants.Markers.SOF9; this.IsExtended = sofMarker.Marker is JpegConstants.Markers.SOF1 or JpegConstants.Markers.SOF9;
this.Progressive = sofMarker.Marker is JpegConstants.Markers.SOF2 or JpegConstants.Markers.SOF10; this.Progressive = sofMarker.Marker is JpegConstants.Markers.SOF2 or JpegConstants.Markers.SOF10;
this.Precision = precision; this.Precision = precision;
@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary> /// <summary>
/// Gets a value indicating whether the frame uses the extended specification. /// Gets a value indicating whether the frame uses the extended specification.
/// </summary> /// </summary>
public bool Extended { get; private set; } public bool IsExtended { get; private set; }
/// <summary> /// <summary>
/// Gets a value indicating whether the frame uses the progressive specification. /// Gets a value indicating whether the frame uses the progressive specification.
@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <remarks> /// <remarks>
/// This is true for progressive and baseline non-interleaved images. /// This is true for progressive and baseline non-interleaved images.
/// </remarks> /// </remarks>
public bool MultiScan { get; set; } public bool Interleaved { get; set; }
/// <summary> /// <summary>
/// Gets the precision. /// Gets the precision.
@ -141,7 +141,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
public void AllocateComponents() public void AllocateComponents()
{ {
bool fullScan = this.Progressive || this.MultiScan; bool fullScan = this.Progressive || !this.Interleaved;
for (int i = 0; i < this.ComponentCount; i++) for (int i = 0; i < this.ComponentCount; i++)
{ {
IJpegComponent component = this.Components[i]; IJpegComponent component = this.Components[i];

4
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs

@ -1,12 +1,10 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{ {
/// <summary> /// <summary>
/// Converter used to convert jpeg spectral data to color pixels. /// Converter used to convert jpeg spectral data to pixels.
/// </summary> /// </summary>
internal abstract class SpectralConverter internal abstract class SpectralConverter
{ {

1
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs

@ -5,7 +5,6 @@ using System;
using System.Buffers; using System.Buffers;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;

116
src/ImageSharp/Formats/Jpeg/Components/Encoder/Component.cs

@ -0,0 +1,116 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// Represents a single frame component.
/// </summary>
internal class Component : IDisposable
{
private readonly MemoryAllocator memoryAllocator;
public Component(MemoryAllocator memoryAllocator, int horizontalFactor, int verticalFactor, int quantizationTableIndex)
{
this.memoryAllocator = memoryAllocator;
this.HorizontalSamplingFactor = horizontalFactor;
this.VerticalSamplingFactor = verticalFactor;
this.SamplingFactors = new Size(horizontalFactor, verticalFactor);
this.QuantizationTableIndex = quantizationTableIndex;
}
/// <summary>
/// Gets or sets DC coefficient predictor.
/// </summary>
public int DcPredictor { get; set; }
/// <summary>
/// Gets the horizontal sampling factor.
/// </summary>
public int HorizontalSamplingFactor { get; }
/// <summary>
/// Gets the vertical sampling factor.
/// </summary>
public int VerticalSamplingFactor { get; }
public Buffer2D<Block8x8> SpectralBlocks { get; private set; }
public Size SubSamplingDivisors { get; private set; }
public int QuantizationTableIndex { get; }
public Size SizeInBlocks { get; private set; }
public Size SamplingFactors { get; set; }
/// <summary>
/// Gets the number of blocks per line.
/// </summary>
public int WidthInBlocks { get; private set; }
/// <summary>
/// Gets the number of blocks per column.
/// </summary>
public int HeightInBlocks { get; private set; }
/// <summary>
/// Gets or sets the index for the DC Huffman table.
/// </summary>
public int DcTableId { get; set; }
/// <summary>
/// Gets or sets the index for the AC Huffman table.
/// </summary>
public int AcTableId { get; set; }
/// <inheritdoc/>
public void Dispose()
{
this.SpectralBlocks?.Dispose();
this.SpectralBlocks = null;
}
/// <summary>
/// Initializes component for future buffers initialization.
/// </summary>
/// <param name="frame">asdfasdf.</param>
/// <param name="maxSubFactorH">Maximal horizontal subsampling factor among all the components.</param>
/// <param name="maxSubFactorV">Maximal vertical subsampling factor among all the components.</param>
public void Init(JpegFrame frame, int maxSubFactorH, int maxSubFactorV)
{
uint widthInBlocks = ((uint)frame.PixelWidth + 7) / 8;
uint heightInBlocks = ((uint)frame.PixelHeight + 7) / 8;
this.WidthInBlocks = (int)MathF.Ceiling(
(float)widthInBlocks * this.HorizontalSamplingFactor / maxSubFactorH);
this.HeightInBlocks = (int)MathF.Ceiling(
(float)heightInBlocks * this.VerticalSamplingFactor / maxSubFactorV);
int blocksPerLineForMcu = frame.McusPerLine * this.HorizontalSamplingFactor;
int blocksPerColumnForMcu = frame.McusPerColumn * this.VerticalSamplingFactor;
this.SizeInBlocks = new Size(blocksPerLineForMcu, blocksPerColumnForMcu);
this.SubSamplingDivisors = new Size(maxSubFactorH, maxSubFactorV).DivideBy(this.SamplingFactors);
if (this.SubSamplingDivisors.Width == 0 || this.SubSamplingDivisors.Height == 0)
{
JpegThrowHelper.ThrowBadSampling();
}
}
public void AllocateSpectral(bool fullScan)
{
int spectralAllocWidth = this.SizeInBlocks.Width;
int spectralAllocHeight = fullScan ? this.SizeInBlocks.Height : this.VerticalSamplingFactor;
this.SpectralBlocks = this.memoryAllocator.Allocate2D<Block8x8>(spectralAllocWidth, spectralAllocHeight);
}
}
}

231
src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs

@ -0,0 +1,231 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
internal class ComponentProcessor : IDisposable
{
private readonly Size blockAreaSize;
private readonly Component component;
private Block8x8F quantTable;
public ComponentProcessor(MemoryAllocator memoryAllocator, Component component, Size postProcessorBufferSize, Block8x8F quantTable)
{
this.component = component;
this.quantTable = quantTable;
this.component = component;
this.blockAreaSize = component.SubSamplingDivisors * 8;
// alignment of 8 so each block stride can be sampled from a single 'ref pointer'
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>(
postProcessorBufferSize.Width,
postProcessorBufferSize.Height,
8,
AllocationOptions.Clean);
}
/// <summary>
/// Gets the temporary working buffer of color values.
/// </summary>
public Buffer2D<float> ColorBuffer { get; }
public void CopyColorBufferToBlocks(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.component.SpectralBlocks;
int destAreaStride = this.ColorBuffer.Width;
int yBlockStart = spectralStep * this.component.SamplingFactors.Height;
Block8x8F workspaceBlock = default;
// handle subsampling
Size subsamplingFactors = this.component.SubSamplingDivisors;
if (subsamplingFactors.Width != 1 || subsamplingFactors.Height != 1)
{
this.PackColorBuffer();
}
int blocksRowsPerStep = this.component.SamplingFactors.Height;
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++)
{
// load 8x8 block from 8 pixel strides
int xColorBufferStart = xBlock * 8;
workspaceBlock.ScaledCopyFrom(
ref colorBufferRow[xColorBufferStart],
destAreaStride);
// level shift via -128f
workspaceBlock.AddInPlace(-128f);
// FDCT
FloatingPointDCT.TransformFDCT(ref workspaceBlock);
// Quantize and save to spectral blocks
Block8x8F.Quantize(ref workspaceBlock, ref blockRow[xBlock], ref this.quantTable);
}
}
}
public Span<float> GetColorBufferRowSpan(int row)
=> this.ColorBuffer.DangerousGetRowSpan(row);
public void Dispose()
=> this.ColorBuffer.Dispose();
private void PackColorBuffer()
{
Size factors = this.component.SubSamplingDivisors;
int packedWidth = this.ColorBuffer.Width / factors.Width;
float averageMultiplier = 1f / (factors.Width * factors.Height);
for (int i = 0; i < this.ColorBuffer.Height; i += factors.Height)
{
Span<float> sourceRow = this.ColorBuffer.DangerousGetRowSpan(i);
// vertical sum
for (int j = 1; j < factors.Height; j++)
{
SumVertical(sourceRow, this.ColorBuffer.DangerousGetRowSpan(i + j));
}
// horizontal sum
SumHorizontal(sourceRow, factors.Width);
// calculate average
MultiplyToAverage(sourceRow, averageMultiplier);
// copy to the first 8 slots
sourceRow.Slice(0, packedWidth).CopyTo(this.ColorBuffer.DangerousGetRowSpan(i / factors.Height));
}
static void SumVertical(Span<float> target, Span<float> source)
{
if (Avx.IsSupported)
{
ref Vector256<float> targetVectorRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(target));
ref Vector256<float> sourceVectorRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(source));
// Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed
nint count = source.Length / Vector256<float>.Count;
for (nint i = 0; i < count; i++)
{
Unsafe.Add(ref targetVectorRef, i) = Avx.Add(Unsafe.Add(ref targetVectorRef, i), Unsafe.Add(ref sourceVectorRef, i));
}
}
else
{
ref Vector<float> targetVectorRef = ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(target));
ref Vector<float> sourceVectorRef = ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(source));
nint count = source.Length / Vector<float>.Count;
for (nint i = 0; i < count; i++)
{
Unsafe.Add(ref targetVectorRef, i) += Unsafe.Add(ref sourceVectorRef, i);
}
ref float targetRef = ref MemoryMarshal.GetReference(target);
ref float sourceRef = ref MemoryMarshal.GetReference(source);
for (nint i = count * Vector<float>.Count; i < source.Length; i++)
{
Unsafe.Add(ref targetRef, i) += Unsafe.Add(ref sourceRef, i);
}
}
}
static void SumHorizontal(Span<float> target, int factor)
{
Span<float> source = target;
if (Avx2.IsSupported)
{
ref Vector256<float> targetRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(target));
// Ideally we need to use log2: Numerics.Log2((uint)factor)
// but division by 2 works just fine in this case
int haddIterationsCount = (int)((uint)factor / 2);
// Transform spans so that it only contains 'remainder'
// values for the scalar fallback code
int scalarRemainder = target.Length % (Vector<float>.Count * factor);
int touchedCount = target.Length - scalarRemainder;
source = source.Slice(touchedCount);
target = target.Slice(touchedCount / factor);
uint length = (uint)touchedCount / (uint)Vector256<float>.Count;
for (int i = 0; i < haddIterationsCount; i++)
{
length /= 2;
for (nuint j = 0; j < length; j++)
{
nuint indexLeft = j * 2;
nuint indexRight = indexLeft + 1;
Vector256<float> sum = Avx.HorizontalAdd(Unsafe.Add(ref targetRef, indexLeft), Unsafe.Add(ref targetRef, indexRight));
Unsafe.Add(ref targetRef, j) = Avx2.Permute4x64(sum.AsDouble(), 0b11_01_10_00).AsSingle();
}
}
}
// scalar remainder
for (int i = 0; i < source.Length / factor; i++)
{
target[i] = source[i * factor];
for (int j = 1; j < factor; j++)
{
target[i] += source[(i * factor) + j];
}
}
}
static void MultiplyToAverage(Span<float> target, float multiplier)
{
if (Avx.IsSupported)
{
ref Vector256<float> targetVectorRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(target));
// Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed
nint count = target.Length / Vector256<float>.Count;
var multiplierVector = Vector256.Create(multiplier);
for (nint i = 0; i < count; i++)
{
Unsafe.Add(ref targetVectorRef, i) = Avx.Multiply(Unsafe.Add(ref targetVectorRef, i), multiplierVector);
}
}
else
{
ref Vector<float> targetVectorRef = ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(target));
nint count = target.Length / Vector<float>.Count;
var multiplierVector = new Vector<float>(multiplier);
for (nint i = 0; i < count; i++)
{
Unsafe.Add(ref targetVectorRef, i) *= multiplierVector;
}
ref float targetRef = ref MemoryMarshal.GetReference(target);
for (nint i = count * Vector<float>.Count; i < target.Length; i++)
{
Unsafe.Add(ref targetRef, i) *= multiplier;
}
}
}
}
}
}

30
src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegComponentConfig.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
internal class JpegComponentConfig
{
public JpegComponentConfig(byte id, int hsf, int vsf, int quantIndex, int dcIndex, int acIndex)
{
this.Id = id;
this.HorizontalSampleFactor = hsf;
this.VerticalSampleFactor = vsf;
this.QuantizatioTableIndex = quantIndex;
this.DcTableSelector = dcIndex;
this.AcTableSelector = acIndex;
}
public byte Id { get; }
public int HorizontalSampleFactor { get; }
public int VerticalSampleFactor { get; }
public int QuantizatioTableIndex { get; }
public int DcTableSelector { get; }
public int AcTableSelector { get; }
}
}

44
src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegFrameConfig.cs

@ -0,0 +1,44 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
internal class JpegFrameConfig
{
public JpegFrameConfig(JpegColorSpace colorType, JpegEncodingColor encodingColor, JpegComponentConfig[] components, JpegHuffmanTableConfig[] huffmanTables, JpegQuantizationTableConfig[] quantTables)
{
this.ColorType = colorType;
this.EncodingColor = encodingColor;
this.Components = components;
this.HuffmanTables = huffmanTables;
this.QuantizationTables = quantTables;
this.MaxHorizontalSamplingFactor = components[0].HorizontalSampleFactor;
this.MaxVerticalSamplingFactor = components[0].VerticalSampleFactor;
for (int i = 1; i < components.Length; i++)
{
JpegComponentConfig component = components[i];
this.MaxHorizontalSamplingFactor = Math.Max(this.MaxHorizontalSamplingFactor, component.HorizontalSampleFactor);
this.MaxVerticalSamplingFactor = Math.Max(this.MaxVerticalSamplingFactor, component.VerticalSampleFactor);
}
}
public JpegColorSpace ColorType { get; }
public JpegEncodingColor EncodingColor { get; }
public JpegComponentConfig[] Components { get; }
public JpegHuffmanTableConfig[] HuffmanTables { get; }
public JpegQuantizationTableConfig[] QuantizationTables { get; }
public int MaxHorizontalSamplingFactor { get; }
public int MaxVerticalSamplingFactor { get; }
public byte? AdobeColorTransformMarkerFlag { get; set; } = null;
}
}

21
src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegHuffmanTableConfig.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
internal class JpegHuffmanTableConfig
{
public JpegHuffmanTableConfig(int @class, int destIndex, HuffmanSpec table)
{
this.Class = @class;
this.DestinationIndex = destIndex;
this.Table = table;
}
public int Class { get; }
public int DestinationIndex { get; }
public HuffmanSpec Table { get; }
}
}

20
src/ImageSharp/Formats/Jpeg/Components/Encoder/EncodingConfigs/JpegQuantizationTableConfig.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
internal class JpegQuantizationTableConfig
{
public JpegQuantizationTableConfig(int destIndex, ReadOnlySpan<byte> quantizationTable)
{
this.DestinationIndex = destIndex;
this.Table = Block8x8.Load(quantizationTable);
}
public int DestinationIndex { get; }
public Block8x8 Table { get; }
}
}

35
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffIndex.cs

@ -1,35 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// Enumerates the Huffman tables
/// </summary>
internal enum HuffIndex
{
/// <summary>
/// The DC luminance huffman table index
/// </summary>
LuminanceDC = 0,
// ReSharper disable UnusedMember.Local
/// <summary>
/// The AC luminance huffman table index
/// </summary>
LuminanceAC = 1,
/// <summary>
/// The DC chrominance huffman table index
/// </summary>
ChrominanceDC = 2,
/// <summary>
/// The AC chrominance huffman table index
/// </summary>
ChrominanceAC = 3,
// ReSharper restore UnusedMember.Local
}
}

17
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs

@ -26,23 +26,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </remarks> /// </remarks>
internal readonly struct HuffmanLut internal readonly struct HuffmanLut
{ {
/// <summary>
/// The compiled representations of theHuffmanSpec.
/// </summary>
public static readonly HuffmanLut[] TheHuffmanLut = new HuffmanLut[4];
/// <summary>
/// Initializes static members of the <see cref="HuffmanLut"/> struct.
/// </summary>
static HuffmanLut()
{
// Initialize the Huffman tables
for (int i = 0; i < HuffmanSpec.TheHuffmanSpecs.Length; i++)
{
TheHuffmanLut[i] = new HuffmanLut(HuffmanSpec.TheHuffmanSpecs[i]);
}
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HuffmanLut"/> struct. /// Initializes a new instance of the <see cref="HuffmanLut"/> struct.
/// </summary> /// </summary>

366
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs

@ -7,7 +7,6 @@ using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
@ -60,10 +59,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
private const int OutputBufferLengthMultiplier = 2; private const int OutputBufferLengthMultiplier = 2;
/// <summary> /// <summary>
/// Compiled huffman tree to encode given values. /// The DC Huffman tables.
/// </summary> /// </summary>
/// <remarks>Yields codewords by index consisting of [run length | bitsize].</remarks> private readonly HuffmanLut[] dcHuffmanTables = new HuffmanLut[4];
private HuffmanLut[] huffmanTables;
/// <summary>
/// The AC Huffman tables.
/// </summary>
private readonly HuffmanLut[] acHuffmanTables = new HuffmanLut[4];
/// <summary> /// <summary>
/// Emitted bits 'micro buffer' before being transferred to the <see cref="emitBuffer"/>. /// Emitted bits 'micro buffer' before being transferred to the <see cref="emitBuffer"/>.
@ -94,8 +97,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
private int emitWriteIndex; private int emitWriteIndex;
private Block8x8 tempBlock;
/// <summary> /// <summary>
/// The output stream. All attempted writes after the first error become no-ops. /// The output stream. All attempted writes after the first error become no-ops.
/// </summary> /// </summary>
@ -128,57 +129,67 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
get => this.emitWriteIndex < (uint)this.emitBuffer.Length / 2; get => this.emitWriteIndex < (uint)this.emitBuffer.Length / 2;
} }
public void BuildHuffmanTable(JpegHuffmanTableConfig tableConfig)
{
HuffmanLut[] tables = tableConfig.Class == 0 ? this.dcHuffmanTables : this.acHuffmanTables;
tables[tableConfig.DestinationIndex] = new HuffmanLut(tableConfig.Table);
}
/// <summary> /// <summary>
/// Encodes the image with no subsampling. /// Encodes scan in baseline interleaved mode.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="color">Output color space.</param>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param> /// <param name="frame">Frame to encode.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param> /// <param name="converter">Converter from color to spectral.</param>
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee.</param> /// <param name="cancellationToken">The token to request cancellation.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param> public void EncodeScanBaselineInterleaved<TPixel>(JpegEncodingColor color, JpegFrame frame, SpectralConverter<TPixel> converter, CancellationToken cancellationToken)
public void Encode444<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); switch (color)
FloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); {
case JpegEncodingColor.YCbCrRatio444:
this.huffmanTables = HuffmanLut.TheHuffmanLut; case JpegEncodingColor.Rgb:
this.EncodeThreeComponentBaselineInterleavedScanNoSubsampling(frame, converter, cancellationToken);
// ReSharper disable once InconsistentNaming break;
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; default:
this.EncodeScanBaselineInterleaved(frame, converter, cancellationToken);
break;
}
}
ImageFrame<TPixel> frame = pixels.Frames.RootFrame; /// <summary>
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer; /// Encodes grayscale scan in baseline interleaved mode.
RowOctet<TPixel> currentRows = default; /// </summary>
/// <param name="component">Component with grayscale data.</param>
/// <param name="converter">Converter from color to spectral.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeScanBaselineSingleComponent<TPixel>(Component component, SpectralConverter<TPixel> converter, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int h = component.HeightInBlocks;
int w = component.WidthInBlocks;
var pixelConverter = new YCbCrForwardConverter444<TPixel>(frame); ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
for (int y = 0; y < pixels.Height; y += 8) for (int i = 0; i < h; i++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
currentRows.Update(pixelBuffer, y);
for (int x = 0; x < pixels.Width; x += 8) // Convert from pixels to spectral via given converter
{ converter.ConvertStrideBaseline();
pixelConverter.Convert(x, y, ref currentRows);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.Y,
ref luminanceQuantTable);
prevDCCb = this.WriteBlock( // Encode spectral to binary
QuantIndex.Chrominance, Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: 0);
prevDCCb, ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
ref pixelConverter.Cb,
ref chrominanceQuantTable);
prevDCCr = this.WriteBlock( for (nint k = 0; k < w; k++)
QuantIndex.Chrominance, {
prevDCCr, this.WriteBlock(
ref pixelConverter.Cr, component,
ref chrominanceQuantTable); ref Unsafe.Add(ref blockRef, k),
ref dcHuffmanTable,
ref acHuffmanTable);
if (this.IsStreamFlushNeeded) if (this.IsStreamFlushNeeded)
{ {
@ -191,65 +202,33 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
} }
/// <summary> /// <summary>
/// Encodes the image with subsampling. The Cb and Cr components are each subsampled /// Encodes scan with a single component in baseline non-interleaved mode.
/// at a factor of 2 both horizontally and vertically.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="component">Component with grayscale data.</param>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param> /// <param name="cancellationToken">The token to request cancellation.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param> public void EncodeScanBaseline(Component component, CancellationToken cancellationToken)
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void Encode420<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{ {
FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); int h = component.HeightInBlocks;
FloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); int w = component.WidthInBlocks;
this.huffmanTables = HuffmanLut.TheHuffmanLut;
// ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
ImageFrame<TPixel> frame = pixels.Frames.RootFrame;
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
RowOctet<TPixel> currentRows = default;
var pixelConverter = new YCbCrForwardConverter420<TPixel>(frame); ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
for (int y = 0; y < pixels.Height; y += 16) for (int i = 0; i < h; i++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
for (int x = 0; x < pixels.Width; x += 16)
{
for (int i = 0; i < 2; i++)
{
int yOff = i * 8;
currentRows.Update(pixelBuffer, y + yOff);
pixelConverter.Convert(x, y, ref currentRows, i);
prevDCY = this.WriteBlock( // Encode spectral to binary
QuantIndex.Luminance, Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
prevDCY, ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
ref pixelConverter.YLeft,
ref luminanceQuantTable);
prevDCY = this.WriteBlock( for (nint k = 0; k < w; k++)
QuantIndex.Luminance, {
prevDCY, this.WriteBlock(
ref pixelConverter.YRight, component,
ref luminanceQuantTable); ref Unsafe.Add(ref blockRef, k),
} ref dcHuffmanTable,
ref acHuffmanTable);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
ref pixelConverter.Cb,
ref chrominanceQuantTable);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
ref pixelConverter.Cr,
ref chrominanceQuantTable);
if (this.IsStreamFlushNeeded) if (this.IsStreamFlushNeeded)
{ {
@ -262,43 +241,64 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
} }
/// <summary> /// <summary>
/// Encodes the image with no chroma, just luminance. /// Encodes scan in baseline interleaved mode for any amount of component with arbitrary sampling factors.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="frame">Frame to encode.</param>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param> /// <param name="converter">Converter from color to spectral.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param> /// <param name="cancellationToken">The token to request cancellation.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param> private void EncodeScanBaselineInterleaved<TPixel>(JpegFrame frame, SpectralConverter<TPixel> converter, CancellationToken cancellationToken)
public void EncodeGrayscale<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); nint mcu = 0;
nint mcusPerColumn = frame.McusPerColumn;
nint mcusPerLine = frame.McusPerLine;
for (int j = 0; j < mcusPerColumn; j++)
{
cancellationToken.ThrowIfCancellationRequested();
this.huffmanTables = HuffmanLut.TheHuffmanLut; // Convert from pixels to spectral via given converter
converter.ConvertStrideBaseline();
// ReSharper disable once InconsistentNaming // Encode spectral to binary
int prevDCY = 0; for (nint i = 0; i < mcusPerLine; i++)
{
// Scan an interleaved mcu... process components in order
nint mcuCol = mcu % mcusPerLine;
for (nint k = 0; k < frame.Components.Length; k++)
{
Component component = frame.Components[k];
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
ImageFrame<TPixel> frame = pixels.Frames.RootFrame; nint h = component.HorizontalSamplingFactor;
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer; int v = component.VerticalSamplingFactor;
RowOctet<TPixel> currentRows = default;
var pixelConverter = new LuminanceForwardConverter<TPixel>(frame); nint blockColBase = mcuCol * h;
for (int y = 0; y < pixels.Height; y += 8) // Scan out an mcu's worth of this component; that's just determined
// by the basic H and V specified for the component
for (int y = 0; y < v; y++)
{ {
cancellationToken.ThrowIfCancellationRequested(); Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y);
currentRows.Update(pixelBuffer, y); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (int x = 0; x < pixels.Width; x += 8) for (nint x = 0; x < h; x++)
{ {
pixelConverter.Convert(x, y, ref currentRows); nint blockCol = blockColBase + x;
prevDCY = this.WriteBlock( this.WriteBlock(
QuantIndex.Luminance, component,
prevDCY, ref Unsafe.Add(ref blockRef, blockCol),
ref pixelConverter.Y, ref dcHuffmanTable,
ref luminanceQuantTable); ref acHuffmanTable);
}
}
}
// After all interleaved components, that's an interleaved MCU
mcu++;
if (this.IsStreamFlushNeeded) if (this.IsStreamFlushNeeded)
{ {
this.FlushToStream(); this.FlushToStream();
@ -310,54 +310,59 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
} }
/// <summary> /// <summary>
/// Encodes the image with no subsampling and keeps the pixel data as Rgb24. /// Encodes scan in baseline interleaved mode with exactly 3 components with no subsampling.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="frame">Frame to encode.</param>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param> /// <param name="converter">Converter from color to spectral.</param>
/// <param name="quantTable">Quantization table provided by the callee.</param> /// <param name="cancellationToken">The token to request cancellation.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param> private void EncodeThreeComponentBaselineInterleavedScanNoSubsampling<TPixel>(JpegFrame frame, SpectralConverter<TPixel> converter, CancellationToken cancellationToken)
public void EncodeRgb<TPixel>(Image<TPixel> pixels, ref Block8x8F quantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
FloatingPointDCT.AdjustToFDCT(ref quantTable); nint mcusPerColumn = frame.McusPerColumn;
nint mcusPerLine = frame.McusPerLine;
this.huffmanTables = HuffmanLut.TheHuffmanLut;
// ReSharper disable once InconsistentNaming Component c2 = frame.Components[2];
int prevDCR = 0, prevDCG = 0, prevDCB = 0; Component c1 = frame.Components[1];
Component c0 = frame.Components[0];
ImageFrame<TPixel> frame = pixels.Frames.RootFrame; ref HuffmanLut c0dcHuffmanTable = ref this.dcHuffmanTables[c0.DcTableId];
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer; ref HuffmanLut c0acHuffmanTable = ref this.acHuffmanTables[c0.AcTableId];
RowOctet<TPixel> currentRows = default; ref HuffmanLut c1dcHuffmanTable = ref this.dcHuffmanTables[c1.DcTableId];
ref HuffmanLut c1acHuffmanTable = ref this.acHuffmanTables[c1.AcTableId];
ref HuffmanLut c2dcHuffmanTable = ref this.dcHuffmanTables[c2.DcTableId];
ref HuffmanLut c2acHuffmanTable = ref this.acHuffmanTables[c2.AcTableId];
var pixelConverter = new RgbForwardConverter<TPixel>(frame); ref Block8x8 c0BlockRef = ref MemoryMarshal.GetReference(c0.SpectralBlocks.DangerousGetRowSpan(y: 0));
ref Block8x8 c1BlockRef = ref MemoryMarshal.GetReference(c1.SpectralBlocks.DangerousGetRowSpan(y: 0));
ref Block8x8 c2BlockRef = ref MemoryMarshal.GetReference(c2.SpectralBlocks.DangerousGetRowSpan(y: 0));
for (int y = 0; y < pixels.Height; y += 8) for (nint j = 0; j < mcusPerColumn; j++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
currentRows.Update(pixelBuffer, y);
for (int x = 0; x < pixels.Width; x += 8) // Convert from pixels to spectral via given converter
{ converter.ConvertStrideBaseline();
pixelConverter.Convert(x, y, ref currentRows);
prevDCR = this.WriteBlock( // Encode spectral to binary
QuantIndex.Luminance, for (nint i = 0; i < mcusPerLine; i++)
prevDCR, {
ref pixelConverter.R, this.WriteBlock(
ref quantTable); c0,
ref Unsafe.Add(ref c0BlockRef, i),
ref c0dcHuffmanTable,
ref c0acHuffmanTable);
prevDCG = this.WriteBlock( this.WriteBlock(
QuantIndex.Luminance, c1,
prevDCG, ref Unsafe.Add(ref c1BlockRef, i),
ref pixelConverter.G, ref c1dcHuffmanTable,
ref quantTable); ref c1acHuffmanTable);
prevDCB = this.WriteBlock( this.WriteBlock(
QuantIndex.Luminance, c2,
prevDCB, ref Unsafe.Add(ref c2BlockRef, i),
ref pixelConverter.B, ref c2dcHuffmanTable,
ref quantTable); ref c2acHuffmanTable);
if (this.IsStreamFlushNeeded) if (this.IsStreamFlushNeeded)
{ {
@ -369,44 +374,24 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
this.FlushRemainingBytes(); this.FlushRemainingBytes();
} }
/// <summary> private void WriteBlock(
/// Writes a block of pixel data using the given quantization table, Component component,
/// returning the post-quantized DC value of the DCT-transformed block. ref Block8x8 block,
/// The block is in natural (not zig-zag) order. ref HuffmanLut dcTable,
/// </summary> ref HuffmanLut acTable)
/// <param name="index">The quantization table index.</param>
/// <param name="prevDC">The previous DC value.</param>
/// <param name="block">Source block.</param>
/// <param name="quant">Quantization table.</param>
/// <returns>The <see cref="int"/>.</returns>
private int WriteBlock(
QuantIndex index,
int prevDC,
ref Block8x8F block,
ref Block8x8F quant)
{ {
ref Block8x8 spectralBlock = ref this.tempBlock;
// Shifting level from 0..255 to -128..127
block.AddInPlace(-128f);
// Discrete cosine transform
FloatingPointDCT.TransformFDCT(ref block);
// Quantization
Block8x8F.Quantize(ref block, ref spectralBlock, ref quant);
// Emit the DC delta. // Emit the DC delta.
int dc = spectralBlock[0]; int dc = block[0];
this.EmitHuffRLE(this.huffmanTables[2 * (int)index].Values, 0, dc - prevDC); this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor);
component.DcPredictor = dc;
// Emit the AC components. // Emit the AC components.
int[] acHuffTable = this.huffmanTables[(2 * (int)index) + 1].Values; int[] acHuffTable = acTable.Values;
nint lastValuableIndex = spectralBlock.GetLastNonZeroIndex(); nint lastValuableIndex = block.GetLastNonZeroIndex();
int runLength = 0; int runLength = 0;
ref short blockRef = ref Unsafe.As<Block8x8, short>(ref spectralBlock); ref short blockRef = ref Unsafe.As<Block8x8, short>(ref block);
for (nint zig = 1; zig <= lastValuableIndex; zig++) for (nint zig = 1; zig <= lastValuableIndex; zig++)
{ {
const int zeroRun1 = 1 << 4; const int zeroRun1 = 1 << 4;
@ -437,8 +422,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{ {
this.EmitHuff(acHuffTable, 0x00); this.EmitHuff(acHuffTable, 0x00);
} }
return dc;
} }
/// <summary> /// <summary>
@ -655,7 +638,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// <summary> /// <summary>
/// Flushes spectral data bytes after encoding all channel blocks /// Flushes spectral data bytes after encoding all channel blocks
/// in a single jpeg macroblock using <see cref="WriteBlock"/>. /// in a single jpeg macroblock using <see cref="WriteBlock(Component, ref Block8x8, ref HuffmanLut, ref HuffmanLut)"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This must be called only if <see cref="IsStreamFlushNeeded"/> is true /// This must be called only if <see cref="IsStreamFlushNeeded"/> is true
@ -684,6 +667,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
// Flush cached bytes to the output stream with padding bits // Flush cached bytes to the output stream with padding bits
int lastByteIndex = (this.emitWriteIndex * 4) - valuableBytesCount; int lastByteIndex = (this.emitWriteIndex * 4) - valuableBytesCount;
this.FlushToStream(lastByteIndex); this.FlushToStream(lastByteIndex);
// Clear huffman register
// This is needed for for images with multiples scans
this.bitCount = 0;
this.accumulatedBits = 0;
} }
} }
} }

53
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs

@ -6,18 +6,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// <summary> /// <summary>
/// The Huffman encoding specifications. /// The Huffman encoding specifications.
/// </summary> /// </summary>
internal readonly struct HuffmanSpec public readonly struct HuffmanSpec
{ {
#pragma warning disable SA1118 // ParameterMustNotSpanMultipleLines
/// <summary> /// <summary>
/// The Huffman encoding specifications. /// Huffman talbe specification for luminance DC.
/// This encoder uses the same Huffman encoding for all images.
/// </summary> /// </summary>
public static readonly HuffmanSpec[] TheHuffmanSpecs = /// <remarks>
{ /// This is an example specification taken from the jpeg specification paper.
// Luminance DC. /// </remarks>
new HuffmanSpec( public static readonly HuffmanSpec LuminanceDC = new(
new byte[] new byte[]
{ {
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
@ -26,10 +23,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
new byte[] new byte[]
{ {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
}), });
// Luminance AC. /// <summary>
new HuffmanSpec( /// Huffman talbe specification for luminance AC.
/// </summary>
/// <remarks>
/// This is an example specification taken from the jpeg specification paper.
/// </remarks>
public static readonly HuffmanSpec LuminanceAC = new(
new byte[] new byte[]
{ {
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0,
@ -61,10 +63,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,
0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa 0xf9, 0xfa
}), });
// Chrominance DC. /// <summary>
new HuffmanSpec( /// Huffman talbe specification for chrominance DC.
/// </summary>
/// <remarks>
/// This is an example specification taken from the jpeg specification paper.
/// </remarks>
public static readonly HuffmanSpec ChrominanceDC = new(
new byte[] new byte[]
{ {
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
@ -73,10 +80,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
new byte[] new byte[]
{ {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
}), });
// Chrominance AC. /// <summary>
new HuffmanSpec( /// Huffman talbe specification for chrominance DC.
/// </summary>
/// <remarks>
/// This is an example specification taken from the jpeg specification paper.
/// </remarks>
public static readonly HuffmanSpec ChrominanceAC = new(
new byte[] new byte[]
{ {
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0,
@ -108,9 +120,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa 0xf9, 0xfa
}) });
};
#pragma warning restore SA1118 // ParameterMustNotSpanMultipleLines
/// <summary> /// <summary>
/// Gets count[i] - The number of codes of length i bits. /// Gets count[i] - The number of codes of length i bits.
/// </summary> /// </summary>

85
src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs

@ -0,0 +1,85 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// Represent a single jpeg frame.
/// </summary>
internal sealed class JpegFrame : IDisposable
{
public JpegFrame(Image image, JpegFrameConfig frameConfig, bool interleaved)
{
this.ColorSpace = frameConfig.ColorType;
this.Interleaved = interleaved;
this.PixelWidth = image.Width;
this.PixelHeight = image.Height;
MemoryAllocator allocator = image.GetConfiguration().MemoryAllocator;
JpegComponentConfig[] componentConfigs = frameConfig.Components;
this.Components = new Component[componentConfigs.Length];
for (int i = 0; i < this.Components.Length; i++)
{
JpegComponentConfig componentConfig = componentConfigs[i];
this.Components[i] = new Component(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.Components.Length; i++)
{
Component component = this.Components[i];
component.Init(this, maxSubFactorH, maxSubFactorV);
}
}
public JpegColorSpace ColorSpace { get; }
public bool Interleaved { get; }
public int PixelHeight { get; }
public int PixelWidth { get; }
public Component[] Components { get; }
public int McusPerLine { get; }
public int McusPerColumn { get; }
public int BlocksPerMcu { get; }
public void Dispose()
{
for (int i = 0; i < this.Components.Length; i++)
{
this.Components[i].Dispose();
}
}
public void AllocateComponents(bool fullScan)
{
for (int i = 0; i < this.Components.Length; i++)
{
Component component = this.Components[i];
component.AllocateSpectral(fullScan);
}
}
}
}

127
src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs

@ -1,127 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// On-stack worker struct to efficiently encapsulate the TPixel -> L8 -> Y conversion chain of 8x8 pixel blocks.
/// </summary>
/// <typeparam name="TPixel">The pixel type to work on</typeparam>
internal ref struct LuminanceForwardConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Number of pixels processed per single <see cref="Convert(int, int, ref RowOctet{TPixel})"/> call
/// </summary>
private const int PixelsPerSample = 8 * 8;
/// <summary>
/// The Y component
/// </summary>
public Block8x8F Y;
/// <summary>
/// Temporal 64-pixel span to hold unconverted TPixel data.
/// </summary>
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal 64-byte span to hold converted <see cref="L8"/> data.
/// </summary>
private readonly Span<L8> l8Span;
/// <summary>
/// Sampled pixel buffer size.
/// </summary>
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations.
/// </summary>
private readonly Configuration config;
public LuminanceForwardConverter(ImageFrame<TPixel> frame)
{
this.Y = default;
this.pixelSpan = new TPixel[PixelsPerSample].AsSpan();
this.l8Span = new L8[PixelsPerSample].AsSpan();
this.samplingAreaSize = new Size(frame.Width, frame.Height);
this.config = frame.GetConfiguration();
}
/// <summary>
/// Gets size of sampling area from given frame pixel buffer.
/// </summary>
private static Size SampleSize => new(8, 8);
/// <summary>
/// Converts a 8x8 image area inside 'pixels' at position (x,y) placing the result members of the structure (<see cref="Y"/>)
/// </summary>
public void Convert(int x, int y, ref RowOctet<TPixel> currentRows)
{
YCbCrForwardConverter<TPixel>.LoadAndStretchEdges(currentRows, this.pixelSpan, new Point(x, y), SampleSize, this.samplingAreaSize);
PixelOperations<TPixel>.Instance.ToL8(this.config, this.pixelSpan, this.l8Span);
ref Block8x8F yBlock = ref this.Y;
ref L8 l8Start = ref MemoryMarshal.GetReference(this.l8Span);
if (RgbToYCbCrConverterVectorized.IsSupported)
{
ConvertAvx(ref l8Start, ref yBlock);
}
else
{
ConvertScalar(ref l8Start, ref yBlock);
}
}
/// <summary>
/// Converts 8x8 L8 pixel matrix to 8x8 Block of floats using Avx2 Intrinsics.
/// </summary>
/// <param name="l8Start">Start of span of L8 pixels with size of 64</param>
/// <param name="yBlock">8x8 destination matrix of Luminance(Y) converted data</param>
private static void ConvertAvx(ref L8 l8Start, ref Block8x8F yBlock)
{
Debug.Assert(RgbToYCbCrConverterVectorized.IsSupported, "AVX2 is required to run this converter");
#if SUPPORTS_RUNTIME_INTRINSICS
ref Vector128<byte> l8ByteSpan = ref Unsafe.As<L8, Vector128<byte>>(ref l8Start);
ref Vector256<float> destRef = ref yBlock.V0;
const int bytesPerL8Stride = 8;
for (nint i = 0; i < 8; i++)
{
Unsafe.Add(ref destRef, i) = Avx2.ConvertToVector256Single(Avx2.ConvertToVector256Int32(Unsafe.AddByteOffset(ref l8ByteSpan, bytesPerL8Stride * i)));
}
#endif
}
/// <summary>
/// Converts 8x8 L8 pixel matrix to 8x8 Block of floats.
/// </summary>
/// <param name="l8Start">Start of span of L8 pixels with size of 64</param>
/// <param name="yBlock">8x8 destination matrix of Luminance(Y) converted data</param>
private static void ConvertScalar(ref L8 l8Start, ref Block8x8F yBlock)
{
for (int i = 0; i < Block8x8F.Size; i++)
{
ref L8 c = ref Unsafe.Add(ref l8Start, i);
yBlock[i] = c.PackedValue;
}
}
}
}

21
src/ImageSharp/Formats/Jpeg/Components/Encoder/QuantIndex.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// Enumerates the quantization tables.
/// </summary>
internal enum QuantIndex
{
/// <summary>
/// The luminance quantization table index.
/// </summary>
Luminance = 0,
/// <summary>
/// The chrominance quantization table index.
/// </summary>
Chrominance = 1,
}
}

165
src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs

@ -1,165 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// On-stack worker struct to convert TPixel -> Rgb24 of 8x8 pixel blocks.
/// </summary>
/// <typeparam name="TPixel">The pixel type to work on.</typeparam>
internal ref struct RgbForwardConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Number of pixels processed per single <see cref="Convert(int, int, ref RowOctet{TPixel})"/> call
/// </summary>
private const int PixelsPerSample = 8 * 8;
/// <summary>
/// Total byte size of processed pixels converted from TPixel to <see cref="Rgb24"/>
/// </summary>
private const int RgbSpanByteSize = PixelsPerSample * 3;
/// <summary>
/// The Red component.
/// </summary>
public Block8x8F R;
/// <summary>
/// The Green component.
/// </summary>
public Block8x8F G;
/// <summary>
/// The Blue component.
/// </summary>
public Block8x8F B;
/// <summary>
/// Temporal 64-byte span to hold unconverted TPixel data.
/// </summary>
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal 64-byte span to hold converted Rgb24 data.
/// </summary>
private readonly Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size.
/// </summary>
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations.
/// </summary>
private readonly Configuration config;
public RgbForwardConverter(ImageFrame<TPixel> frame)
{
this.R = default;
this.G = default;
this.B = default;
// temporal pixel buffers
this.pixelSpan = new TPixel[PixelsPerSample].AsSpan();
this.rgbSpan = MemoryMarshal.Cast<byte, Rgb24>(new byte[RgbSpanByteSize + RgbToYCbCrConverterVectorized.AvxCompatibilityPadding].AsSpan());
// frame data
this.samplingAreaSize = new Size(frame.Width, frame.Height);
this.config = frame.GetConfiguration();
}
/// <summary>
/// Gets size of sampling area from given frame pixel buffer.
/// </summary>
private static Size SampleSize => new(8, 8);
/// <summary>
/// Converts a 8x8 image area inside 'pixels' at position (x, y) to Rgb24.
/// </summary>
public void Convert(int x, int y, ref RowOctet<TPixel> currentRows)
{
YCbCrForwardConverter<TPixel>.LoadAndStretchEdges(currentRows, this.pixelSpan, new Point(x, y), SampleSize, this.samplingAreaSize);
PixelOperations<TPixel>.Instance.ToRgb24(this.config, this.pixelSpan, this.rgbSpan);
ref Block8x8F redBlock = ref this.R;
ref Block8x8F greenBlock = ref this.G;
ref Block8x8F blueBlock = ref this.B;
if (RgbToYCbCrConverterVectorized.IsSupported)
{
ConvertAvx(this.rgbSpan, ref redBlock, ref greenBlock, ref blueBlock);
}
else
{
ConvertScalar(this.rgbSpan, ref redBlock, ref greenBlock, ref blueBlock);
}
}
/// <summary>
/// Converts 8x8 RGB24 pixel matrix to 8x8 Block of floats using Avx2 Intrinsics.
/// </summary>
/// <param name="rgbSpan">Span of Rgb24 pixels with size of 64</param>
/// <param name="rBlock">8x8 destination matrix of Red converted data</param>
/// <param name="gBlock">8x8 destination matrix of Blue converted data</param>
/// <param name="bBlock">8x8 destination matrix of Green converted data</param>
private static void ConvertAvx(Span<Rgb24> rgbSpan, ref Block8x8F rBlock, ref Block8x8F gBlock, ref Block8x8F bBlock)
{
Debug.Assert(RgbToYCbCrConverterVectorized.IsSupported, "AVX2 is required to run this converter");
#if SUPPORTS_RUNTIME_INTRINSICS
ref Vector256<byte> rgbByteSpan = ref Unsafe.As<Rgb24, Vector256<byte>>(ref MemoryMarshal.GetReference(rgbSpan));
ref Vector256<float> redRef = ref rBlock.V0;
ref Vector256<float> greenRef = ref gBlock.V0;
ref Vector256<float> blueRef = ref bBlock.V0;
var zero = Vector256.Create(0).AsByte();
var extractToLanesMask = Unsafe.As<byte, Vector256<uint>>(ref MemoryMarshal.GetReference(RgbToYCbCrConverterVectorized.MoveFirst24BytesToSeparateLanes));
var extractRgbMask = Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(RgbToYCbCrConverterVectorized.ExtractRgb));
Vector256<byte> rgb, rg, bx;
const int bytesPerRgbStride = 24;
for (nint i = 0; i < 8; i++)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, bytesPerRgbStride * i).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.Shuffle(rgb, extractRgbMask);
rg = Avx2.UnpackLow(rgb, zero);
bx = Avx2.UnpackHigh(rgb, zero);
Unsafe.Add(ref redRef, i) = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, zero).AsInt32());
Unsafe.Add(ref greenRef, i) = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, zero).AsInt32());
Unsafe.Add(ref blueRef, i) = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, zero).AsInt32());
}
#endif
}
private static void ConvertScalar(Span<Rgb24> rgbSpan, ref Block8x8F redBlock, ref Block8x8F greenBlock, ref Block8x8F blueBlock)
{
ref Rgb24 rgbStart = ref MemoryMarshal.GetReference(rgbSpan);
for (int i = 0; i < Block8x8F.Size; i++)
{
Rgb24 c = Unsafe.Add(ref rgbStart, (nint)(uint)i);
redBlock[i] = c.R;
greenBlock[i] = c.G;
blueBlock[i] = c.B;
}
}
}
}

237
src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterLut.cs

@ -1,237 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// Provides 8-bit lookup tables for converting from Rgb to YCbCr colorspace.
/// Methods to build the tables are based on libjpeg implementation.
/// </summary>
internal unsafe struct RgbToYCbCrConverterLut
{
/// <summary>
/// The red luminance table
/// </summary>
public fixed int YRTable[256];
/// <summary>
/// The green luminance table
/// </summary>
public fixed int YGTable[256];
/// <summary>
/// The blue luminance table
/// </summary>
public fixed int YBTable[256];
/// <summary>
/// The red blue-chrominance table
/// </summary>
public fixed int CbRTable[256];
/// <summary>
/// The green blue-chrominance table
/// </summary>
public fixed int CbGTable[256];
/// <summary>
/// The blue blue-chrominance table
/// B=>Cb and R=>Cr are the same
/// </summary>
public fixed int CbBTable[256];
/// <summary>
/// The green red-chrominance table
/// </summary>
public fixed int CrGTable[256];
/// <summary>
/// The blue red-chrominance table
/// </summary>
public fixed int CrBTable[256];
// Speediest right-shift on some machines and gives us enough accuracy at 4 decimal places.
private const int ScaleBits = 16;
private const int CBCrOffset = 128 << ScaleBits;
private const int Half = 1 << (ScaleBits - 1);
/// <summary>
/// Initializes the YCbCr tables
/// </summary>
/// <returns>The initialized <see cref="RgbToYCbCrConverterLut"/></returns>
public static RgbToYCbCrConverterLut Create()
{
RgbToYCbCrConverterLut tables = default;
for (int i = 0; i <= 255; i++)
{
// The values for the calculations are left scaled up since we must add them together before rounding.
tables.YRTable[i] = Fix(0.299F) * i;
tables.YGTable[i] = Fix(0.587F) * i;
tables.YBTable[i] = (Fix(0.114F) * i) + Half;
tables.CbRTable[i] = (-Fix(0.168735892F)) * i;
tables.CbGTable[i] = (-Fix(0.331264108F)) * i;
// We use a rounding fudge - factor of 0.5 - epsilon for Cb and Cr.
// This ensures that the maximum output will round to 255
// not 256, and thus that we don't have to range-limit.
//
// B=>Cb and R=>Cr tables are the same
tables.CbBTable[i] = (Fix(0.5F) * i) + CBCrOffset + Half - 1;
tables.CrGTable[i] = (-Fix(0.418687589F)) * i;
tables.CrBTable[i] = (-Fix(0.081312411F)) * i;
}
return tables;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private float CalculateY(byte r, byte g, byte b)
{
// float y = (0.299F * r) + (0.587F * g) + (0.114F * b);
return (this.YRTable[r] + this.YGTable[g] + this.YBTable[b]) >> ScaleBits;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private float CalculateCb(byte r, byte g, byte b)
{
// float cb = 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b));
return (this.CbRTable[r] + this.CbGTable[g] + this.CbBTable[b]) >> ScaleBits;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private float CalculateCr(byte r, byte g, byte b)
{
// float cr = 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b));
return (this.CbBTable[r] + this.CrGTable[g] + this.CrBTable[b]) >> ScaleBits;
}
/// <summary>
/// Converts Rgb24 pixels into YCbCr color space with 4:4:4 subsampling sampling of luminance and chroma.
/// </summary>
/// <param name="rgbSpan">Span of Rgb24 pixel data</param>
/// <param name="yBlock">Resulting Y values block</param>
/// <param name="cbBlock">Resulting Cb values block</param>
/// <param name="crBlock">Resulting Cr values block</param>
public void Convert444(Span<Rgb24> rgbSpan, ref Block8x8F yBlock, ref Block8x8F cbBlock, ref Block8x8F crBlock)
{
ref Rgb24 rgbStart = ref rgbSpan[0];
for (int i = 0; i < Block8x8F.Size; i++)
{
Rgb24 c = Unsafe.Add(ref rgbStart, i);
yBlock[i] = this.CalculateY(c.R, c.G, c.B);
cbBlock[i] = this.CalculateCb(c.R, c.G, c.B);
crBlock[i] = this.CalculateCr(c.R, c.G, c.B);
}
}
/// <summary>
/// Converts Rgb24 pixels into YCbCr color space with 4:2:0 subsampling of luminance and chroma.
/// </summary>
/// <remarks>Calculates 2 out of 4 luminance blocks and half of chroma blocks. This method must be called twice per 4x 8x8 DCT blocks with different row param.</remarks>
/// <param name="rgbSpan">Span of Rgb24 pixel data</param>
/// <param name="yBlockLeft">First or "left" resulting Y block</param>
/// <param name="yBlockRight">Second or "right" resulting Y block</param>
/// <param name="cbBlock">Resulting Cb values block</param>
/// <param name="crBlock">Resulting Cr values block</param>
/// <param name="row">Row index of the 16x16 block, 0 or 1</param>
public void Convert420(Span<Rgb24> rgbSpan, ref Block8x8F yBlockLeft, ref Block8x8F yBlockRight, ref Block8x8F cbBlock, ref Block8x8F crBlock, int row)
{
DebugGuard.MustBeBetweenOrEqualTo(row, 0, 1, nameof(row));
ref float yBlockLeftRef = ref Unsafe.As<Block8x8F, float>(ref yBlockLeft);
ref float yBlockRightRef = ref Unsafe.As<Block8x8F, float>(ref yBlockRight);
// 0-31 or 32-63
// upper or lower part
int chromaWriteOffset = row * (Block8x8F.Size / 2);
ref float cbBlockRef = ref Unsafe.Add(ref Unsafe.As<Block8x8F, float>(ref cbBlock), chromaWriteOffset);
ref float crBlockRef = ref Unsafe.Add(ref Unsafe.As<Block8x8F, float>(ref crBlock), chromaWriteOffset);
ref Rgb24 rgbStart = ref rgbSpan[0];
for (int i = 0; i < 8; i += 2)
{
int yBlockWriteOffset = i * 8;
ref Rgb24 stride = ref Unsafe.Add(ref rgbStart, i * 16);
int chromaOffset = 8 * (i / 2);
// left
this.ConvertChunk420(
ref stride,
ref Unsafe.Add(ref yBlockLeftRef, yBlockWriteOffset),
ref Unsafe.Add(ref cbBlockRef, chromaOffset),
ref Unsafe.Add(ref crBlockRef, chromaOffset));
// right
this.ConvertChunk420(
ref Unsafe.Add(ref stride, 8),
ref Unsafe.Add(ref yBlockRightRef, yBlockWriteOffset),
ref Unsafe.Add(ref cbBlockRef, chromaOffset + 4),
ref Unsafe.Add(ref crBlockRef, chromaOffset + 4));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ConvertChunk420(ref Rgb24 stride, ref float yBlock, ref float cbBlock, ref float crBlock)
{
// jpeg 8x8 blocks are processed as 16x16 blocks with 16x8 subpasses (this is done for performance reasons)
// each row is 16 pixels wide thus +16 stride reference offset
// resulting luminance (Y`) are sampled at original resolution thus +8 reference offset
for (int k = 0; k < 8; k += 2)
{
ref float yBlockRef = ref Unsafe.Add(ref yBlock, k);
// top row
Rgb24 px0 = Unsafe.Add(ref stride, k);
Rgb24 px1 = Unsafe.Add(ref stride, k + 1);
yBlockRef = this.CalculateY(px0.R, px0.G, px0.B);
Unsafe.Add(ref yBlockRef, 1) = this.CalculateY(px1.R, px1.G, px1.B);
// bottom row
Rgb24 px2 = Unsafe.Add(ref stride, k + 16);
Rgb24 px3 = Unsafe.Add(ref stride, k + 17);
Unsafe.Add(ref yBlockRef, 8) = this.CalculateY(px2.R, px2.G, px2.B);
Unsafe.Add(ref yBlockRef, 9) = this.CalculateY(px3.R, px3.G, px3.B);
// chroma average for 2x2 pixel block
Unsafe.Add(ref cbBlock, k / 2) = this.CalculateAverageCb(px0, px1, px2, px3);
Unsafe.Add(ref crBlock, k / 2) = this.CalculateAverageCr(px0, px1, px2, px3);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private float CalculateAverageCb(Rgb24 px0, Rgb24 px1, Rgb24 px2, Rgb24 px3)
{
return 0.25f
* (this.CalculateCb(px0.R, px0.G, px0.B)
+ this.CalculateCb(px1.R, px1.G, px1.B)
+ this.CalculateCb(px2.R, px2.G, px2.B)
+ this.CalculateCb(px3.R, px3.G, px3.B));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private float CalculateAverageCr(Rgb24 px0, Rgb24 px1, Rgb24 px2, Rgb24 px3)
{
return 0.25f
* (this.CalculateCr(px0.R, px0.G, px0.B)
+ this.CalculateCr(px1.R, px1.G, px1.B)
+ this.CalculateCr(px2.R, px2.G, px2.B)
+ this.CalculateCr(px3.R, px3.G, px3.B));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Fix(float x)
=> (int)((x * (1L << ScaleBits)) + 0.5F);
}
}

259
src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterVectorized.cs

@ -1,259 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Diagnostics;
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
internal static class RgbToYCbCrConverterVectorized
{
public static bool IsSupported
{
get
{
#if SUPPORTS_RUNTIME_INTRINSICS
return Avx2.IsSupported;
#else
return false;
#endif
}
}
public static int AvxCompatibilityPadding
{
// rgb byte matrices contain 8 strides by 8 pixels each, thus 64 pixels total
// Strides are stored sequentially - one big span of 64 * 3 = 192 bytes
// Each stride has exactly 3 * 8 = 24 bytes or 3 * 8 * 8 = 192 bits
// Avx registers are 256 bits so rgb span will be loaded with extra 64 bits from the next stride:
// stride 0 0 - 192 -(+64bits)-> 256
// stride 1 192 - 384 -(+64bits)-> 448
// stride 2 384 - 576 -(+64bits)-> 640
// stride 3 576 - 768 -(+64bits)-> 832
// stride 4 768 - 960 -(+64bits)-> 1024
// stride 5 960 - 1152 -(+64bits)-> 1216
// stride 6 1152 - 1344 -(+64bits)-> 1408
// stride 7 1344 - 1536 -(+64bits)-> 1600 <-- READ ACCESS VIOLATION
//
// Total size of the 64 pixel rgb span: 64 * 3 * 8 = 1536 bits, avx operations require 1600 bits
// This is not permitted - we are reading foreign memory
//
// 8 byte padding to rgb byte span will solve this problem without extra code in converters
get
{
#if SUPPORTS_RUNTIME_INTRINSICS
if (IsSupported)
{
return 8;
}
#endif
return 0;
}
}
#if SUPPORTS_RUNTIME_INTRINSICS
internal static ReadOnlySpan<byte> MoveFirst24BytesToSeparateLanes => new byte[]
{
0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0,
3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0
};
internal static ReadOnlySpan<byte> ExtractRgb => new byte[]
{
0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF,
0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF
};
#endif
/// <summary>
/// Converts 8x8 Rgb24 pixel matrix to YCbCr pixel matrices with 4:4:4 subsampling
/// </summary>
/// <remarks>Total size of rgb span must be 200 bytes</remarks>
/// <param name="rgbSpan">Span of rgb pixels with size of 64</param>
/// <param name="yBlock">8x8 destination matrix of Luminance(Y) converted data</param>
/// <param name="cbBlock">8x8 destination matrix of Chrominance(Cb) converted data</param>
/// <param name="crBlock">8x8 destination matrix of Chrominance(Cr) converted data</param>
public static void Convert444(ReadOnlySpan<Rgb24> rgbSpan, ref Block8x8F yBlock, ref Block8x8F cbBlock, ref Block8x8F crBlock)
{
Debug.Assert(IsSupported, "AVX2 is required to run this converter");
#if SUPPORTS_RUNTIME_INTRINSICS
var f0299 = Vector256.Create(0.299f);
var f0587 = Vector256.Create(0.587f);
var f0114 = Vector256.Create(0.114f);
var fn0168736 = Vector256.Create(-0.168736f);
var fn0331264 = Vector256.Create(-0.331264f);
var f128 = Vector256.Create(128f);
var fn0418688 = Vector256.Create(-0.418688f);
var fn0081312F = Vector256.Create(-0.081312F);
var f05 = Vector256.Create(0.5f);
var zero = Vector256.Create(0).AsByte();
ref Vector256<byte> rgbByteSpan = ref Unsafe.As<Rgb24, Vector256<byte>>(ref MemoryMarshal.GetReference(rgbSpan));
ref Vector256<float> destYRef = ref yBlock.V0;
ref Vector256<float> destCbRef = ref cbBlock.V0;
ref Vector256<float> destCrRef = ref crBlock.V0;
var extractToLanesMask = Unsafe.As<byte, Vector256<uint>>(ref MemoryMarshal.GetReference(MoveFirst24BytesToSeparateLanes));
var extractRgbMask = Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(ExtractRgb));
Vector256<byte> rgb, rg, bx;
Vector256<float> r, g, b;
const int bytesPerRgbStride = 24;
for (int i = 0; i < 8; i++)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (IntPtr)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.Shuffle(rgb, extractRgbMask);
rg = Avx2.UnpackLow(rgb, zero);
bx = Avx2.UnpackHigh(rgb, zero);
r = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, zero).AsInt32());
g = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, zero).AsInt32());
b = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, zero).AsInt32());
// (0.299F * r) + (0.587F * g) + (0.114F * b);
Unsafe.Add(ref destYRef, i) = SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(f0114, b), f0587, g), f0299, r);
// 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b))
Unsafe.Add(ref destCbRef, i) = Avx.Add(f128, SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(f05, b), fn0331264, g), fn0168736, r));
// 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b))
Unsafe.Add(ref destCrRef, i) = Avx.Add(f128, SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(fn0081312F, b), fn0418688, g), f05, r));
}
#endif
}
/// <summary>
/// Converts 16x8 Rgb24 pixels matrix to 2 Y 8x8 matrices with 4:2:0 subsampling
/// </summary>
public static void Convert420(ReadOnlySpan<Rgb24> rgbSpan, ref Block8x8F yBlockLeft, ref Block8x8F yBlockRight, ref Block8x8F cbBlock, ref Block8x8F crBlock, int row)
{
Debug.Assert(IsSupported, "AVX2 is required to run this converter");
#if SUPPORTS_RUNTIME_INTRINSICS
var f0299 = Vector256.Create(0.299f);
var f0587 = Vector256.Create(0.587f);
var f0114 = Vector256.Create(0.114f);
var fn0168736 = Vector256.Create(-0.168736f);
var fn0331264 = Vector256.Create(-0.331264f);
var f128 = Vector256.Create(128f);
var fn0418688 = Vector256.Create(-0.418688f);
var fn0081312F = Vector256.Create(-0.081312F);
var f05 = Vector256.Create(0.5f);
var zero = Vector256.Create(0).AsByte();
ref Vector256<byte> rgbByteSpan = ref Unsafe.As<Rgb24, Vector256<byte>>(ref MemoryMarshal.GetReference(rgbSpan));
int destOffset = row * 4;
ref Vector256<float> destCbRef = ref Unsafe.Add(ref Unsafe.As<Block8x8F, Vector256<float>>(ref cbBlock), destOffset);
ref Vector256<float> destCrRef = ref Unsafe.Add(ref Unsafe.As<Block8x8F, Vector256<float>>(ref crBlock), destOffset);
var extractToLanesMask = Unsafe.As<byte, Vector256<uint>>(ref MemoryMarshal.GetReference(MoveFirst24BytesToSeparateLanes));
var extractRgbMask = Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(ExtractRgb));
Vector256<byte> rgb, rg, bx;
Vector256<float> r, g, b;
Span<Vector256<float>> rDataLanes = stackalloc Vector256<float>[4];
Span<Vector256<float>> gDataLanes = stackalloc Vector256<float>[4];
Span<Vector256<float>> bDataLanes = stackalloc Vector256<float>[4];
const int bytesPerRgbStride = 24;
for (int i = 0; i < 4; i++)
{
// 16x2 => 8x1
// left 8x8 column conversions
for (int j = 0; j < 4; j += 2)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (IntPtr)(bytesPerRgbStride * ((i * 4) + j))).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.Shuffle(rgb, extractRgbMask);
rg = Avx2.UnpackLow(rgb, zero);
bx = Avx2.UnpackHigh(rgb, zero);
r = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, zero).AsInt32());
g = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, zero).AsInt32());
b = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, zero).AsInt32());
int yBlockVerticalOffset = (i * 2) + ((j & 2) >> 1);
// (0.299F * r) + (0.587F * g) + (0.114F * b);
Unsafe.Add(ref yBlockLeft.V0, yBlockVerticalOffset) = SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(f0114, b), f0587, g), f0299, r);
rDataLanes[j] = r;
gDataLanes[j] = g;
bDataLanes[j] = b;
}
// 16x2 => 8x1
// right 8x8 column conversions
for (int j = 1; j < 4; j += 2)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (IntPtr)(bytesPerRgbStride * ((i * 4) + j))).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.Shuffle(rgb, extractRgbMask);
rg = Avx2.UnpackLow(rgb, zero);
bx = Avx2.UnpackHigh(rgb, zero);
r = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, zero).AsInt32());
g = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, zero).AsInt32());
b = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, zero).AsInt32());
int yBlockVerticalOffset = (i * 2) + ((j & 2) >> 1);
// (0.299F * r) + (0.587F * g) + (0.114F * b);
Unsafe.Add(ref yBlockRight.V0, yBlockVerticalOffset) = SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(f0114, b), f0587, g), f0299, r);
rDataLanes[j] = r;
gDataLanes[j] = g;
bDataLanes[j] = b;
}
r = Scale16x2_8x1(rDataLanes);
g = Scale16x2_8x1(gDataLanes);
b = Scale16x2_8x1(bDataLanes);
// 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b))
Unsafe.Add(ref destCbRef, i) = Avx.Add(f128, SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(f05, b), fn0331264, g), fn0168736, r));
// 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b))
Unsafe.Add(ref destCrRef, i) = Avx.Add(f128, SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(fn0081312F, b), fn0418688, g), f05, r));
}
#endif
}
#if SUPPORTS_RUNTIME_INTRINSICS
/// <summary>
/// Scales 16x2 matrix to 8x1 using 2x2 average
/// </summary>
/// <param name="v">Input matrix consisting of 4 256bit vectors</param>
/// <returns>256bit vector containing upper and lower scaled parts of the input matrix</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector256<float> Scale16x2_8x1(ReadOnlySpan<Vector256<float>> v)
{
Debug.Assert(Avx2.IsSupported, "AVX2 is required to run this converter");
DebugGuard.IsTrue(v.Length == 4, "Input span must consist of 4 elements");
var f025 = Vector256.Create(0.25f);
Vector256<float> left = Avx.Add(v[0], v[2]);
Vector256<float> right = Avx.Add(v[1], v[3]);
Vector256<float> avg2x2 = Avx.Multiply(Avx.HorizontalAdd(left, right), f025);
return Avx2.Permute4x64(avg2x2.AsDouble(), 0b11_01_10_00).AsSingle();
}
#endif
}
}

12
src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs

@ -0,0 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// Converter used to convert pixel data to jpeg spectral data.
/// </summary>
internal abstract class SpectralConverter
{
}
}

149
src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs

@ -0,0 +1,149 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Buffers;
using System.Linq;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <inheritdoc/>
internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ComponentProcessor[] componentProcessors;
private readonly int pixelRowsPerStep;
private int pixelRowCounter;
private readonly Buffer2D<TPixel> pixelBuffer;
private readonly IMemoryOwner<float> redLane;
private readonly IMemoryOwner<float> greenLane;
private readonly IMemoryOwner<float> blueLane;
private readonly int alignedPixelWidth;
private readonly JpegColorConverterBase colorConverter;
public SpectralConverter(JpegFrame frame, Image<TPixel> image, Block8x8F[] dequantTables)
{
MemoryAllocator allocator = image.GetConfiguration().MemoryAllocator;
// iteration data
int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width);
int majorVerticalSamplingFactor = frame.Components.Max((component) => component.SamplingFactors.Height);
const int blockPixelHeight = 8;
this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelHeight;
// pixel buffer of the image
this.pixelBuffer = image.GetRootFramePixelBuffer();
// component processors from spectral to Rgb24
const int blockPixelWidth = 8;
this.alignedPixelWidth = majorBlockWidth * blockPixelWidth;
var postProcessorBufferSize = new Size(this.alignedPixelWidth, this.pixelRowsPerStep);
this.componentProcessors = new ComponentProcessor[frame.Components.Length];
for (int i = 0; i < this.componentProcessors.Length; i++)
{
Component component = frame.Components[i];
this.componentProcessors[i] = new ComponentProcessor(
allocator,
component,
postProcessorBufferSize,
dequantTables[component.QuantizationTableIndex]);
}
this.redLane = allocator.Allocate<float>(this.alignedPixelWidth, AllocationOptions.Clean);
this.greenLane = allocator.Allocate<float>(this.alignedPixelWidth, AllocationOptions.Clean);
this.blueLane = allocator.Allocate<float>(this.alignedPixelWidth, AllocationOptions.Clean);
// color converter from Rgb24 to YCbCr
this.colorConverter = JpegColorConverterBase.GetConverter(colorSpace: frame.ColorSpace, precision: 8);
}
public void ConvertStrideBaseline()
{
// Codestyle suggests expression body but it
// also requires empty line before comments
// which looks ugly with expression bodies thus this warning disable
#pragma warning disable IDE0022
// Convert next pixel stride using single spectral `stride'
// Note that zero passing eliminates the need of virtual call
// from JpegComponentPostProcessor
this.ConvertStride(spectralStep: 0);
#pragma warning restore IDE0022
}
public void ConvertFull()
{
int steps = (int)Numerics.DivideCeil((uint)this.pixelBuffer.Height, (uint)this.pixelRowsPerStep);
for (int i = 0; i < steps; i++)
{
this.ConvertStride(i);
}
}
private void ConvertStride(int spectralStep)
{
int start = this.pixelRowCounter;
int end = start + this.pixelRowsPerStep;
int pixelBufferLastVerticalIndex = this.pixelBuffer.Height - 1;
// Pixel strides must be padded with the last pixel of the stride
int paddingStartIndex = this.pixelBuffer.Width;
int paddedPixelsCount = this.alignedPixelWidth - this.pixelBuffer.Width;
Span<float> rLane = this.redLane.GetSpan();
Span<float> gLane = this.greenLane.GetSpan();
Span<float> bLane = this.blueLane.GetSpan();
for (int yy = start; yy < end; yy++)
{
int y = yy - this.pixelRowCounter;
// Unpack TPixel to r/g/b planes
int srcIndex = Math.Min(yy, pixelBufferLastVerticalIndex);
Span<TPixel> sourceRow = this.pixelBuffer.DangerousGetRowSpan(srcIndex);
PixelOperations<TPixel>.Instance.UnpackIntoRgbPlanes(rLane, gLane, bLane, sourceRow);
rLane.Slice(paddingStartIndex).Fill(rLane[paddingStartIndex - 1]);
gLane.Slice(paddingStartIndex).Fill(gLane[paddingStartIndex - 1]);
bLane.Slice(paddingStartIndex).Fill(bLane[paddingStartIndex - 1]);
// Convert from rgb24 to target pixel type
var values = new JpegColorConverterBase.ComponentValues(this.componentProcessors, y);
this.colorConverter.ConvertFromRgb(values, rLane, gLane, bLane);
}
// Convert pixels to spectral
for (int i = 0; i < this.componentProcessors.Length; i++)
{
this.componentProcessors[i].CopyColorBufferToBlocks(spectralStep);
}
this.pixelRowCounter = end;
}
/// <inheritdoc/>
public void Dispose()
{
foreach (ComponentProcessor cpp in this.componentProcessors)
{
cpp.Dispose();
}
this.redLane.Dispose();
this.greenLane.Dispose();
this.blueLane.Dispose();
}
}
}

121
src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs

@ -1,121 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// On-stack worker struct to efficiently encapsulate the TPixel -> Rgb24 -> YCbCr conversion chain of 8x8 pixel blocks.
/// </summary>
/// <typeparam name="TPixel">The pixel type to work on</typeparam>
internal ref struct YCbCrForwardConverter420<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Number of pixels processed per single <see cref="Convert(int, int, ref RowOctet{TPixel}, int)"/> call
/// </summary>
private const int PixelsPerSample = 16 * 8;
/// <summary>
/// Total byte size of processed pixels converted from TPixel to <see cref="Rgb24"/>
/// </summary>
private const int RgbSpanByteSize = PixelsPerSample * 3;
/// <summary>
/// The left Y component
/// </summary>
public Block8x8F YLeft;
/// <summary>
/// The left Y component
/// </summary>
public Block8x8F YRight;
/// <summary>
/// The Cb component
/// </summary>
public Block8x8F Cb;
/// <summary>
/// The Cr component
/// </summary>
public Block8x8F Cr;
/// <summary>
/// The color conversion tables
/// </summary>
private RgbToYCbCrConverterLut colorTables;
/// <summary>
/// Temporal 16x8 block to hold TPixel data
/// </summary>
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal RGB block
/// </summary>
private readonly Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size
/// </summary>
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations
/// </summary>
private readonly Configuration config;
public YCbCrForwardConverter420(ImageFrame<TPixel> frame)
{
// matrices would be filled during convert calls
this.YLeft = default;
this.YRight = default;
this.Cb = default;
this.Cr = default;
// temporal pixel buffers
this.pixelSpan = new TPixel[PixelsPerSample].AsSpan();
this.rgbSpan = MemoryMarshal.Cast<byte, Rgb24>(new byte[RgbSpanByteSize + RgbToYCbCrConverterVectorized.AvxCompatibilityPadding].AsSpan());
// frame data
this.samplingAreaSize = new Size(frame.Width, frame.Height);
this.config = frame.GetConfiguration();
// conversion vector fallback data
if (!RgbToYCbCrConverterVectorized.IsSupported)
{
this.colorTables = RgbToYCbCrConverterLut.Create();
}
else
{
this.colorTables = default;
}
}
/// <summary>
/// Gets size of sampling area from given frame pixel buffer.
/// </summary>
private static Size SampleSize => new(16, 8);
public void Convert(int x, int y, ref RowOctet<TPixel> currentRows, int idx)
{
YCbCrForwardConverter<TPixel>.LoadAndStretchEdges(currentRows, this.pixelSpan, new Point(x, y), SampleSize, this.samplingAreaSize);
PixelOperations<TPixel>.Instance.ToRgb24(this.config, this.pixelSpan, this.rgbSpan);
if (RgbToYCbCrConverterVectorized.IsSupported)
{
RgbToYCbCrConverterVectorized.Convert420(this.rgbSpan, ref this.YLeft, ref this.YRight, ref this.Cb, ref this.Cr, idx);
}
else
{
this.colorTables.Convert420(this.rgbSpan, ref this.YLeft, ref this.YRight, ref this.Cb, ref this.Cr, idx);
}
}
}
}

122
src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs

@ -1,122 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// On-stack worker struct to efficiently encapsulate the TPixel -> Rgb24 -> YCbCr conversion chain of 8x8 pixel blocks.
/// </summary>
/// <typeparam name="TPixel">The pixel type to work on</typeparam>
internal ref struct YCbCrForwardConverter444<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Number of pixels processed per single <see cref="Convert(int, int, ref RowOctet{TPixel})"/> call
/// </summary>
private const int PixelsPerSample = 8 * 8;
/// <summary>
/// Total byte size of processed pixels converted from TPixel to <see cref="Rgb24"/>
/// </summary>
private const int RgbSpanByteSize = PixelsPerSample * 3;
/// <summary>
/// The Y component
/// </summary>
public Block8x8F Y;
/// <summary>
/// The Cb component
/// </summary>
public Block8x8F Cb;
/// <summary>
/// The Cr component
/// </summary>
public Block8x8F Cr;
/// <summary>
/// The color conversion tables
/// </summary>
private RgbToYCbCrConverterLut colorTables;
/// <summary>
/// Temporal 64-byte span to hold unconverted TPixel data
/// </summary>
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal 64-byte span to hold converted Rgb24 data
/// </summary>
private readonly Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size
/// </summary>
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations
/// </summary>
private readonly Configuration config;
public YCbCrForwardConverter444(ImageFrame<TPixel> frame)
{
// matrices would be filled during convert calls
this.Y = default;
this.Cb = default;
this.Cr = default;
// temporal pixel buffers
this.pixelSpan = new TPixel[PixelsPerSample].AsSpan();
this.rgbSpan = MemoryMarshal.Cast<byte, Rgb24>(new byte[RgbSpanByteSize + RgbToYCbCrConverterVectorized.AvxCompatibilityPadding].AsSpan());
// frame data
this.samplingAreaSize = new Size(frame.Width, frame.Height);
this.config = frame.GetConfiguration();
// conversion vector fallback data
if (!RgbToYCbCrConverterVectorized.IsSupported)
{
this.colorTables = RgbToYCbCrConverterLut.Create();
}
else
{
this.colorTables = default;
}
}
/// <summary>
/// Gets size of sampling area from given frame pixel buffer.
/// </summary>
private static Size SampleSize => new(8, 8);
/// <summary>
/// Converts a 8x8 image area inside 'pixels' at position (x,y) placing the result members of the structure (<see cref="Y"/>, <see cref="Cb"/>, <see cref="Cr"/>)
/// </summary>
public void Convert(int x, int y, ref RowOctet<TPixel> currentRows)
{
YCbCrForwardConverter<TPixel>.LoadAndStretchEdges(currentRows, this.pixelSpan, new Point(x, y), SampleSize, this.samplingAreaSize);
PixelOperations<TPixel>.Instance.ToRgb24(this.config, this.pixelSpan, this.rgbSpan);
ref Block8x8F yBlock = ref this.Y;
ref Block8x8F cbBlock = ref this.Cb;
ref Block8x8F crBlock = ref this.Cr;
if (RgbToYCbCrConverterVectorized.IsSupported)
{
RgbToYCbCrConverterVectorized.Convert444(this.rgbSpan, ref yBlock, ref cbBlock, ref crBlock);
}
else
{
this.colorTables.Convert444(this.rgbSpan, ref yBlock, ref cbBlock, ref crBlock);
}
}
}
}

61
src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs

@ -1,61 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
internal static class YCbCrForwardConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
public static void LoadAndStretchEdges(RowOctet<TPixel> source, Span<TPixel> dest, Point start, Size sampleSize, Size totalSize)
{
DebugGuard.MustBeBetweenOrEqualTo(start.X, 0, totalSize.Width - 1, nameof(start.X));
DebugGuard.MustBeBetweenOrEqualTo(start.Y, 0, totalSize.Height - 1, nameof(start.Y));
int width = Math.Min(sampleSize.Width, totalSize.Width - start.X);
int height = Math.Min(sampleSize.Height, totalSize.Height - start.Y);
uint byteWidth = (uint)(width * Unsafe.SizeOf<TPixel>());
int remainderXCount = sampleSize.Width - width;
ref byte blockStart = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<TPixel, byte>(dest));
int rowSizeInBytes = sampleSize.Width * Unsafe.SizeOf<TPixel>();
for (int y = 0; y < height; y++)
{
Span<TPixel> row = source[y];
ref byte s = ref Unsafe.As<TPixel, byte>(ref row[start.X]);
ref byte d = ref Unsafe.Add(ref blockStart, y * rowSizeInBytes);
Unsafe.CopyBlock(ref d, ref s, byteWidth);
ref TPixel last = ref Unsafe.Add(ref Unsafe.As<byte, TPixel>(ref d), width - 1);
for (int x = 1; x <= remainderXCount; x++)
{
Unsafe.Add(ref last, x) = last;
}
}
int remainderYCount = sampleSize.Height - height;
if (remainderYCount == 0)
{
return;
}
ref byte lastRowStart = ref Unsafe.Add(ref blockStart, (height - 1) * rowSizeInBytes);
for (int y = 1; y <= remainderYCount; y++)
{
ref byte remStart = ref Unsafe.Add(ref lastRowStart, rowSizeInBytes * y);
Unsafe.CopyBlock(ref remStart, ref lastRowStart, (uint)rowSizeInBytes);
}
}
}
}

4
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegColorSpace.cs → src/ImageSharp/Formats/Jpeg/Components/JpegColorSpace.cs

@ -1,15 +1,13 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{ {
/// <summary> /// <summary>
/// Identifies the colorspace of a Jpeg image. /// Identifies the colorspace of a Jpeg image.
/// </summary> /// </summary>
internal enum JpegColorSpace internal enum JpegColorSpace
{ {
Undefined = 0,
/// <summary> /// <summary>
/// Color space with 1 component. /// Color space with 1 component.
/// </summary> /// </summary>

15
src/ImageSharp/Formats/Jpeg/Components/Quantization.cs

@ -176,7 +176,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
return quality < 50 ? (5000 / quality) : (200 - (quality * 2)); return quality < 50 ? (5000 / quality) : (200 - (quality * 2));
} }
private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan<byte> unscaledTable) public static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan<byte> unscaledTable)
{ {
Block8x8F table = default; Block8x8F table = default;
for (int j = 0; j < Block8x8F.Size; j++) for (int j = 0; j < Block8x8F.Size; j++)
@ -188,6 +188,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
return table; return table;
} }
public static Block8x8 ScaleQuantizationTable(int quality, Block8x8 unscaledTable)
{
int scale = QualityToScale(quality);
Block8x8 table = default;
for (int j = 0; j < Block8x8.Size; j++)
{
int x = ((unscaledTable[j] * scale) + 50) / 100;
table[j] = (short)(uint)Numerics.Clamp(x, 1, 255);
}
return table;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Block8x8F ScaleLuminanceTable(int quality) public static Block8x8F ScaleLuminanceTable(int quality)
=> ScaleQuantizationTable(scale: QualityToScale(quality), LuminanceTable); => ScaleQuantizationTable(scale: QualityToScale(quality), LuminanceTable);

13
src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs

@ -16,8 +16,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public int? Quality { get; set; } public int? Quality { get; set; }
/// <summary> /// <summary>
/// Gets the color type, that will be used to encode the image. /// Gets or sets the component encoding mode.
/// </summary> /// </summary>
JpegColorType? ColorType { get; } /// <remarks>
/// Interleaved encoding mode encodes all color components in a single scan.
/// Non-interleaved encoding mode encodes each color component in a separate scan.
/// </remarks>
public bool? Interleaved { get; set; }
/// <summary>
/// Gets or sets jpeg color for encoding.
/// </summary>
public JpegEncodingColor? ColorType { get; set; }
} }
} }

1
src/ImageSharp/Formats/Jpeg/JpegDecoder.cs

@ -15,6 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <inheritdoc/> /// <inheritdoc/>
IImageInfo IImageInfoDetector.Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) IImageInfo IImageInfoDetector.Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{ {
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
using JpegDecoderCore decoder = new(new() { GeneralOptions = options }); using JpegDecoderCore decoder = new(new() { GeneralOptions = options });

38
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -500,6 +500,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Read on. // Read on.
fileMarker = FindNextFileMarker(stream); fileMarker = FindNextFileMarker(stream);
} }
this.Metadata.GetJpegMetadata().Interleaved = this.Frame.Interleaved;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -579,57 +581,58 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Returns the jpeg color type based on the colorspace and subsampling used. /// Returns the jpeg color type based on the colorspace and subsampling used.
/// </summary> /// </summary>
/// <returns>Jpeg color type.</returns> /// <returns>Jpeg color type.</returns>
private JpegColorType DeduceJpegColorType() private JpegEncodingColor DeduceJpegColorType()
{ {
switch (this.ColorSpace) switch (this.ColorSpace)
{ {
case JpegColorSpace.Grayscale: case JpegColorSpace.Grayscale:
return JpegColorType.Luminance; return JpegEncodingColor.Luminance;
case JpegColorSpace.RGB: case JpegColorSpace.RGB:
return JpegColorType.Rgb; return JpegEncodingColor.Rgb;
case JpegColorSpace.YCbCr: case JpegColorSpace.YCbCr:
if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 && if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegColorType.YCbCrRatio444; return JpegEncodingColor.YCbCrRatio444;
} }
else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 2 && else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 1 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegColorType.YCbCrRatio420; return JpegEncodingColor.YCbCrRatio422;
} }
else if (this.Frame.Components[0].HorizontalSamplingFactor == 1 && this.Frame.Components[0].VerticalSamplingFactor == 1 && else if (this.Frame.Components[0].HorizontalSamplingFactor == 2 && this.Frame.Components[0].VerticalSamplingFactor == 2 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 2 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 2) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegColorType.YCbCrRatio422; return JpegEncodingColor.YCbCrRatio420;
} }
else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 1 && else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 1 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegColorType.YCbCrRatio411; return JpegEncodingColor.YCbCrRatio411;
} }
else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 2 && else if (this.Frame.Components[0].HorizontalSamplingFactor == 4 && this.Frame.Components[0].VerticalSamplingFactor == 2 &&
this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 && this.Frame.Components[1].HorizontalSamplingFactor == 1 && this.Frame.Components[1].VerticalSamplingFactor == 1 &&
this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1) this.Frame.Components[2].HorizontalSamplingFactor == 1 && this.Frame.Components[2].VerticalSamplingFactor == 1)
{ {
return JpegColorType.YCbCrRatio410; return JpegEncodingColor.YCbCrRatio410;
} }
else else
{ {
return JpegColorType.YCbCrRatio420; return JpegEncodingColor.YCbCrRatio420;
} }
case JpegColorSpace.Cmyk: case JpegColorSpace.Cmyk:
return JpegColorType.Cmyk; return JpegEncodingColor.Cmyk;
case JpegColorSpace.Ycck:
return JpegEncodingColor.Ycck;
default: default:
return JpegColorType.YCbCrRatio420; return JpegEncodingColor.YCbCrRatio420;
} }
} }
@ -1212,6 +1215,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount);
this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive;
remaining -= length; remaining -= length;
@ -1422,7 +1426,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// selectorsCount*2 bytes: component index + huffman tables indices // selectorsCount*2 bytes: component index + huffman tables indices
stream.Read(this.temp, 0, selectorsBytes); stream.Read(this.temp, 0, selectorsBytes);
this.Frame.MultiScan = this.Frame.ComponentCount != selectorsCount; this.Frame.Interleaved = this.Frame.ComponentCount == selectorsCount;
for (int i = 0; i < selectorsBytes; i += 2) for (int i = 0; i < selectorsBytes; i += 2)
{ {
// 1 byte: Component id // 1 byte: Component id

25
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,11 +14,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions
{ {
/// <summary>
/// Backing field for <see cref="Quality"/>.
/// </summary>
private int? quality;
/// <inheritdoc/>
public int? Quality
{
get => this.quality;
set
{
if (value is < 1 or > 100)
{
throw new ArgumentException("Quality factor must be in [1..100] range.");
}
this.quality = value;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public int? Quality { get; set; } public bool? Interleaved { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public JpegColorType? ColorType { get; set; } public JpegEncodingColor? ColorType { get; set; }
/// <summary> /// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.

196
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs

@ -0,0 +1,196 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
namespace SixLabors.ImageSharp.Formats.Jpeg
{
/// <summary>
/// Image encoder for writing an image to a stream as a jpeg.
/// </summary>
internal sealed unsafe partial class JpegEncoderCore
{
private static JpegFrameConfig[] CreateFrameConfigs()
{
var defaultLuminanceHuffmanDC = new JpegHuffmanTableConfig(@class: 0, destIndex: 0, HuffmanSpec.LuminanceDC);
var defaultLuminanceHuffmanAC = new JpegHuffmanTableConfig(@class: 1, destIndex: 0, HuffmanSpec.LuminanceAC);
var defaultChrominanceHuffmanDC = new JpegHuffmanTableConfig(@class: 0, destIndex: 1, HuffmanSpec.ChrominanceDC);
var defaultChrominanceHuffmanAC = new JpegHuffmanTableConfig(@class: 1, destIndex: 1, HuffmanSpec.ChrominanceAC);
var defaultLuminanceQuantTable = new JpegQuantizationTableConfig(0, Quantization.LuminanceTable);
var defaultChrominanceQuantTable = new JpegQuantizationTableConfig(1, Quantization.ChrominanceTable);
var yCbCrHuffmanConfigs = new JpegHuffmanTableConfig[]
{
defaultLuminanceHuffmanDC,
defaultLuminanceHuffmanAC,
defaultChrominanceHuffmanDC,
defaultChrominanceHuffmanAC,
};
var yCbCrQuantTableConfigs = new JpegQuantizationTableConfig[]
{
defaultLuminanceQuantTable,
defaultChrominanceQuantTable,
};
return new JpegFrameConfig[]
{
// YCbCr 4:4:4
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio444,
new JpegComponentConfig[]
{
new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
},
yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs),
// YCbCr 4:2:2
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio422,
new JpegComponentConfig[]
{
new JpegComponentConfig(id: 1, hsf: 2, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
},
yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs),
// YCbCr 4:2:0
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio420,
new JpegComponentConfig[]
{
new JpegComponentConfig(id: 1, hsf: 2, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
},
yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs),
// YCbCr 4:1:1
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio411,
new JpegComponentConfig[]
{
new JpegComponentConfig(id: 1, hsf: 4, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
},
yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs),
// YCbCr 4:1:0
new JpegFrameConfig(
JpegColorSpace.YCbCr,
JpegEncodingColor.YCbCrRatio410,
new JpegComponentConfig[]
{
new JpegComponentConfig(id: 1, hsf: 4, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
},
yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs),
// Luminance
new JpegFrameConfig(
JpegColorSpace.Grayscale,
JpegEncodingColor.Luminance,
new JpegComponentConfig[]
{
new JpegComponentConfig(id: 0, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
},
new JpegHuffmanTableConfig[]
{
defaultLuminanceHuffmanDC,
defaultLuminanceHuffmanAC
},
new JpegQuantizationTableConfig[]
{
defaultLuminanceQuantTable
}),
// Rgb
new JpegFrameConfig(
JpegColorSpace.RGB,
JpegEncodingColor.Rgb,
new JpegComponentConfig[]
{
new JpegComponentConfig(id: 82, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 71, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 66, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
},
new JpegHuffmanTableConfig[]
{
defaultLuminanceHuffmanDC,
defaultLuminanceHuffmanAC
},
new JpegQuantizationTableConfig[]
{
defaultLuminanceQuantTable
})
{
AdobeColorTransformMarkerFlag = JpegConstants.Adobe.ColorTransformUnknown
},
// Cmyk
new JpegFrameConfig(
JpegColorSpace.Cmyk,
JpegEncodingColor.Cmyk,
new JpegComponentConfig[]
{
new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 4, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
},
new JpegHuffmanTableConfig[]
{
defaultLuminanceHuffmanDC,
defaultLuminanceHuffmanAC
},
new JpegQuantizationTableConfig[]
{
defaultLuminanceQuantTable
})
{
AdobeColorTransformMarkerFlag = JpegConstants.Adobe.ColorTransformUnknown,
},
// YccK
new JpegFrameConfig(
JpegColorSpace.Ycck,
JpegEncodingColor.Ycck,
new JpegComponentConfig[]
{
new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 4, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
},
new JpegHuffmanTableConfig[]
{
defaultLuminanceHuffmanDC,
defaultLuminanceHuffmanAC
},
new JpegQuantizationTableConfig[]
{
defaultLuminanceQuantTable
})
{
AdobeColorTransformMarkerFlag = JpegConstants.Adobe.ColorTransformYcck,
},
};
}
}
}

556
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -8,7 +8,6 @@ using System.Linq;
using System.Threading; using System.Threading;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -22,27 +21,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream as a jpeg. /// Image encoder for writing an image to a stream as a jpeg.
/// </summary> /// </summary>
internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
{ {
/// <summary> /// <summary>
/// The number of quantization tables. /// The available encodable frame configs.
/// </summary> /// </summary>
private const int QuantizationTableCount = 2; private static readonly JpegFrameConfig[] FrameConfigs = CreateFrameConfigs();
/// <summary> /// <summary>
/// A scratch buffer to reduce allocations. /// A scratch buffer to reduce allocations.
/// </summary> /// </summary>
private readonly byte[] buffer = new byte[20]; private readonly byte[] buffer = new byte[20];
/// <summary> private readonly IJpegEncoderOptions options;
/// The quality, that will be used to encode the image.
/// </summary>
private readonly int? quality;
/// <summary>
/// Gets or sets the colorspace to use.
/// </summary>
private JpegColorType? colorType;
/// <summary> /// <summary>
/// The output stream. All attempted writes after the first error become no-ops. /// The output stream. All attempted writes after the first error become no-ops.
@ -54,14 +45,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
public JpegEncoderCore(IJpegEncoderOptions options) public JpegEncoderCore(IJpegEncoderOptions options)
{ => this.options = options;
this.quality = options.Quality;
if (IsSupportedColorType(options.ColorType)) public Block8x8F[] QuantizationTables { get; } = new Block8x8F[4];
{
this.colorType = options.ColorType;
}
}
/// <summary> /// <summary>
/// Encode writes the image to the jpeg baseline format with the given options. /// Encode writes the image to the jpeg baseline format with the given options.
@ -84,72 +70,45 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
this.outputStream = stream; this.outputStream = stream;
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); JpegMetadata jpegMetadata = metadata.GetJpegMetadata();
JpegFrameConfig frameConfig = this.GetFrameConfig(jpegMetadata);
// If the color type was not specified by the user, preserve the color type of the input image. bool interleaved = this.options.Interleaved ?? jpegMetadata.Interleaved ?? true;
if (!this.colorType.HasValue) using var frame = new JpegFrame(image, frameConfig, interleaved);
{
this.colorType = GetFallbackColorType(image);
}
// Compute number of components based on color type in options.
int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3;
ReadOnlySpan<byte> componentIds = this.GetComponentIds();
// TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that
// Initialize the quantization tables.
this.InitQuantizationTables(componentCount, jpegMetadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable);
// Write the Start Of Image marker. // Write the Start Of Image marker.
this.WriteStartOfImage(); this.WriteStartOfImage();
// Do not write APP0 marker for RGB colorspace. // Write APP0 marker
if (this.colorType != JpegColorType.Rgb) if (frameConfig.AdobeColorTransformMarkerFlag is null)
{ {
this.WriteJfifApplicationHeader(metadata); this.WriteJfifApplicationHeader(metadata);
} }
// Write Exif, XMP, ICC and IPTC profiles // Write APP14 marker with adobe color extension
this.WriteProfiles(metadata); else
if (this.colorType == JpegColorType.Rgb)
{ {
// Write App14 marker to indicate RGB color space. this.WriteApp14Marker(frameConfig.AdobeColorTransformMarkerFlag.Value);
this.WriteApp14Marker();
} }
// Write the quantization tables. // Write Exif, XMP, ICC and IPTC profiles
this.WriteDefineQuantizationTables(ref luminanceQuantTable, ref chrominanceQuantTable); this.WriteProfiles(metadata);
// Write the image dimensions. // Write the image dimensions.
this.WriteStartOfFrame(image.Width, image.Height, componentCount, componentIds); this.WriteStartOfFrame(image.Width, image.Height, frameConfig);
// Write the Huffman tables. // Write the Huffman tables.
this.WriteDefineHuffmanTables(componentCount); var scanEncoder = new HuffmanScanEncoder(frame.BlocksPerMcu, stream);
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder);
// Write the scan header.
this.WriteStartOfScan(componentCount, componentIds); // Write the quantization tables.
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.options.Quality, jpegMetadata);
// Write the scan compressed data.
switch (this.colorType) // Write scans with actual pixel data
{ using var spectralConverter = new SpectralConverter<TPixel>(frame, image, this.QuantizationTables);
case JpegColorType.YCbCrRatio444: this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, cancellationToken);
new HuffmanScanEncoder(3, stream).Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break;
case JpegColorType.YCbCrRatio420:
new HuffmanScanEncoder(6, stream).Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break;
case JpegColorType.Luminance:
new HuffmanScanEncoder(1, stream).EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken);
break;
case JpegColorType.Rgb:
new HuffmanScanEncoder(3, stream).EncodeRgb(image, ref luminanceQuantTable, cancellationToken);
break;
default:
// all other non-supported color types are checked at the start of this method
break;
}
// Write the End Of Image marker. // Write the End Of Image marker.
this.WriteEndOfImageMarker(); this.WriteEndOfImageMarker();
@ -157,75 +116,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
stream.Flush(); stream.Flush();
} }
/// <summary>
/// If color type was not set, set it based on the given image.
/// Note, if there is no metadata and the image has multiple components this method
/// returns <see langword="null"/> defering the field assignment
/// to <see cref="InitQuantizationTables(int, JpegMetadata, out Block8x8F, out Block8x8F)"/>.
/// </summary>
private static JpegColorType? GetFallbackColorType<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
// First inspect the image metadata.
JpegColorType? colorType = null;
JpegMetadata metadata = image.Metadata.GetJpegMetadata();
if (IsSupportedColorType(metadata.ColorType))
{
return metadata.ColorType;
}
// Secondly, inspect the pixel type.
// TODO: PixelTypeInfo should contain a component count!
bool isGrayscale =
typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) ||
typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32);
// We don't set multi-component color types here since we can set it based upon
// the quality in InitQuantizationTables.
if (isGrayscale)
{
colorType = JpegColorType.Luminance;
}
return colorType;
}
/// <summary>
/// Returns true, if the color type is supported by the encoder.
/// </summary>
/// <param name="colorType">The color type.</param>
/// <returns>true, if color type is supported.</returns>
private static bool IsSupportedColorType(JpegColorType? colorType)
=> colorType == JpegColorType.YCbCrRatio444
|| colorType == JpegColorType.YCbCrRatio420
|| colorType == JpegColorType.Luminance
|| colorType == JpegColorType.Rgb;
/// <summary>
/// Gets the component ids.
/// For color space RGB this will be RGB as ASCII, otherwise 1, 2, 3.
/// </summary>
/// <returns>The component Ids.</returns>
private ReadOnlySpan<byte> GetComponentIds() => this.colorType == JpegColorType.Rgb
? new ReadOnlySpan<byte>(new byte[] { 82, 71, 66 })
: new ReadOnlySpan<byte>(new byte[] { 1, 2, 3 });
/// <summary>
/// Writes data to "Define Quantization Tables" block for QuantIndex.
/// </summary>
/// <param name="dqt">The "Define Quantization Tables" block.</param>
/// <param name="offset">Offset in "Define Quantization Tables" block.</param>
/// <param name="i">The quantization index.</param>
/// <param name="quant">The quantization table to copy data from.</param>
private static void WriteDataToDqt(byte[] dqt, ref int offset, QuantIndex i, ref Block8x8F quant)
{
dqt[offset++] = (byte)i;
for (int j = 0; j < Block8x8F.Size; j++)
{
dqt[offset++] = (byte)quant[ZigZag.ZigZagOrder[j]];
}
}
/// <summary> /// <summary>
/// Write the start of image marker. /// Write the start of image marker.
/// </summary> /// </summary>
@ -286,71 +176,40 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Writes the Define Huffman Table marker and tables. /// Writes the Define Huffman Table marker and tables.
/// </summary> /// </summary>
/// <param name="componentCount">The number of components to write.</param> private void WriteDefineHuffmanTables(JpegHuffmanTableConfig[] tableConfigs, HuffmanScanEncoder scanEncoder)
private void WriteDefineHuffmanTables(int componentCount)
{
// This uses a C#'s compiler optimization that refers to the static data segment of the assembly,
// and doesn't incur any allocation at all.
// Table identifiers.
ReadOnlySpan<byte> headers = new byte[]
{
0x00,
0x10,
0x01,
0x11
};
int markerlen = 2;
HuffmanSpec[] specs = HuffmanSpec.TheHuffmanSpecs;
if (componentCount == 1)
{ {
// Drop the Chrominance tables. if (tableConfigs is null)
specs = new[] { HuffmanSpec.TheHuffmanSpecs[0], HuffmanSpec.TheHuffmanSpecs[1] };
}
for (int i = 0; i < specs.Length; i++)
{ {
ref HuffmanSpec s = ref specs[i]; throw new ArgumentNullException(nameof(tableConfigs));
markerlen += 1 + 16 + s.Values.Length;
} }
this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen); int markerlen = 2;
for (int i = 0; i < specs.Length; i++)
for (int i = 0; i < tableConfigs.Length; i++)
{ {
this.outputStream.WriteByte(headers[i]); markerlen += 1 + 16 + tableConfigs[i].Table.Values.Length;
this.outputStream.Write(specs[i].Count);
this.outputStream.Write(specs[i].Values);
}
} }
/// <summary> this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen);
/// Writes the Define Quantization Marker and tables. for (int i = 0; i < tableConfigs.Length; i++)
/// </summary>
private void WriteDefineQuantizationTables(ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable)
{ {
// Marker + quantization table lengths. JpegHuffmanTableConfig tableConfig = tableConfigs[i];
int markerlen = 2 + (QuantizationTableCount * (1 + Block8x8F.Size));
this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen);
// Loop through and collect the tables as one array. int header = (tableConfig.Class << 4) | tableConfig.DestinationIndex;
// This allows us to reduce the number of writes to the stream. this.outputStream.WriteByte((byte)header);
int dqtCount = (QuantizationTableCount * Block8x8F.Size) + QuantizationTableCount; this.outputStream.Write(tableConfig.Table.Count);
byte[] dqt = new byte[dqtCount]; this.outputStream.Write(tableConfig.Table.Values);
int offset = 0;
WriteDataToDqt(dqt, ref offset, QuantIndex.Luminance, ref luminanceQuantTable); scanEncoder.BuildHuffmanTable(tableConfig);
WriteDataToDqt(dqt, ref offset, QuantIndex.Chrominance, ref chrominanceQuantTable); }
this.outputStream.Write(dqt, 0, dqtCount);
} }
/// <summary> /// <summary>
/// Writes the APP14 marker to indicate the image is in RGB color space. /// Writes the APP14 marker to indicate the image is in RGB color space.
/// </summary> /// </summary>
private void WriteApp14Marker() private void WriteApp14Marker(byte colorTransform)
{ {
this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + AdobeMarker.Length); this.WriteMarkerHeader(JpegConstants.Markers.APP14, 2 + Components.Decoder.AdobeMarker.Length);
// Identifier: ASCII "Adobe". // Identifier: ASCII "Adobe".
this.buffer[0] = 0x41; this.buffer[0] = 0x41;
@ -368,8 +227,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Flags1 // Flags1
BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(9, 2), 0); BinaryPrimitives.WriteInt16BigEndian(this.buffer.AsSpan(9, 2), 0);
// Transform byte, 0 in combination with three components means the image is in RGB colorspace. // Color transform byte
this.buffer[11] = 0; this.buffer[11] = colorTransform;
this.outputStream.Write(this.buffer.AsSpan(0, 12)); this.outputStream.Write(this.buffer.AsSpan(0, 12));
} }
@ -385,8 +244,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return; return;
} }
const int MaxBytesApp1 = 65533; // 64k - 2 padding bytes const int maxBytesApp1 = 65533; // 64k - 2 padding bytes
const int MaxBytesWithExifId = 65527; // Max - 6 bytes for EXIF header. const int maxBytesWithExifId = 65527; // Max - 6 bytes for EXIF header.
byte[] data = exifProfile.ToByteArray(); byte[] data = exifProfile.ToByteArray();
@ -396,27 +255,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
// We can write up to a maximum of 64 data to the initial marker so calculate boundaries. // We can write up to a maximum of 64 data to the initial marker so calculate boundaries.
int exifMarkerLength = ProfileResolver.ExifMarker.Length; int exifMarkerLength = Components.Decoder.ProfileResolver.ExifMarker.Length;
int remaining = exifMarkerLength + data.Length; int remaining = exifMarkerLength + data.Length;
int bytesToWrite = remaining > MaxBytesApp1 ? MaxBytesApp1 : remaining; int bytesToWrite = remaining > maxBytesApp1 ? maxBytesApp1 : remaining;
int app1Length = bytesToWrite + 2; int app1Length = bytesToWrite + 2;
// Write the app marker, EXIF marker, and data // Write the app marker, EXIF marker, and data
this.WriteApp1Header(app1Length); this.WriteApp1Header(app1Length);
this.outputStream.Write(ProfileResolver.ExifMarker); this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker);
this.outputStream.Write(data, 0, bytesToWrite - exifMarkerLength); this.outputStream.Write(data, 0, bytesToWrite - exifMarkerLength);
remaining -= bytesToWrite; remaining -= bytesToWrite;
// If the exif data exceeds 64K, write it in multiple APP1 Markers // If the exif data exceeds 64K, write it in multiple APP1 Markers
for (int idx = MaxBytesWithExifId; idx < data.Length; idx += MaxBytesWithExifId) for (int idx = maxBytesWithExifId; idx < data.Length; idx += maxBytesWithExifId)
{ {
bytesToWrite = remaining > MaxBytesWithExifId ? MaxBytesWithExifId : remaining; bytesToWrite = remaining > maxBytesWithExifId ? maxBytesWithExifId : remaining;
app1Length = bytesToWrite + 2 + exifMarkerLength; app1Length = bytesToWrite + 2 + exifMarkerLength;
this.WriteApp1Header(app1Length); this.WriteApp1Header(app1Length);
// Write Exif00 marker // Write Exif00 marker
this.outputStream.Write(ProfileResolver.ExifMarker); this.outputStream.Write(Components.Decoder.ProfileResolver.ExifMarker);
// Write the exif data // Write the exif data
this.outputStream.Write(data, idx, bytesToWrite); this.outputStream.Write(data, idx, bytesToWrite);
@ -434,7 +293,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </exception> /// </exception>
private void WriteIptcProfile(IptcProfile iptcProfile) private void WriteIptcProfile(IptcProfile iptcProfile)
{ {
const int Max = 65533; const int maxBytes = 65533;
if (iptcProfile is null || !iptcProfile.Values.Any()) if (iptcProfile is null || !iptcProfile.Values.Any())
{ {
return; return;
@ -447,19 +306,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return; return;
} }
if (data.Length > Max) if (data.Length > maxBytes)
{ {
throw new ImageFormatException($"Iptc profile size exceeds limit of {Max} bytes"); throw new ImageFormatException($"Iptc profile size exceeds limit of {maxBytes} bytes");
} }
int app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length + int app13Length = 2 + Components.Decoder.ProfileResolver.AdobePhotoshopApp13Marker.Length +
ProfileResolver.AdobeImageResourceBlockMarker.Length + Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker.Length +
ProfileResolver.AdobeIptcMarker.Length + Components.Decoder.ProfileResolver.AdobeIptcMarker.Length +
2 + 4 + data.Length; 2 + 4 + data.Length;
this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13); this.WriteAppHeader(app13Length, JpegConstants.Markers.APP13);
this.outputStream.Write(ProfileResolver.AdobePhotoshopApp13Marker); this.outputStream.Write(Components.Decoder.ProfileResolver.AdobePhotoshopApp13Marker);
this.outputStream.Write(ProfileResolver.AdobeImageResourceBlockMarker); this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeImageResourceBlockMarker);
this.outputStream.Write(ProfileResolver.AdobeIptcMarker); this.outputStream.Write(Components.Decoder.ProfileResolver.AdobeIptcMarker);
this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even) this.outputStream.WriteByte(0); // a empty pascal string (padded to make size even)
this.outputStream.WriteByte(0); this.outputStream.WriteByte(0);
BinaryPrimitives.WriteInt32BigEndian(this.buffer, data.Length); BinaryPrimitives.WriteInt32BigEndian(this.buffer, data.Length);
@ -481,9 +340,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return; return;
} }
const int XmpOverheadLength = 29; const int xmpOverheadLength = 29;
const int Max = 65533; const int maxBytes = 65533;
const int MaxData = Max - XmpOverheadLength; const int maxData = maxBytes - xmpOverheadLength;
byte[] data = xmpProfile.Data; byte[] data = xmpProfile.Data;
@ -499,16 +358,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{ {
int length = dataLength; // Number of bytes to write. int length = dataLength; // Number of bytes to write.
if (length > MaxData) if (length > maxData)
{ {
length = MaxData; length = maxData;
} }
dataLength -= length; dataLength -= length;
int app1Length = 2 + ProfileResolver.XmpMarker.Length + length; int app1Length = 2 + Components.Decoder.ProfileResolver.XmpMarker.Length + length;
this.WriteApp1Header(app1Length); this.WriteApp1Header(app1Length);
this.outputStream.Write(ProfileResolver.XmpMarker); this.outputStream.Write(Components.Decoder.ProfileResolver.XmpMarker);
this.outputStream.Write(data, offset, length); this.outputStream.Write(data, offset, length);
offset += length; offset += length;
@ -551,9 +410,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return; return;
} }
const int IccOverheadLength = 14; const int iccOverheadLength = 14;
const int Max = 65533; const int maxBytes = 65533;
const int MaxData = Max - IccOverheadLength; const int maxData = maxBytes - iccOverheadLength;
byte[] data = iccProfile.ToByteArray(); byte[] data = iccProfile.ToByteArray();
@ -564,9 +423,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Calculate the number of markers we'll need, rounding up of course. // Calculate the number of markers we'll need, rounding up of course.
int dataLength = data.Length; int dataLength = data.Length;
int count = dataLength / MaxData; int count = dataLength / maxData;
if (count * MaxData != dataLength) if (count * maxData != dataLength)
{ {
count++; count++;
} }
@ -579,9 +438,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{ {
int length = dataLength; // Number of bytes to write. int length = dataLength; // Number of bytes to write.
if (length > MaxData) if (length > maxData)
{ {
length = MaxData; length = maxData;
} }
dataLength -= length; dataLength -= length;
@ -609,7 +468,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.buffer[12] = (byte)current; // The position within the collection. this.buffer[12] = (byte)current; // The position within the collection.
this.buffer[13] = (byte)count; // The total number of profiles. this.buffer[13] = (byte)count; // The total number of profiles.
this.outputStream.Write(this.buffer, 0, IccOverheadLength); this.outputStream.Write(this.buffer, 0, iccOverheadLength);
this.outputStream.Write(data, offset, length); this.outputStream.Write(data, offset, length);
current++; current++;
@ -643,124 +502,46 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Writes the Start Of Frame (Baseline) marker. /// Writes the Start Of Frame (Baseline) marker.
/// </summary> /// </summary>
/// <param name="width">The width of the image.</param> private void WriteStartOfFrame(int width, int height, JpegFrameConfig frame)
/// <param name="height">The height of the image.</param>
/// <param name="componentCount">The number of components in a pixel.</param>
/// <param name="componentIds">The component Id's.</param>
private void WriteStartOfFrame(int width, int height, int componentCount, ReadOnlySpan<byte> componentIds)
{
// This uses a C#'s compiler optimization that refers to the static data segment of the assembly,
// and doesn't incur any allocation at all.
// "default" to 4:2:0
ReadOnlySpan<byte> subsamples = new byte[]
{
0x22,
0x11,
0x11
};
ReadOnlySpan<byte> chroma = new byte[]
{
0x00,
0x01,
0x01
};
if (this.colorType == JpegColorType.Luminance)
{
subsamples = new byte[]
{
0x11,
0x00,
0x00
};
}
else
{
switch (this.colorType)
{
case JpegColorType.YCbCrRatio444:
case JpegColorType.Rgb:
subsamples = new byte[]
{ {
0x11, JpegComponentConfig[] components = frame.Components;
0x11,
0x11
};
if (this.colorType == JpegColorType.Rgb)
{
chroma = new byte[]
{
0x00,
0x00,
0x00
};
}
break;
case JpegColorType.YCbCrRatio420:
subsamples = new byte[]
{
0x22,
0x11,
0x11
};
break;
}
}
// Length (high byte, low byte), 8 + components * 3. // Length (high byte, low byte), 8 + components * 3.
int markerlen = 8 + (3 * componentCount); int markerlen = 8 + (3 * components.Length);
this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen); this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen);
this.buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported this.buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
this.buffer[1] = (byte)(height >> 8); this.buffer[1] = (byte)(height >> 8);
this.buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported this.buffer[2] = (byte)(height & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
this.buffer[3] = (byte)(width >> 8); this.buffer[3] = (byte)(width >> 8);
this.buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported this.buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
this.buffer[5] = (byte)componentCount; this.buffer[5] = (byte)components.Length;
for (int i = 0; i < componentCount; i++) // Components data
for (int i = 0; i < components.Length; i++)
{ {
int i3 = 3 * i; int i3 = 3 * i;
// Component ID.
Span<byte> bufferSpan = this.buffer.AsSpan(i3 + 6, 3); Span<byte> bufferSpan = this.buffer.AsSpan(i3 + 6, 3);
bufferSpan[2] = chroma[i];
bufferSpan[1] = subsamples[i]; // Quantization table selector
bufferSpan[0] = componentIds[i]; bufferSpan[2] = (byte)components[i].QuantizatioTableIndex;
// Sampling factors
// 4 bits
int samplingFactors = (components[i].HorizontalSampleFactor << 4) | components[i].VerticalSampleFactor;
bufferSpan[1] = (byte)samplingFactors;
// Id
bufferSpan[0] = components[i].Id;
} }
this.outputStream.Write(this.buffer, 0, (3 * (componentCount - 1)) + 9); this.outputStream.Write(this.buffer, 0, (3 * (components.Length - 1)) + 9);
} }
/// <summary> /// <summary>
/// Writes the StartOfScan marker. /// Writes the StartOfScan marker.
/// </summary> /// </summary>
/// <param name="componentCount">The number of components in a pixel.</param> private void WriteStartOfScan(Span<JpegComponentConfig> components)
/// <param name="componentIds">The componentId's.</param>
private void WriteStartOfScan(int componentCount, ReadOnlySpan<byte> componentIds)
{
// This uses a C#'s compiler optimization that refers to the static data segment of the assembly,
// and doesn't incur any allocation at all.
ReadOnlySpan<byte> huffmanId = new byte[]
{
0x00,
0x11,
0x11
};
// Use the same DC/AC tables for all channels for RGB.
if (this.colorType == JpegColorType.Rgb)
{
huffmanId = new byte[]
{ {
0x00,
0x00,
0x00
};
}
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c", // - the marker length "\x00\x0c",
// - the number of components "\x03", // - the number of components "\x03",
@ -774,15 +555,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.buffer[1] = JpegConstants.Markers.SOS; this.buffer[1] = JpegConstants.Markers.SOS;
// Length (high byte, low byte), must be 6 + 2 * (number of components in scan) // Length (high byte, low byte), must be 6 + 2 * (number of components in scan)
int sosSize = 6 + (2 * componentCount); int sosSize = 6 + (2 * components.Length);
this.buffer[2] = 0x00; this.buffer[2] = 0x00;
this.buffer[3] = (byte)sosSize; this.buffer[3] = (byte)sosSize;
this.buffer[4] = (byte)componentCount; // Number of components in a scan this.buffer[4] = (byte)components.Length; // Number of components in a scan
for (int i = 0; i < componentCount; i++)
// Components data
for (int i = 0; i < components.Length; i++)
{ {
int i2 = 2 * i; int i2 = 2 * i;
this.buffer[i2 + 5] = componentIds[i]; // Component Id
this.buffer[i2 + 6] = huffmanId[i]; // DC/AC Huffman table // Id
this.buffer[i2 + 5] = components[i].Id;
// Table selectors
int tableSelectors = (components[i].DcTableSelector << 4) | components[i].AcTableSelector;
this.buffer[i2 + 6] = (byte)tableSelectors;
} }
this.buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection. this.buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
@ -801,6 +589,40 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream.Write(this.buffer, 0, 2); this.outputStream.Write(this.buffer, 0, 2);
} }
/// <summary>
/// Writes scans for given config.
/// </summary>
private void WriteHuffmanScans<TPixel>(JpegFrame frame, JpegFrameConfig frameConfig, SpectralConverter<TPixel> spectralConverter, HuffmanScanEncoder encoder, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (frame.Components.Length == 1)
{
frame.AllocateComponents(fullScan: false);
this.WriteStartOfScan(frameConfig.Components);
encoder.EncodeScanBaselineSingleComponent(frame.Components[0], spectralConverter, cancellationToken);
}
else if (frame.Interleaved)
{
frame.AllocateComponents(fullScan: false);
this.WriteStartOfScan(frameConfig.Components);
encoder.EncodeScanBaselineInterleaved(frameConfig.EncodingColor, frame, spectralConverter, cancellationToken);
}
else
{
frame.AllocateComponents(fullScan: true);
spectralConverter.ConvertFull();
Span<JpegComponentConfig> components = frameConfig.Components;
for (int i = 0; i < frame.Components.Length; i++)
{
this.WriteStartOfScan(components.Slice(i, 1));
encoder.EncodeScanBaseline(frame.Components[i], cancellationToken);
}
}
}
/// <summary> /// <summary>
/// Writes the header for a marker with the given length. /// Writes the header for a marker with the given length.
/// </summary> /// </summary>
@ -817,54 +639,78 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
/// <summary> /// <summary>
/// Initializes quantization tables. /// Writes the Define Quantization Marker and prepares tables for encoding.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>
/// Zig-zag ordering is NOT applied to the resulting tables.
/// </para>
/// <para>
/// We take quality values in a hierarchical order: /// We take quality values in a hierarchical order:
/// 1. Check if encoder has set quality /// <list type = "number" >
/// 2. Check if metadata has set quality /// <item>Check if encoder has set quality.</item>
/// 3. Take default quality value - 75 /// <item>Check if metadata has set quality.</item>
/// </para> /// <item>Take default quality value from <see cref="Quantization.DefaultQualityFactor"/></item>
/// </list>
/// </remarks> /// </remarks>
/// <param name="componentCount">Color components count.</param> /// <param name="configs">Quantization tables configs.</param>
/// <param name="optionsQuality">Optional quality value from the options.</param>
/// <param name="metadata">Jpeg metadata instance.</param> /// <param name="metadata">Jpeg metadata instance.</param>
/// <param name="luminanceQuantTable">Output luminance quantization table.</param> private void WriteDefineQuantizationTables(JpegQuantizationTableConfig[] configs, int? optionsQuality, JpegMetadata metadata)
/// <param name="chrominanceQuantTable">Output chrominance quantization table.</param> {
private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) int dataLen = configs.Length * (1 + Block8x8.Size);
// Marker + quantization table lengths.
int markerlen = 2 + dataLen;
this.WriteMarkerHeader(JpegConstants.Markers.DQT, markerlen);
byte[] buffer = new byte[dataLen];
int offset = 0;
Block8x8F workspaceBlock = default;
for (int i = 0; i < configs.Length; i++)
{ {
int lumaQuality; JpegQuantizationTableConfig config = configs[i];
int chromaQuality;
if (this.quality.HasValue) int quality = GetQualityForTable(config.DestinationIndex, optionsQuality, metadata);
Block8x8 scaledTable = Quantization.ScaleQuantizationTable(quality, config.Table);
// write to the output stream
buffer[offset++] = (byte)config.DestinationIndex;
for (int j = 0; j < Block8x8.Size; j++)
{ {
lumaQuality = this.quality.Value; buffer[offset++] = (byte)(uint)scaledTable[ZigZag.ZigZagOrder[j]];
chromaQuality = this.quality.Value;
} }
else
{ // apply FDCT multipliers and inject to the destination index
lumaQuality = metadata.LuminanceQuality; workspaceBlock.LoadFrom(ref scaledTable);
chromaQuality = metadata.ChrominanceQuality; FloatingPointDCT.AdjustToFDCT(ref workspaceBlock);
this.QuantizationTables[config.DestinationIndex] = workspaceBlock;
} }
// Luminance // write filled buffer to the stream
lumaQuality = Numerics.Clamp(lumaQuality, 1, 100); this.outputStream.Write(buffer);
luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality);
// Chrominance static int GetQualityForTable(int destIndex, int? encoderQuality, JpegMetadata metadata) => destIndex switch
chrominanceQuantTable = default;
if (componentCount > 1)
{ {
chromaQuality = Numerics.Clamp(chromaQuality, 1, 100); 0 => encoderQuality ?? metadata.LuminanceQuality ?? Quantization.DefaultQualityFactor,
chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); 1 => encoderQuality ?? metadata.ChrominanceQuality ?? Quantization.DefaultQualityFactor,
_ => encoderQuality ?? metadata.Quality,
};
}
if (!this.colorType.HasValue) private JpegFrameConfig GetFrameConfig(JpegMetadata metadata)
{ {
this.colorType = chromaQuality >= 91 ? JpegColorType.YCbCrRatio444 : JpegColorType.YCbCrRatio420; JpegEncodingColor color = this.options.ColorType ?? metadata.ColorType ?? JpegEncodingColor.YCbCrRatio420;
} JpegFrameConfig frameConfig = Array.Find(
FrameConfigs,
cfg => cfg.EncodingColor == color);
if (frameConfig == null)
{
throw new ArgumentException(nameof(color));
} }
return frameConfig;
} }
} }
} }

15
src/ImageSharp/Formats/Jpeg/JpegColorType.cs → src/ImageSharp/Formats/Jpeg/JpegEncodingColor.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Provides enumeration of available JPEG color types. /// Provides enumeration of available JPEG color types.
/// </summary> /// </summary>
public enum JpegColorType : byte public enum JpegEncodingColor : byte
{ {
/// <summary> /// <summary>
/// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
@ -25,24 +25,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
/// The two chroma components are sampled at half the horizontal sample rate of luma while vertically it has full resolution. /// The two chroma components are sampled at half the horizontal sample rate of luma while vertically it has full resolution.
///
/// Note: Not supported by the encoder.
/// </summary> /// </summary>
YCbCrRatio422 = 2, YCbCrRatio422 = 2,
/// <summary> /// <summary>
/// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
/// In 4:1:1 chroma subsampling, the horizontal color resolution is quartered. /// In 4:1:1 chroma subsampling, the horizontal color resolution is quartered.
///
/// Note: Not supported by the encoder.
/// </summary> /// </summary>
YCbCrRatio411 = 3, YCbCrRatio411 = 3,
/// <summary> /// <summary>
/// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification. /// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
/// This ratio uses half of the vertical and one-fourth the horizontal color resolutions. /// This ratio uses half of the vertical and one-fourth the horizontal color resolutions.
///
/// Note: Not supported by the encoder.
/// </summary> /// </summary>
YCbCrRatio410 = 4, YCbCrRatio410 = 4,
@ -58,9 +52,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// CMYK colorspace (cyan, magenta, yellow, and key black) intended for printing. /// CMYK colorspace (cyan, magenta, yellow, and key black) intended for printing.
///
/// Note: Not supported by the encoder.
/// </summary> /// </summary>
Cmyk = 7, Cmyk = 7,
/// <summary>
/// YCCK colorspace (Y, Cb, Cr, and key black).
/// </summary>
Ycck = 8,
} }
} }

80
src/ImageSharp/Formats/Jpeg/JpegMetadata.cs

@ -11,16 +11,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
public class JpegMetadata : IDeepCloneable public class JpegMetadata : IDeepCloneable
{ {
/// <summary>
/// Backing field for <see cref="LuminanceQuality"/>
/// </summary>
private int? luminanceQuality;
/// <summary>
/// Backing field for <see cref="ChrominanceQuality"/>
/// </summary>
private int? chrominanceQuality;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JpegMetadata"/> class. /// Initializes a new instance of the <see cref="JpegMetadata"/> class.
/// </summary> /// </summary>
@ -36,8 +26,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{ {
this.ColorType = other.ColorType; this.ColorType = other.ColorType;
this.luminanceQuality = other.luminanceQuality; this.LuminanceQuality = other.LuminanceQuality;
this.chrominanceQuality = other.chrominanceQuality; this.ChrominanceQuality = other.ChrominanceQuality;
} }
/// <summary> /// <summary>
@ -47,11 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// This value might not be accurate if it was calculated during jpeg decoding /// This value might not be accurate if it was calculated during jpeg decoding
/// with non-complient ITU quantization tables. /// with non-complient ITU quantization tables.
/// </remarks> /// </remarks>
internal int LuminanceQuality internal int? LuminanceQuality { get; set; }
{
get => this.luminanceQuality ?? Quantization.DefaultQualityFactor;
set => this.luminanceQuality = value;
}
/// <summary> /// <summary>
/// Gets or sets the jpeg chrominance quality. /// Gets or sets the jpeg chrominance quality.
@ -60,55 +46,61 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// This value might not be accurate if it was calculated during jpeg decoding /// This value might not be accurate if it was calculated during jpeg decoding
/// with non-complient ITU quantization tables. /// with non-complient ITU quantization tables.
/// </remarks> /// </remarks>
internal int ChrominanceQuality internal int? ChrominanceQuality { get; set; }
{
get => this.chrominanceQuality ?? Quantization.DefaultQualityFactor;
set => this.chrominanceQuality = value;
}
/// <summary> /// <summary>
/// Gets or sets the encoded quality. /// Gets the encoded quality.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Note that jpeg image can have different quality for luminance and chrominance components. /// Note that jpeg image can have different quality for luminance and chrominance components.
/// This property returns maximum value of luma/chroma qualities. /// This property returns maximum value of luma/chroma qualities if both are present.
/// </remarks> /// </remarks>
public int Quality public int Quality
{ {
get get
{ {
// Jpeg always has a luminance table thus it must have a luminance quality derived from it if (this.LuminanceQuality.HasValue)
if (!this.luminanceQuality.HasValue)
{ {
return Quantization.DefaultQualityFactor; if (this.ChrominanceQuality.HasValue)
{
return Math.Max(this.LuminanceQuality.Value, this.ChrominanceQuality.Value);
} }
int lumaQuality = this.luminanceQuality.Value; return this.LuminanceQuality.Value;
}
// Jpeg might not have a chrominance table - return luminance quality (grayscale images) else
if (!this.chrominanceQuality.HasValue) {
if (this.ChrominanceQuality.HasValue)
{ {
return lumaQuality; return this.ChrominanceQuality.Value;
} }
int chromaQuality = this.chrominanceQuality.Value; return Quantization.DefaultQualityFactor;
// Theoretically, luma quality would always be greater or equal to chroma quality
// But we've already encountered images which can have higher quality of chroma components
return Math.Max(lumaQuality, chromaQuality);
} }
set
{
this.LuminanceQuality = value;
this.ChrominanceQuality = value;
} }
} }
/// <summary> /// <summary>
/// Gets or sets the color type. /// Gets the color type.
/// </summary>
public JpegEncodingColor? ColorType { get; internal set; }
/// <summary>
/// Gets the component encoding mode.
/// </summary> /// </summary>
public JpegColorType? ColorType { get; set; } /// <remarks>
/// Interleaved encoding mode encodes all color components in a single scan.
/// Non-interleaved encoding mode encodes each color component in a separate scan.
/// </remarks>
public bool? Interleaved { get; internal set; }
/// <summary>
/// Gets the scan encoding mode.
/// </summary>
/// <remarks>
/// Progressive jpeg images encode component data across multiple scans.
/// </remarks>
public bool? Progressive { get; internal set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new JpegMetadata(this); public IDeepCloneable DeepClone() => new JpegMetadata(this);

2
src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffJpegCompressor.cs

@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
var image = Image.LoadPixelData<Rgb24>(rows, width, height); var image = Image.LoadPixelData<Rgb24>(rows, width, height);
image.Save(memoryStream, new JpegEncoder() image.Save(memoryStream, new JpegEncoder()
{ {
ColorType = JpegColorType.Rgb ColorType = JpegEncodingColor.Rgb
}); });
memoryStream.Position = 0; memoryStream.Position = 0;
memoryStream.WriteTo(this.Output); memoryStream.WriteTo(this.Output);

2
src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs

@ -1,8 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors

31
src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Spectral converter for YCbCr TIFF's which use the JPEG compression.
/// The jpeg data should be always treated as RGB color space.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
internal sealed class RgbJpegSpectralConverter<TPixel> : SpectralConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="RgbJpegSpectralConverter{TPixel}"/> class.
/// This Spectral converter will always convert the pixel data to RGB color.
/// </summary>
/// <param name="configuration">The configuration.</param>
public RgbJpegSpectralConverter(Configuration configuration)
: base(configuration)
{
}
/// <inheritdoc/>
protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, frame.Precision);
}
}

2
src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs

@ -1,8 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;

7
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -417,6 +417,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff
return; return;
case TiffPhotometricInterpretation.Rgb: case TiffPhotometricInterpretation.Rgb:
// Make sure 1 Bit compression is only used with 1 bit pixel type.
if (IsOneBitCompression(this.CompressionType))
{
// Invalid compression / bits per pixel combination, fallback to no compression.
compression = DefaultCompression;
}
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, photometricInterpretation, compression, predictor); this.SetEncoderOptions(TiffBitsPerPixel.Bit24, photometricInterpretation, compression, predictor);
return; return;
} }

8
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/Rgb24.PixelOperations.cs

@ -36,6 +36,14 @@ namespace SixLabors.ImageSharp.PixelFormats
SimdUtils.PackFromRgbPlanes(configuration, redChannel, greenChannel, blueChannel, destination); SimdUtils.PackFromRgbPlanes(configuration, redChannel, greenChannel, blueChannel, destination);
} }
/// <inheritdoc />
internal override void UnpackIntoRgbPlanes(
Span<float> redChannel,
Span<float> greenChannel,
Span<float> blueChannel,
ReadOnlySpan<Rgb24> source)
=> SimdUtils.UnpackToRgbPlanes(redChannel, greenChannel, blueChannel, source);
} }
} }
} }

31
src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs

@ -198,6 +198,37 @@ namespace SixLabors.ImageSharp.PixelFormats
} }
} }
/// <summary>
/// Bulk operation that unpacks pixels from <paramref name="source"/>
/// into 3 seperate RGB channels. The destination must have a padding of 3.
/// </summary>
/// <param name="redChannel">A <see cref="ReadOnlySpan{T}"/> to the red values.</param>
/// <param name="greenChannel">A <see cref="ReadOnlySpan{T}"/> to the green values.</param>
/// <param name="blueChannel">A <see cref="ReadOnlySpan{T}"/> to the blue values.</param>
/// <param name="source">A <see cref="Span{T}"/> to the destination pixels.</param>
internal virtual void UnpackIntoRgbPlanes(
Span<float> redChannel,
Span<float> greenChannel,
Span<float> blueChannel,
ReadOnlySpan<TPixel> source)
{
int count = redChannel.Length;
Rgba32 rgba32 = default;
ref float r = ref MemoryMarshal.GetReference(redChannel);
ref float g = ref MemoryMarshal.GetReference(greenChannel);
ref float b = ref MemoryMarshal.GetReference(blueChannel);
ref TPixel src = ref MemoryMarshal.GetReference(source);
for (int i = 0; i < count; i++)
{
Unsafe.Add(ref src, i).ToRgba32(ref rgba32);
Unsafe.Add(ref r, i) = rgba32.R;
Unsafe.Add(ref g, i) = rgba32.G;
Unsafe.Add(ref b, i) = rgba32.B;
}
}
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
internal static void GuardPackFromRgbPlanes(ReadOnlySpan<byte> greenChannel, ReadOnlySpan<byte> blueChannel, Span<TPixel> destination, int count) internal static void GuardPackFromRgbPlanes(ReadOnlySpan<byte> greenChannel, ReadOnlySpan<byte> blueChannel, Span<TPixel> destination, int count)
{ {

8
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.cs

@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; using SixLabors.ImageSharp.Formats.Jpeg.Components;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromCmykScalar(8).ConvertToRgbInplace(values); new JpegColorConverterBase.CmykScalar(8).ConvertToRgbInplace(values);
} }
[Benchmark] [Benchmark]
@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromCmykVector(8).ConvertToRgbInplace(values); new JpegColorConverterBase.CmykVector(8).ConvertToRgbInplace(values);
} }
#if SUPPORTS_RUNTIME_INTRINSICS #if SUPPORTS_RUNTIME_INTRINSICS
@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromCmykAvx(8).ConvertToRgbInplace(values); new JpegColorConverterBase.CmykAvx(8).ConvertToRgbInplace(values);
} }
#endif #endif
} }

6
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/GrayscaleColorConversion.cs

@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; using SixLabors.ImageSharp.Formats.Jpeg.Components;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromGrayscaleScalar(8).ConvertToRgbInplace(values); new JpegColorConverterBase.GrayscaleScalar(8).ConvertToRgbInplace(values);
} }
#if SUPPORTS_RUNTIME_INTRINSICS #if SUPPORTS_RUNTIME_INTRINSICS
@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromGrayscaleAvx(8).ConvertToRgbInplace(values); new JpegColorConverterBase.GrayscaleAvx(8).ConvertToRgbInplace(values);
} }
#endif #endif
} }

8
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/RgbColorConversion.cs

@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; using SixLabors.ImageSharp.Formats.Jpeg.Components;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromRgbScalar(8).ConvertToRgbInplace(values); new JpegColorConverterBase.RgbScalar(8).ConvertToRgbInplace(values);
} }
[Benchmark] [Benchmark]
@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromRgbVector(8).ConvertToRgbInplace(values); new JpegColorConverterBase.RgbVector(8).ConvertToRgbInplace(values);
} }
#if SUPPORTS_RUNTIME_INTRINSICS #if SUPPORTS_RUNTIME_INTRINSICS
@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromRgbAvx(8).ConvertToRgbInplace(values); new JpegColorConverterBase.RgbAvx(8).ConvertToRgbInplace(values);
} }
#endif #endif
} }

8
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrColorConversion.cs

@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; using SixLabors.ImageSharp.Formats.Jpeg.Components;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromYCbCrScalar(8).ConvertToRgbInplace(values); new JpegColorConverterBase.YCbCrScalar(8).ConvertToRgbInplace(values);
} }
[Benchmark] [Benchmark]
@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromYCbCrVector(8).ConvertToRgbInplace(values); new JpegColorConverterBase.YCbCrVector(8).ConvertToRgbInplace(values);
} }
#if SUPPORTS_RUNTIME_INTRINSICS #if SUPPORTS_RUNTIME_INTRINSICS
@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromYCbCrAvx(8).ConvertToRgbInplace(values); new JpegColorConverterBase.YCbCrAvx(8).ConvertToRgbInplace(values);
} }
#endif #endif
} }

56
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrForwardConverterBenchmark.cs

@ -1,56 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Benchmarks.Format.Jpeg.Components.Encoder
{
public class YCbCrForwardConverterBenchmark
{
private RgbToYCbCrConverterLut converter;
private Rgb24[] data;
[GlobalSetup]
public void Setup()
{
this.converter = RgbToYCbCrConverterLut.Create();
var r = new Random(42);
this.data = new Rgb24[64];
var d = new byte[3];
for (int i = 0; i < this.data.Length; i++)
{
r.NextBytes(d);
this.data[i] = new Rgb24(d[0], d[1], d[2]);
}
}
[Benchmark(Baseline = true)]
public void ConvertLut()
{
Block8x8F y = default;
Block8x8F cb = default;
Block8x8F cr = default;
this.converter.Convert444(this.data.AsSpan(), ref y, ref cb, ref cr);
}
[Benchmark]
public void ConvertVectorized()
{
Block8x8F y = default;
Block8x8F cb = default;
Block8x8F cr = default;
if (RgbToYCbCrConverterVectorized.IsSupported)
{
RgbToYCbCrConverterVectorized.Convert444(this.data.AsSpan(), ref y, ref cb, ref cr);
}
}
}
}

8
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YccKColorConverter.cs

@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; using SixLabors.ImageSharp.Formats.Jpeg.Components;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromYccKScalar(8).ConvertToRgbInplace(values); new JpegColorConverterBase.YccKScalar(8).ConvertToRgbInplace(values);
} }
[Benchmark] [Benchmark]
@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromYccKVector(8).ConvertToRgbInplace(values); new JpegColorConverterBase.YccKVector(8).ConvertToRgbInplace(values);
} }
#if SUPPORTS_RUNTIME_INTRINSICS #if SUPPORTS_RUNTIME_INTRINSICS
@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
var values = new JpegColorConverterBase.ComponentValues(this.Input, 0); var values = new JpegColorConverterBase.ComponentValues(this.Input, 0);
new JpegColorConverterBase.FromYccKAvx(8).ConvertToRgbInplace(values); new JpegColorConverterBase.YccKAvx(8).ConvertToRgbInplace(values);
} }
#endif #endif
} }

2
tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegComparison.cs

@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
using FileStream imageBinaryStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage)); using FileStream imageBinaryStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage));
this.imageImageSharp = Image.Load<Rgba32>(imageBinaryStream); this.imageImageSharp = Image.Load<Rgba32>(imageBinaryStream);
this.encoderImageSharp = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio420 }; this.encoderImageSharp = new JpegEncoder { Quality = this.Quality, ColorType = JpegEncodingColor.YCbCrRatio420 };
this.destinationStream = new MemoryStream(); this.destinationStream = new MemoryStream();
} }

48
tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegFeatures.cs

@ -22,14 +22,19 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
// No metadata // No metadata
private const string TestImage = TestImages.Jpeg.Baseline.Calliphora; private const string TestImage = TestImages.Jpeg.Baseline.Calliphora;
public static IEnumerable<JpegColorType> ColorSpaceValues => public static IEnumerable<JpegEncodingColor> ColorSpaceValues => new[]
new[] { JpegColorType.Luminance, JpegColorType.Rgb, JpegColorType.YCbCrRatio420, JpegColorType.YCbCrRatio444 }; {
JpegEncodingColor.Luminance,
JpegEncodingColor.Rgb,
JpegEncodingColor.YCbCrRatio420,
JpegEncodingColor.YCbCrRatio444,
};
[Params(75, 90, 100)] [Params(75, 90, 100)]
public int Quality; public int Quality;
[ParamsSource(nameof(ColorSpaceValues), Priority = -100)] [ParamsSource(nameof(ColorSpaceValues), Priority = -100)]
public JpegColorType TargetColorSpace; public JpegEncodingColor TargetColorSpace;
private Image<Rgb24> bmpCore; private Image<Rgb24> bmpCore;
private JpegEncoder encoder; private JpegEncoder encoder;
@ -41,7 +46,12 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{ {
using FileStream imageBinaryStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage)); using FileStream imageBinaryStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage));
this.bmpCore = Image.Load<Rgb24>(imageBinaryStream); this.bmpCore = Image.Load<Rgb24>(imageBinaryStream);
this.encoder = new JpegEncoder { Quality = this.Quality, ColorType = this.TargetColorSpace }; this.encoder = new JpegEncoder
{
Quality = this.Quality,
ColorType = this.TargetColorSpace,
Interleaved = true,
};
this.destinationStream = new MemoryStream(); this.destinationStream = new MemoryStream();
} }
@ -67,23 +77,23 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
/* /*
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044 BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044
Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.100-preview.3.21202.5 .NET SDK=6.0.202
[Host] : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT [Host] : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
DefaultJob : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT DefaultJob : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
| Method | TargetColorSpace | Quality | Mean | Error | StdDev | | Method | TargetColorSpace | Quality | Mean | Error | StdDev |
|---------- |----------------- |-------- |----------:|----------:|----------:| |---------- |----------------- |-------- |----------:|----------:|----------:|
| Benchmark | Luminance | 75 | 7.055 ms | 0.1411 ms | 0.3297 ms | | Benchmark | Luminance | 75 | 4.618 ms | 0.0263 ms | 0.0233 ms |
| Benchmark | Rgb | 75 | 12.139 ms | 0.0645 ms | 0.0538 ms | | Benchmark | Rgb | 75 | 12.543 ms | 0.0650 ms | 0.0608 ms |
| Benchmark | YCbCrRatio420 | 75 | 6.463 ms | 0.0282 ms | 0.0235 ms | | Benchmark | YCbCrRatio420 | 75 | 6.639 ms | 0.0778 ms | 0.1256 ms |
| Benchmark | YCbCrRatio444 | 75 | 8.616 ms | 0.0422 ms | 0.0374 ms | | Benchmark | YCbCrRatio444 | 75 | 8.590 ms | 0.0570 ms | 0.0505 ms |
| Benchmark | Luminance | 90 | 7.011 ms | 0.0361 ms | 0.0301 ms | | Benchmark | Luminance | 90 | 4.902 ms | 0.0307 ms | 0.0288 ms |
| Benchmark | Rgb | 90 | 13.119 ms | 0.0947 ms | 0.0886 ms | | Benchmark | Rgb | 90 | 13.447 ms | 0.0468 ms | 0.0415 ms |
| Benchmark | YCbCrRatio420 | 90 | 6.786 ms | 0.0328 ms | 0.0274 ms | | Benchmark | YCbCrRatio420 | 90 | 7.218 ms | 0.0586 ms | 0.0548 ms |
| Benchmark | YCbCrRatio444 | 90 | 8.672 ms | 0.0772 ms | 0.0722 ms | | Benchmark | YCbCrRatio444 | 90 | 9.150 ms | 0.0779 ms | 0.0729 ms |
| Benchmark | Luminance | 100 | 9.554 ms | 0.1211 ms | 0.1012 ms | | Benchmark | Luminance | 100 | 6.731 ms | 0.0325 ms | 0.0304 ms |
| Benchmark | Rgb | 100 | 19.475 ms | 0.1080 ms | 0.0958 ms | | Benchmark | Rgb | 100 | 19.831 ms | 0.1009 ms | 0.0788 ms |
| Benchmark | YCbCrRatio420 | 100 | 10.146 ms | 0.0585 ms | 0.0519 ms | | Benchmark | YCbCrRatio420 | 100 | 10.541 ms | 0.0423 ms | 0.0396 ms |
| Benchmark | YCbCrRatio444 | 100 | 15.317 ms | 0.0709 ms | 0.0592 ms | | Benchmark | YCbCrRatio444 | 100 | 15.345 ms | 0.3276 ms | 0.3065 ms |
*/ */

240
tests/ImageSharp.Benchmarks/Color/RgbToYCbCr.LookupTables.cs

@ -1,240 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Benchmarks
{
public partial class RgbToYCbCr
{
/// <summary>
/// Scaled integer RGBA to YCbCr lookup tables
/// </summary>
private static class LookupTables
{
public static readonly int[] Y0 =
{
0, 306, 612, 918, 1224, 1530, 1836, 2142, 2448, 2754, 3060, 3366, 3672, 3978, 4284,
4590, 4896, 5202, 5508, 5814, 6120, 6426, 6732, 7038, 7344, 7650, 7956, 8262, 8568,
8874, 9180, 9486, 9792, 10098, 10404, 10710, 11016, 11322, 11628, 11934, 12240,
12546, 12852, 13158, 13464, 13770, 14076, 14382, 14688, 14994, 15300, 15606, 15912,
16218, 16524, 16830, 17136, 17442, 17748, 18054, 18360, 18666, 18972, 19278, 19584,
19890, 20196, 20502, 20808, 21114, 21420, 21726, 22032, 22338, 22644, 22950, 23256,
23562, 23868, 24174, 24480, 24786, 25092, 25398, 25704, 26010, 26316, 26622, 26928,
27234, 27540, 27846, 28152, 28458, 28764, 29070, 29376, 29682, 29988, 30294, 30600,
30906, 31212, 31518, 31824, 32130, 32436, 32742, 33048, 33354, 33660, 33966, 34272,
34578, 34884, 35190, 35496, 35802, 36108, 36414, 36720, 37026, 37332, 37638, 37944,
38250, 38556, 38862, 39168, 39474, 39780, 40086, 40392, 40698, 41004, 41310, 41616,
41922, 42228, 42534, 42840, 43146, 43452, 43758, 44064, 44370, 44676, 44982, 45288,
45594, 45900, 46206, 46512, 46818, 47124, 47430, 47736, 48042, 48348, 48654, 48960,
49266, 49572, 49878, 50184, 50490, 50796, 51102, 51408, 51714, 52020, 52326, 52632,
52938, 53244, 53550, 53856, 54162, 54468, 54774, 55080, 55386, 55692, 55998, 56304,
56610, 56916, 57222, 57528, 57834, 58140, 58446, 58752, 59058, 59364, 59670, 59976,
60282, 60588, 60894, 61200, 61506, 61812, 62118, 62424, 62730, 63036, 63342, 63648,
63954, 64260, 64566, 64872, 65178, 65484, 65790, 66096, 66402, 66708, 67014, 67320,
67626, 67932, 68238, 68544, 68850, 69156, 69462, 69768, 70074, 70380, 70686, 70992,
71298, 71604, 71910, 72216, 72522, 72828, 73134, 73440, 73746, 74052, 74358, 74664,
74970, 75276, 75582, 75888, 76194, 76500, 76806, 77112, 77418, 77724, 78030
};
public static readonly int[] Y1 =
{
0, 601, 1202, 1803, 2404, 3005, 3606, 4207, 4808, 5409, 6010, 6611, 7212, 7813, 8414,
9015, 9616, 10217, 10818, 11419, 12020, 12621, 13222, 13823, 14424, 15025, 15626,
16227, 16828, 17429, 18030, 18631, 19232, 19833, 20434, 21035, 21636, 22237, 22838,
23439, 24040, 24641, 25242, 25843, 26444, 27045, 27646, 28247, 28848, 29449, 30050,
30651, 31252, 31853, 32454, 33055, 33656, 34257, 34858, 35459, 36060, 36661, 37262,
37863, 38464, 39065, 39666, 40267, 40868, 41469, 42070, 42671, 43272, 43873, 44474,
45075, 45676, 46277, 46878, 47479, 48080, 48681, 49282, 49883, 50484, 51085, 51686,
52287, 52888, 53489, 54090, 54691, 55292, 55893, 56494, 57095, 57696, 58297, 58898,
59499, 60100, 60701, 61302, 61903, 62504, 63105, 63706, 64307, 64908, 65509, 66110,
66711, 67312, 67913, 68514, 69115, 69716, 70317, 70918, 71519, 72120, 72721, 73322,
73923, 74524, 75125, 75726, 76327, 76928, 77529, 78130, 78731, 79332, 79933, 80534,
81135, 81736, 82337, 82938, 83539, 84140, 84741, 85342, 85943, 86544, 87145, 87746,
88347, 88948, 89549, 90150, 90751, 91352, 91953, 92554, 93155, 93756, 94357, 94958,
95559, 96160, 96761, 97362, 97963, 98564, 99165, 99766, 100367, 100968, 101569,
102170, 102771, 103372, 103973, 104574, 105175, 105776, 106377, 106978, 107579,
108180, 108781, 109382, 109983, 110584, 111185, 111786, 112387, 112988, 113589,
114190, 114791, 115392, 115993, 116594, 117195, 117796, 118397, 118998, 119599,
120200, 120801, 121402, 122003, 122604, 123205, 123806, 124407, 125008, 125609,
126210, 126811, 127412, 128013, 128614, 129215, 129816, 130417, 131018, 131619,
132220, 132821, 133422, 134023, 134624, 135225, 135826, 136427, 137028, 137629,
138230, 138831, 139432, 140033, 140634, 141235, 141836, 142437, 143038, 143639,
144240, 144841, 145442, 146043, 146644, 147245, 147846, 148447, 149048, 149649,
150250, 150851, 151452, 152053, 152654, 153255
};
public static readonly int[] Y2 =
{
0, 117, 234, 351, 468, 585, 702, 819, 936, 1053, 1170, 1287, 1404, 1521, 1638, 1755,
1872, 1989, 2106, 2223, 2340, 2457, 2574, 2691, 2808, 2925, 3042, 3159, 3276, 3393,
3510, 3627, 3744, 3861, 3978, 4095, 4212, 4329, 4446, 4563, 4680, 4797, 4914, 5031,
5148, 5265, 5382, 5499, 5616, 5733, 5850, 5967, 6084, 6201, 6318, 6435, 6552, 6669,
6786, 6903, 7020, 7137, 7254, 7371, 7488, 7605, 7722, 7839, 7956, 8073, 8190, 8307,
8424, 8541, 8658, 8775, 8892, 9009, 9126, 9243, 9360, 9477, 9594, 9711, 9828, 9945,
10062, 10179, 10296, 10413, 10530, 10647, 10764, 10881, 10998, 11115, 11232, 11349,
11466, 11583, 11700, 11817, 11934, 12051, 12168, 12285, 12402, 12519, 12636, 12753,
12870, 12987, 13104, 13221, 13338, 13455, 13572, 13689, 13806, 13923, 14040, 14157,
14274, 14391, 14508, 14625, 14742, 14859, 14976, 15093, 15210, 15327, 15444, 15561,
15678, 15795, 15912, 16029, 16146, 16263, 16380, 16497, 16614, 16731, 16848, 16965,
17082, 17199, 17316, 17433, 17550, 17667, 17784, 17901, 18018, 18135, 18252, 18369,
18486, 18603, 18720, 18837, 18954, 19071, 19188, 19305, 19422, 19539, 19656, 19773,
19890, 20007, 20124, 20241, 20358, 20475, 20592, 20709, 20826, 20943, 21060, 21177,
21294, 21411, 21528, 21645, 21762, 21879, 21996, 22113, 22230, 22347, 22464, 22581,
22698, 22815, 22932, 23049, 23166, 23283, 23400, 23517, 23634, 23751, 23868, 23985,
24102, 24219, 24336, 24453, 24570, 24687, 24804, 24921, 25038, 25155, 25272, 25389,
25506, 25623, 25740, 25857, 25974, 26091, 26208, 26325, 26442, 26559, 26676, 26793,
26910, 27027, 27144, 27261, 27378, 27495, 27612, 27729, 27846, 27963, 28080, 28197,
28314, 28431, 28548, 28665, 28782, 28899, 29016, 29133, 29250, 29367, 29484, 29601,
29718, 29835
};
public static readonly int[] Cb0 =
{
0, -172, -344, -516, -688, -860, -1032, -1204, -1376, -1548, -1720, -1892,
-2064, -2236, -2408, -2580, -2752, -2924, -3096, -3268, -3440, -3612,
-3784, -3956, -4128, -4300, -4472, -4644, -4816, -4988, -5160, -5332,
-5504, -5676, -5848, -6020, -6192, -6364, -6536, -6708, -6880, -7052,
-7224, -7396, -7568, -7740, -7912, -8084, -8256, -8428, -8600, -8772,
-8944, -9116, -9288, -9460, -9632, -9804, -9976, -10148, -10320, -10492,
-10664, -10836, -11008, -11180, -11352, -11524, -11696, -11868, -12040,
-12212, -12384, -12556, -12728, -12900, -13072, -13244, -13416, -13588,
-13760, -13932, -14104, -14276, -14448, -14620, -14792, -14964, -15136,
-15308, -15480, -15652, -15824, -15996, -16168, -16340, -16512, -16684,
-16856, -17028, -17200, -17372, -17544, -17716, -17888, -18060, -18232,
-18404, -18576, -18748, -18920, -19092, -19264, -19436, -19608, -19780,
-19952, -20124, -20296, -20468, -20640, -20812, -20984, -21156, -21328,
-21500, -21672, -21844, -22016, -22188, -22360, -22532, -22704, -22876,
-23048, -23220, -23392, -23564, -23736, -23908, -24080, -24252, -24424,
-24596, -24768, -24940, -25112, -25284, -25456, -25628, -25800, -25972,
-26144, -26316, -26488, -26660, -26832, -27004, -27176, -27348, -27520,
-27692, -27864, -28036, -28208, -28380, -28552, -28724, -28896, -29068,
-29240, -29412, -29584, -29756, -29928, -30100, -30272, -30444, -30616,
-30788, -30960, -31132, -31304, -31476, -31648, -31820, -31992, -32164,
-32336, -32508, -32680, -32852, -33024, -33196, -33368, -33540, -33712,
-33884, -34056, -34228, -34400, -34572, -34744, -34916, -35088, -35260,
-35432, -35604, -35776, -35948, -36120, -36292, -36464, -36636, -36808,
-36980, -37152, -37324, -37496, -37668, -37840, -38012, -38184, -38356,
-38528, -38700, -38872, -39044, -39216, -39388, -39560, -39732, -39904,
-40076, -40248, -40420, -40592, -40764, -40936, -41108, -41280, -41452,
-41624, -41796, -41968, -42140, -42312, -42484, -42656, -42828, -43000,
-43172, -43344, -43516, -43688, -43860
};
public static readonly int[] Cb1 =
{
0, 339, 678, 1017, 1356, 1695, 2034, 2373, 2712, 3051, 3390, 3729, 4068,
4407, 4746, 5085, 5424, 5763, 6102, 6441, 6780, 7119, 7458, 7797, 8136,
8475, 8814, 9153, 9492, 9831, 10170, 10509, 10848, 11187, 11526, 11865,
12204, 12543, 12882, 13221, 13560, 13899, 14238, 14577, 14916, 15255,
15594, 15933, 16272, 16611, 16950, 17289, 17628, 17967, 18306, 18645,
18984, 19323, 19662, 20001, 20340, 20679, 21018, 21357, 21696, 22035,
22374, 22713, 23052, 23391, 23730, 24069, 24408, 24747, 25086, 25425,
25764, 26103, 26442, 26781, 27120, 27459, 27798, 28137, 28476, 28815,
29154, 29493, 29832, 30171, 30510, 30849, 31188, 31527, 31866, 32205,
32544, 32883, 33222, 33561, 33900, 34239, 34578, 34917, 35256, 35595,
35934, 36273, 36612, 36951, 37290, 37629, 37968, 38307, 38646, 38985,
39324, 39663, 40002, 40341, 40680, 41019, 41358, 41697, 42036, 42375,
42714, 43053, 43392, 43731, 44070, 44409, 44748, 45087, 45426, 45765,
46104, 46443, 46782, 47121, 47460, 47799, 48138, 48477, 48816, 49155,
49494, 49833, 50172, 50511, 50850, 51189, 51528, 51867, 52206, 52545,
52884, 53223, 53562, 53901, 54240, 54579, 54918, 55257, 55596, 55935,
56274, 56613, 56952, 57291, 57630, 57969, 58308, 58647, 58986, 59325,
59664, 60003, 60342, 60681, 61020, 61359, 61698, 62037, 62376, 62715,
63054, 63393, 63732, 64071, 64410, 64749, 65088, 65427, 65766, 66105,
66444, 66783, 67122, 67461, 67800, 68139, 68478, 68817, 69156, 69495,
69834, 70173, 70512, 70851, 71190, 71529, 71868, 72207, 72546, 72885,
73224, 73563, 73902, 74241, 74580, 74919, 75258, 75597, 75936, 76275,
76614, 76953, 77292, 77631, 77970, 78309, 78648, 78987, 79326, 79665,
80004, 80343, 80682, 81021, 81360, 81699, 82038, 82377, 82716, 83055,
83394, 83733, 84072, 84411, 84750, 85089, 85428, 85767, 86106, 86445
};
public static readonly int[] Cb2Cr0 =
{
0, 512, 1024, 1536, 2048, 2560, 3072, 3584, 4096, 4608, 5120, 5632, 6144,
6656, 7168, 7680, 8192, 8704, 9216, 9728, 10240, 10752, 11264, 11776,
12288, 12800, 13312, 13824, 14336, 14848, 15360, 15872, 16384, 16896,
17408, 17920, 18432, 18944, 19456, 19968, 20480, 20992, 21504, 22016,
22528, 23040, 23552, 24064, 24576, 25088, 25600, 26112, 26624, 27136,
27648, 28160, 28672, 29184, 29696, 30208, 30720, 31232, 31744, 32256,
32768, 33280, 33792, 34304, 34816, 35328, 35840, 36352, 36864, 37376,
37888, 38400, 38912, 39424, 39936, 40448, 40960, 41472, 41984, 42496,
43008, 43520, 44032, 44544, 45056, 45568, 46080, 46592, 47104, 47616,
48128, 48640, 49152, 49664, 50176, 50688, 51200, 51712, 52224, 52736,
53248, 53760, 54272, 54784, 55296, 55808, 56320, 56832, 57344, 57856,
58368, 58880, 59392, 59904, 60416, 60928, 61440, 61952, 62464, 62976,
63488, 64000, 64512, 65024, 65536, 66048, 66560, 67072, 67584, 68096,
68608, 69120, 69632, 70144, 70656, 71168, 71680, 72192, 72704, 73216,
73728, 74240, 74752, 75264, 75776, 76288, 76800, 77312, 77824, 78336,
78848, 79360, 79872, 80384, 80896, 81408, 81920, 82432, 82944, 83456,
83968, 84480, 84992, 85504, 86016, 86528, 87040, 87552, 88064, 88576,
89088, 89600, 90112, 90624, 91136, 91648, 92160, 92672, 93184, 93696,
94208, 94720, 95232, 95744, 96256, 96768, 97280, 97792, 98304, 98816,
99328, 99840, 100352, 100864, 101376, 101888, 102400, 102912, 103424,
103936, 104448, 104960, 105472, 105984, 106496, 107008, 107520, 108032,
108544, 109056, 109568, 110080, 110592, 111104, 111616, 112128, 112640,
113152, 113664, 114176, 114688, 115200, 115712, 116224, 116736, 117248,
117760, 118272, 118784, 119296, 119808, 120320, 120832, 121344, 121856,
122368, 122880, 123392, 123904, 124416, 124928, 125440, 125952, 126464,
126976, 127488, 128000, 128512, 129024, 129536, 130048, 130560
};
public static readonly int[] Cr1 =
{
0, 429, 858, 1287, 1716, 2145, 2574, 3003, 3432, 3861, 4290, 4719, 5148,
5577, 6006, 6435, 6864, 7293, 7722, 8151, 8580, 9009, 9438, 9867, 10296,
10725, 11154, 11583, 12012, 12441, 12870, 13299, 13728, 14157, 14586,
15015, 15444, 15873, 16302, 16731, 17160, 17589, 18018, 18447, 18876,
19305, 19734, 20163, 20592, 21021, 21450, 21879, 22308, 22737, 23166,
23595, 24024, 24453, 24882, 25311, 25740, 26169, 26598, 27027, 27456,
27885, 28314, 28743, 29172, 29601, 30030, 30459, 30888, 31317, 31746,
32175, 32604, 33033, 33462, 33891, 34320, 34749, 35178, 35607, 36036,
36465, 36894, 37323, 37752, 38181, 38610, 39039, 39468, 39897, 40326,
40755, 41184, 41613, 42042, 42471, 42900, 43329, 43758, 44187, 44616,
45045, 45474, 45903, 46332, 46761, 47190, 47619, 48048, 48477, 48906,
49335, 49764, 50193, 50622, 51051, 51480, 51909, 52338, 52767, 53196,
53625, 54054, 54483, 54912, 55341, 55770, 56199, 56628, 57057, 57486,
57915, 58344, 58773, 59202, 59631, 60060, 60489, 60918, 61347, 61776,
62205, 62634, 63063, 63492, 63921, 64350, 64779, 65208, 65637, 66066,
66495, 66924, 67353, 67782, 68211, 68640, 69069, 69498, 69927, 70356,
70785, 71214, 71643, 72072, 72501, 72930, 73359, 73788, 74217, 74646,
75075, 75504, 75933, 76362, 76791, 77220, 77649, 78078, 78507, 78936,
79365, 79794, 80223, 80652, 81081, 81510, 81939, 82368, 82797, 83226,
83655, 84084, 84513, 84942, 85371, 85800, 86229, 86658, 87087, 87516,
87945, 88374, 88803, 89232, 89661, 90090, 90519, 90948, 91377, 91806,
92235, 92664, 93093, 93522, 93951, 94380, 94809, 95238, 95667, 96096,
96525, 96954, 97383, 97812, 98241, 98670, 99099, 99528, 99957, 100386,
100815, 101244, 101673, 102102, 102531, 102960, 103389, 103818, 104247,
104676, 105105, 105534, 105963, 106392, 106821, 107250, 107679, 108108,
108537, 108966, 109395
};
public static readonly int[] Cr2 =
{
0, 83, 166, 249, 332, 415, 498, 581, 664, 747, 830, 913, 996, 1079, 1162,
1245, 1328, 1411, 1494, 1577, 1660, 1743, 1826, 1909, 1992, 2075, 2158,
2241, 2324, 2407, 2490, 2573, 2656, 2739, 2822, 2905, 2988, 3071, 3154,
3237, 3320, 3403, 3486, 3569, 3652, 3735, 3818, 3901, 3984, 4067, 4150,
4233, 4316, 4399, 4482, 4565, 4648, 4731, 4814, 4897, 4980, 5063, 5146,
5229, 5312, 5395, 5478, 5561, 5644, 5727, 5810, 5893, 5976, 6059, 6142,
6225, 6308, 6391, 6474, 6557, 6640, 6723, 6806, 6889, 6972, 7055, 7138,
7221, 7304, 7387, 7470, 7553, 7636, 7719, 7802, 7885, 7968, 8051, 8134,
8217, 8300, 8383, 8466, 8549, 8632, 8715, 8798, 8881, 8964, 9047, 9130,
9213, 9296, 9379, 9462, 9545, 9628, 9711, 9794, 9877, 9960, 10043, 10126,
10209, 10292, 10375, 10458, 10541, 10624, 10707, 10790, 10873, 10956,
11039, 11122, 11205, 11288, 11371, 11454, 11537, 11620, 11703, 11786,
11869, 11952, 12035, 12118, 12201, 12284, 12367, 12450, 12533, 12616,
12699, 12782, 12865, 12948, 13031, 13114, 13197, 13280, 13363, 13446,
13529, 13612, 13695, 13778, 13861, 13944, 14027, 14110, 14193, 14276,
14359, 14442, 14525, 14608, 14691, 14774, 14857, 14940, 15023, 15106,
15189, 15272, 15355, 15438, 15521, 15604, 15687, 15770, 15853, 15936,
16019, 16102, 16185, 16268, 16351, 16434, 16517, 16600, 16683, 16766,
16849, 16932, 17015, 17098, 17181, 17264, 17347, 17430, 17513, 17596,
17679, 17762, 17845, 17928, 18011, 18094, 18177, 18260, 18343, 18426,
18509, 18592, 18675, 18758, 18841, 18924, 19007, 19090, 19173, 19256,
19339, 19422, 19505, 19588, 19671, 19754, 19837, 19920, 20003, 20086,
20169, 20252, 20335, 20418, 20501, 20584, 20667, 20750, 20833, 20916,
20999, 21082, 21165
};
}
}
}

359
tests/ImageSharp.Benchmarks/Color/RgbToYCbCr.cs

@ -1,359 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
namespace SixLabors.ImageSharp.Benchmarks
{
public partial class RgbToYCbCr
{
private const int InputColorCount = 64;
private const int InputByteCount = InputColorCount * 3;
private static readonly Vector3 VectorY = new Vector3(0.299F, 0.587F, 0.114F);
private static readonly Vector3 VectorCb = new Vector3(-0.168736F, 0.331264F, 0.5F);
private static readonly Vector3 VectorCr = new Vector3(0.5F, 0.418688F, 0.081312F);
private static class ScaledCoeffs
{
public static readonly int[] Y =
{
306, 601, 117, 0,
306, 601, 117, 0,
};
public static readonly int[] Cb =
{
-172, 339, 512, 0,
-172, 339, 512, 0,
};
public static readonly int[] Cr =
{
512, 429, 83, 0,
512, 429, 83, 0,
};
public static class SelectLeft
{
public static readonly int[] Y =
{
1, 1, 1, 0,
0, 0, 0, 0,
};
public static readonly int[] Cb =
{
1, -1, 1, 0,
0, 0, 0, 0,
};
public static readonly int[] Cr =
{
1, -1, -1, 0,
0, 0, 0, 0,
};
}
public static class SelectRight
{
public static readonly int[] Y =
{
0, 0, 0, 0,
1, 1, 1, 0,
};
public static readonly int[] Cb =
{
0, 0, 0, 0,
1, -1, 1, 0,
};
public static readonly int[] Cr =
{
0, 0, 0, 0,
1, -1, -1, 0,
};
}
}
private static class OnStackInputCache
{
public unsafe struct Byte
{
public fixed byte Data[InputByteCount * 3];
public static Byte Create(byte[] data)
{
Byte result = default;
for (int i = 0; i < data.Length; i++)
{
result.Data[i] = data[i];
}
return result;
}
}
}
public struct Result
{
internal Block8x8F Y;
internal Block8x8F Cb;
internal Block8x8F Cr;
}
// The operation is defined as "RGBA -> YCbCr Transform a stream of bytes into a stream of floats"
// We need to benchmark the whole operation, to get true results, not missing any side effects!
private byte[] inputSourceRGB;
private int[] inputSourceRGBAsInteger;
[GlobalSetup]
public void Setup()
{
// Console.WriteLine("Vector<int>.Count: " + Vector<int>.Count);
this.inputSourceRGB = new byte[InputByteCount];
for (int i = 0; i < this.inputSourceRGB.Length; i++)
{
this.inputSourceRGB[i] = (byte)(42 + i);
}
this.inputSourceRGBAsInteger = new int[InputByteCount + Vector<int>.Count]; // Filling this should be part of the measured operation
}
[Benchmark(Baseline = true, Description = "Floating Point Conversion")]
public unsafe void RgbaToYcbCrScalarFloat()
{
// Copy the input to the stack:
var input = OnStackInputCache.Byte.Create(this.inputSourceRGB);
// On-stack output:
Result result = default;
var yPtr = (float*)&result.Y;
var cbPtr = (float*)&result.Cb;
var crPtr = (float*)&result.Cr;
for (int i = 0; i < InputColorCount; i++)
{
int i3 = i * 3;
float r = input.Data[i3 + 0];
float g = input.Data[i3 + 1];
float b = input.Data[i3 + 2];
*yPtr++ = (0.299F * r) + (0.587F * g) + (0.114F * b);
*cbPtr++ = 128 + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b));
*crPtr++ = 128 + ((0.5F * r) - (0.418688F * g) - (0.081312F * b));
}
}
[Benchmark(Description = "Simd Floating Point Conversion")]
public unsafe void RgbaToYcbCrSimdFloat()
{
// Copy the input to the stack:
var input = OnStackInputCache.Byte.Create(this.inputSourceRGB);
// On-stack output:
Result result = default;
var yPtr = (float*)&result.Y;
var cbPtr = (float*)&result.Cb;
var crPtr = (float*)&result.Cr;
for (int i = 0; i < InputColorCount; i++)
{
int i3 = i * 3;
var vectorRgb = new Vector3(
input.Data[i3 + 0],
input.Data[i3 + 1],
input.Data[i3 + 2]);
Vector3 vectorY = VectorY * vectorRgb;
Vector3 vectorCb = VectorCb * vectorRgb;
Vector3 vectorCr = VectorCr * vectorRgb;
*yPtr++ = vectorY.X + vectorY.Y + vectorY.Z;
*cbPtr++ = 128 + (vectorCb.X - vectorCb.Y + vectorCb.Z);
*crPtr++ = 128 + (vectorCr.X - vectorCr.Y - vectorCr.Z);
}
}
[Benchmark(Description = "Scaled Integer Conversion + Vector<int>")]
public unsafe void RgbaToYcbCrScaledIntegerSimd()
{
// Copy the input to the stack:
// On-stack output:
Result result = default;
var yPtr = (float*)&result.Y;
var cbPtr = (float*)&result.Cb;
var crPtr = (float*)&result.Cr;
var yCoeffs = new Vector<int>(ScaledCoeffs.Y);
var cbCoeffs = new Vector<int>(ScaledCoeffs.Cb);
var crCoeffs = new Vector<int>(ScaledCoeffs.Cr);
for (int i = 0; i < this.inputSourceRGB.Length; i++)
{
this.inputSourceRGBAsInteger[i] = this.inputSourceRGB[i];
}
for (int i = 0; i < InputColorCount; i += 2)
{
var rgb = new Vector<int>(this.inputSourceRGBAsInteger, i * 3);
Vector<int> y = yCoeffs * rgb;
Vector<int> cb = cbCoeffs * rgb;
Vector<int> cr = crCoeffs * rgb;
*yPtr++ = (y[0] + y[1] + y[2]) >> 10;
*cbPtr++ = 128 + ((cb[0] - cb[1] + cb[2]) >> 10);
*crPtr++ = 128 + ((cr[0] - cr[1] - cr[2]) >> 10);
*yPtr++ = (y[4] + y[5] + y[6]) >> 10;
*cbPtr++ = 128 + ((cb[4] - cb[5] + cb[6]) >> 10);
*crPtr++ = 128 + ((cr[4] - cr[5] - cr[6]) >> 10);
}
}
/// <summary>
/// This should perform better. Coreclr emitted Vector.Dot() code lacks the vectorization even with IsHardwareAccelerated == true.
/// Kept this benchmark because maybe it will be improved in a future CLR release.
/// <see>
/// <cref>https://www.gamedev.net/topic/673396-c-systemnumericsvectors-slow/</cref>
/// </see>
/// </summary>
[Benchmark(Description = "Scaled Integer Conversion + Vector<int> + Dot Product")]
public unsafe void RgbaToYcbCrScaledIntegerSimdWithDotProduct()
{
// Copy the input to the stack:
// On-stack output:
Result result = default;
float* yPtr = (float*)&result.Y;
float* cbPtr = (float*)&result.Cb;
float* crPtr = (float*)&result.Cr;
var yCoeffs = new Vector<int>(ScaledCoeffs.Y);
var cbCoeffs = new Vector<int>(ScaledCoeffs.Cb);
var crCoeffs = new Vector<int>(ScaledCoeffs.Cr);
var leftY = new Vector<int>(ScaledCoeffs.SelectLeft.Y);
var leftCb = new Vector<int>(ScaledCoeffs.SelectLeft.Cb);
var leftCr = new Vector<int>(ScaledCoeffs.SelectLeft.Cr);
var rightY = new Vector<int>(ScaledCoeffs.SelectRight.Y);
var rightCb = new Vector<int>(ScaledCoeffs.SelectRight.Cb);
var rightCr = new Vector<int>(ScaledCoeffs.SelectRight.Cr);
for (int i = 0; i < this.inputSourceRGB.Length; i++)
{
this.inputSourceRGBAsInteger[i] = this.inputSourceRGB[i];
}
for (int i = 0; i < InputColorCount; i += 2)
{
var rgb = new Vector<int>(this.inputSourceRGBAsInteger, i * 3);
Vector<int> y = yCoeffs * rgb;
Vector<int> cb = cbCoeffs * rgb;
Vector<int> cr = crCoeffs * rgb;
VectorizedConvertImpl(ref yPtr, ref cbPtr, ref crPtr, y, cb, cr, leftY, leftCb, leftCr);
VectorizedConvertImpl(ref yPtr, ref cbPtr, ref crPtr, y, cb, cr, rightY, rightCb, rightCr);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe void VectorizedConvertImpl(
ref float* yPtr,
ref float* cbPtr,
ref float* crPtr,
Vector<int> y,
Vector<int> cb,
Vector<int> cr,
Vector<int> yAgg,
Vector<int> cbAgg,
Vector<int> crAgg)
{
int ySum = Vector.Dot(y, yAgg);
int cbSum = Vector.Dot(cb, cbAgg);
int crSum = Vector.Dot(cr, crAgg);
*yPtr++ = ySum >> 10;
*cbPtr++ = 128 + (cbSum >> 10);
*crPtr++ = 128 + (crSum >> 10);
}
[Benchmark(Description = "Scaled Integer Conversion")]
public unsafe void RgbaToYcbCrScaledInteger()
{
// Copy the input to the stack:
var input = OnStackInputCache.Byte.Create(this.inputSourceRGB);
// On-stack output:
Result result = default;
float* yPtr = (float*)&result.Y;
float* cbPtr = (float*)&result.Cb;
float* crPtr = (float*)&result.Cr;
for (int i = 0; i < InputColorCount; i++)
{
int i3 = i * 3;
int r = input.Data[i3 + 0];
int g = input.Data[i3 + 1];
int b = input.Data[i3 + 2];
// Scale by 1024, add .5F and truncate value
int y0 = 306 * r; // (0.299F * 1024) + .5F
int y1 = 601 * g; // (0.587F * 1024) + .5F
int y2 = 117 * b; // (0.114F * 1024) + .5F
int cb0 = -172 * r; // (-0.168736F * 1024) + .5F
int cb1 = 339 * g; // (0.331264F * 1024) + .5F
int cb2 = 512 * b; // (0.5F * 1024) + .5F
int cr0 = 512 * r; // (0.5F * 1024) + .5F
int cr1 = 429 * g; // (0.418688F * 1024) + .5F
int cr2 = 83 * b; // (0.081312F * 1024) + .5F
*yPtr++ = (y0 + y1 + y2) >> 10;
*cbPtr++ = 128 + ((cb0 - cb1 + cb2) >> 10);
*crPtr++ = 128 + ((cr0 - cr1 - cr2) >> 10);
}
}
[Benchmark(Description = "Scaled Integer LUT Conversion")]
public unsafe void RgbaToYcbCrScaledIntegerLut()
{
// Copy the input to the stack:
var input = OnStackInputCache.Byte.Create(this.inputSourceRGB);
// On-stack output:
Result result = default;
float* yPtr = (float*)&result.Y;
float* cbPtr = (float*)&result.Cb;
float* crPtr = (float*)&result.Cr;
for (int i = 0; i < InputColorCount; i++)
{
int i3 = i * 3;
int r = input.Data[i3 + 0];
int g = input.Data[i3 + 1];
int b = input.Data[i3 + 2];
// TODO: Maybe concatenating all the arrays in LookupTables to a flat one can improve this!
*yPtr++ = (LookupTables.Y0[r] + LookupTables.Y1[g] + LookupTables.Y2[b]) >> 10;
*cbPtr++ = 128 + ((LookupTables.Cb0[r] - LookupTables.Cb1[g] + LookupTables.Cb2Cr0[b]) >> 10);
*crPtr++ = 128 + ((LookupTables.Cb2Cr0[r] - LookupTables.Cr1[g] - LookupTables.Cr2[b]) >> 10);
}
}
}
}

2
tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs

@ -10,11 +10,13 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ImageMagick; using ImageMagick;
using PhotoSauce.MagicScaler; using PhotoSauce.MagicScaler;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests; using SixLabors.ImageSharp.Tests;
using SkiaSharp; using SkiaSharp;

20
tests/ImageSharp.Tests.ProfilingSandbox/Program.cs

@ -66,12 +66,6 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
return null; return null;
} }
private static void RunJpegEncoderProfilingTests()
{
var benchmarks = new JpegProfilingBenchmarks(new ConsoleOutput());
benchmarks.EncodeJpeg_SingleMidSize();
}
private static void RunResizeProfilingTest() private static void RunResizeProfilingTest()
{ {
var test = new ResizeProfilingBenchmarks(new ConsoleOutput()); var test = new ResizeProfilingBenchmarks(new ConsoleOutput());
@ -83,19 +77,5 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
var tests = new PixelOperationsTests.Rgba32_OperationsTests(new ConsoleOutput()); var tests = new PixelOperationsTests.Rgba32_OperationsTests(new ConsoleOutput());
tests.Benchmark_ToVector4(); tests.Benchmark_ToVector4();
} }
private static void RunDecodeJpegProfilingTests()
{
Console.WriteLine("RunDecodeJpegProfilingTests...");
var benchmarks = new JpegProfilingBenchmarks(new ConsoleOutput());
foreach (object[] data in JpegProfilingBenchmarks.DecodeJpegData)
{
string fileName = (string)data[0];
int executionCount = (int)data[1];
benchmarks.DecodeJpeg(fileName, executionCount);
}
Console.WriteLine("DONE.");
}
} }
} }

97
tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.CopyToBufferArea.cs

@ -1,97 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// 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<float> 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<float> buffer = Configuration.Default.MemoryAllocator.Allocate2D<float>(20, 20, AllocationOptions.Clean))
{
Buffer2DRegion<float> 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<float> buffer = Configuration.Default.MemoryAllocator.Allocate2D<float>(100, 100, AllocationOptions.Clean))
{
Buffer2DRegion<float> 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);
}
}
}
}
}

57
tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs

@ -2,11 +2,9 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System; using System;
using System.Numerics;
using SixLabors.ImageSharp.ColorSpaces; using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion; using SixLabors.ImageSharp.ColorSpaces.Conversion;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Tests.Colorspaces.Conversion; using SixLabors.ImageSharp.Tests.Colorspaces.Conversion;
using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities;
@ -24,11 +22,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private const int TestBufferLength = 40; private const int TestBufferLength = 40;
#if SUPPORTS_RUNTIME_INTRINSICS
private static readonly HwIntrinsics IntrinsicsConfig = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX; private static readonly HwIntrinsics IntrinsicsConfig = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX;
#else
private static readonly HwIntrinsics IntrinsicsConfig = HwIntrinsics.AllowAll;
#endif
private static readonly ApproximateColorSpaceComparer ColorSpaceComparer = new(epsilon: Precision); private static readonly ApproximateColorSpaceComparer ColorSpaceComparer = new(epsilon: Precision);
@ -46,14 +40,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Fact] [Fact]
public void GetConverterThrowsExceptionOnInvalidColorSpace() public void GetConverterThrowsExceptionOnInvalidColorSpace()
{ {
Assert.Throws<Exception>(() => JpegColorConverterBase.GetConverter(JpegColorSpace.Undefined, 8)); var invalidColorSpace = (JpegColorSpace)(-1);
Assert.Throws<Exception>(() => JpegColorConverterBase.GetConverter(invalidColorSpace, 8));
} }
[Fact] [Fact]
public void GetConverterThrowsExceptionOnInvalidPrecision() public void GetConverterThrowsExceptionOnInvalidPrecision()
{ {
// Valid precisions: 8 & 12 bit // Valid precisions: 8 & 12 bit
Assert.Throws<Exception>(() => JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, 9)); int invalidPrecision = 9;
Assert.Throws<Exception>(() => JpegColorConverterBase.GetConverter(JpegColorSpace.YCbCr, invalidPrecision));
} }
[Theory] [Theory]
@ -95,13 +91,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromYCbCrBasic(int seed) => public void FromYCbCrBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromYCbCrScalar(8), 3, seed); this.TestConverter(new JpegColorConverterBase.YCbCrScalar(8), 3, seed);
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromYCbCrVector(int seed) public void FromYCbCrVector(int seed)
{ {
var converter = new JpegColorConverterBase.FromYCbCrVector(8); var converter = new JpegColorConverterBase.YCbCrVector(8);
if (!converter.IsAvailable) if (!converter.IsAvailable)
{ {
@ -117,7 +113,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
static void RunTest(string arg) => static void RunTest(string arg) =>
ValidateConversion( ValidateConversion(
new JpegColorConverterBase.FromYCbCrVector(8), new JpegColorConverterBase.YCbCrVector(8),
3, 3,
FeatureTestRunner.Deserialize<int>(arg)); FeatureTestRunner.Deserialize<int>(arg));
} }
@ -125,13 +121,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromCmykBasic(int seed) => public void FromCmykBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromCmykScalar(8), 4, seed); this.TestConverter(new JpegColorConverterBase.CmykScalar(8), 4, seed);
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromCmykVector(int seed) public void FromCmykVector(int seed)
{ {
var converter = new JpegColorConverterBase.FromCmykVector(8); var converter = new JpegColorConverterBase.CmykVector(8);
if (!converter.IsAvailable) if (!converter.IsAvailable)
{ {
@ -147,7 +143,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
static void RunTest(string arg) => static void RunTest(string arg) =>
ValidateConversion( ValidateConversion(
new JpegColorConverterBase.FromCmykVector(8), new JpegColorConverterBase.CmykVector(8),
4, 4,
FeatureTestRunner.Deserialize<int>(arg)); FeatureTestRunner.Deserialize<int>(arg));
} }
@ -155,13 +151,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromGrayscaleBasic(int seed) => public void FromGrayscaleBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromGrayscaleScalar(8), 1, seed); this.TestConverter(new JpegColorConverterBase.GrayscaleScalar(8), 1, seed);
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromGrayscaleVector(int seed) public void FromGrayscaleVector(int seed)
{ {
var converter = new JpegColorConverterBase.FromGrayScaleVector(8); var converter = new JpegColorConverterBase.GrayScaleVector(8);
if (!converter.IsAvailable) if (!converter.IsAvailable)
{ {
@ -177,7 +173,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
static void RunTest(string arg) => static void RunTest(string arg) =>
ValidateConversion( ValidateConversion(
new JpegColorConverterBase.FromGrayScaleVector(8), new JpegColorConverterBase.GrayScaleVector(8),
1, 1,
FeatureTestRunner.Deserialize<int>(arg)); FeatureTestRunner.Deserialize<int>(arg));
} }
@ -185,13 +181,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbBasic(int seed) => public void FromRgbBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromRgbScalar(8), 3, seed); this.TestConverter(new JpegColorConverterBase.RgbScalar(8), 3, seed);
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbVector(int seed) public void FromRgbVector(int seed)
{ {
var converter = new JpegColorConverterBase.FromRgbVector(8); var converter = new JpegColorConverterBase.RgbVector(8);
if (!converter.IsAvailable) if (!converter.IsAvailable)
{ {
@ -207,7 +203,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
static void RunTest(string arg) => static void RunTest(string arg) =>
ValidateConversion( ValidateConversion(
new JpegColorConverterBase.FromRgbVector(8), new JpegColorConverterBase.RgbVector(8),
3, 3,
FeatureTestRunner.Deserialize<int>(arg)); FeatureTestRunner.Deserialize<int>(arg));
} }
@ -215,13 +211,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromYccKBasic(int seed) => public void FromYccKBasic(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromYccKScalar(8), 4, seed); this.TestConverter(new JpegColorConverterBase.YccKScalar(8), 4, seed);
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromYccKVector(int seed) public void FromYccKVector(int seed)
{ {
var converter = new JpegColorConverterBase.FromYccKVector(8); var converter = new JpegColorConverterBase.YccKVector(8);
if (!converter.IsAvailable) if (!converter.IsAvailable)
{ {
@ -237,37 +233,35 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
static void RunTest(string arg) => static void RunTest(string arg) =>
ValidateConversion( ValidateConversion(
new JpegColorConverterBase.FromYccKVector(8), new JpegColorConverterBase.YccKVector(8),
4, 4,
FeatureTestRunner.Deserialize<int>(arg)); FeatureTestRunner.Deserialize<int>(arg));
} }
#if SUPPORTS_RUNTIME_INTRINSICS
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromYCbCrAvx2(int seed) => public void FromYCbCrAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromYCbCrAvx(8), 3, seed); this.TestConverter(new JpegColorConverterBase.YCbCrAvx(8), 3, seed);
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromCmykAvx2(int seed) => public void FromCmykAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromCmykAvx(8), 4, seed); this.TestConverter(new JpegColorConverterBase.CmykAvx(8), 4, seed);
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromGrayscaleAvx2(int seed) => public void FromGrayscaleAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromGrayscaleAvx(8), 1, seed); this.TestConverter(new JpegColorConverterBase.GrayscaleAvx(8), 1, seed);
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromRgbAvx2(int seed) => public void FromRgbAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromRgbAvx(8), 3, seed); this.TestConverter(new JpegColorConverterBase.RgbAvx(8), 3, seed);
[Theory] [Theory]
[MemberData(nameof(Seeds))] [MemberData(nameof(Seeds))]
public void FromYccKAvx2(int seed) => public void FromYccKAvx2(int seed) =>
this.TestConverter(new JpegColorConverterBase.FromYccKAvx(8), 4, seed); this.TestConverter(new JpegColorConverterBase.YccKAvx(8), 4, seed);
#endif
private void TestConverter( private void TestConverter(
JpegColorConverterBase converter, JpegColorConverterBase converter,
@ -357,7 +351,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
case JpegColorSpace.YCbCr: case JpegColorSpace.YCbCr:
ValidateYCbCr(original, result, i); ValidateYCbCr(original, result, i);
break; break;
case JpegColorSpace.Undefined:
default: default:
Assert.True(false, $"Invalid Colorspace enum value: {colorSpace}."); Assert.True(false, $"Invalid Colorspace enum value: {colorSpace}.");
break; break;

1
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs

@ -3,6 +3,7 @@
using System; using System;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using Xunit; using Xunit;

29
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs

@ -153,15 +153,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
} }
[Theory] [Theory]
[InlineData(TestImages.Jpeg.Baseline.Floorplan, JpegColorType.Luminance)] [InlineData(TestImages.Jpeg.Baseline.Floorplan, JpegEncodingColor.Luminance)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, JpegColorType.YCbCrRatio420)] [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, JpegEncodingColor.YCbCrRatio420)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg444, JpegColorType.YCbCrRatio444)] [InlineData(TestImages.Jpeg.Baseline.Jpeg444, JpegEncodingColor.YCbCrRatio444)]
[InlineData(TestImages.Jpeg.Baseline.JpegRgb, JpegColorType.Rgb)] [InlineData(TestImages.Jpeg.Baseline.JpegRgb, JpegEncodingColor.Rgb)]
[InlineData(TestImages.Jpeg.Baseline.Cmyk, JpegColorType.Cmyk)] [InlineData(TestImages.Jpeg.Baseline.Cmyk, JpegEncodingColor.Cmyk)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg410, JpegColorType.YCbCrRatio410)] [InlineData(TestImages.Jpeg.Baseline.Jpeg410, JpegEncodingColor.YCbCrRatio410)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg422, JpegColorType.YCbCrRatio422)] [InlineData(TestImages.Jpeg.Baseline.Jpeg411, JpegEncodingColor.YCbCrRatio411)]
[InlineData(TestImages.Jpeg.Baseline.Jpeg411, JpegColorType.YCbCrRatio411)] public void Identify_DetectsCorrectColorType(string imagePath, JpegEncodingColor expectedColorType)
public void Identify_DetectsCorrectColorType(string imagePath, JpegColorType expectedColorType)
{ {
var testFile = TestFile.Create(imagePath); var testFile = TestFile.Create(imagePath);
using var stream = new MemoryStream(testFile.Bytes, false); using var stream = new MemoryStream(testFile.Bytes, false);
@ -171,12 +170,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
} }
[Theory] [Theory]
[WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgb24, JpegColorType.Luminance)] [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgb24, JpegEncodingColor.Luminance)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgb24, JpegColorType.YCbCrRatio420)] [WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio420)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgb24, JpegColorType.YCbCrRatio444)] [WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)]
[WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgb24, JpegColorType.Rgb)] [WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgb24, JpegEncodingColor.Rgb)]
[WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgb24, JpegColorType.Cmyk)] [WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgb24, JpegEncodingColor.Cmyk)]
public void Decode_DetectsCorrectColorType<TPixel>(TestImageProvider<TPixel> provider, JpegColorType expectedColorType) public void Decode_DetectsCorrectColorType<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor expectedColorType)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using Image<TPixel> image = provider.GetImage(JpegDecoder); using Image<TPixel> image = provider.GetImage(JpegDecoder);

148
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs

@ -2,8 +2,13 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using Xunit; using Xunit;
@ -13,6 +18,83 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Trait("Format", "Jpg")] [Trait("Format", "Jpg")]
public partial class JpegEncoderTests public partial class JpegEncoderTests
{ {
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new()
{
{ TestImages.Jpeg.Baseline.Ratio1x1, 1, 1, PixelResolutionUnit.AspectRatio },
{ TestImages.Jpeg.Baseline.Snake, 300, 300, PixelResolutionUnit.PixelsPerInch },
{ TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch }
};
public static readonly TheoryData<string, int> QualityFiles =
new()
{
{ TestImages.Jpeg.Baseline.Calliphora, 80 },
{ TestImages.Jpeg.Progressive.Fb, 75 }
};
[Fact]
public void Encode_PreservesIptcProfile()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
input.Metadata.IptcProfile = new IptcProfile();
input.Metadata.IptcProfile.SetValue(IptcTag.Byline, "unit_test");
// act
using var memStream = new MemoryStream();
input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
IptcProfile actual = output.Metadata.IptcProfile;
Assert.NotNull(actual);
IEnumerable<IptcValue> values = input.Metadata.IptcProfile.Values;
Assert.Equal(values, actual.Values);
}
[Fact]
public void Encode_PreservesExifProfile()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
input.Metadata.ExifProfile = new ExifProfile();
input.Metadata.ExifProfile.SetValue(ExifTag.Software, "unit_test");
// act
using var memStream = new MemoryStream();
input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
ExifProfile actual = output.Metadata.ExifProfile;
Assert.NotNull(actual);
IReadOnlyList<IExifValue> values = input.Metadata.ExifProfile.Values;
Assert.Equal(values, actual.Values);
}
[Fact]
public void Encode_PreservesIccProfile()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
input.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.Profile_Random_Array);
// act
using var memStream = new MemoryStream();
input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
IccProfile actual = output.Metadata.IccProfile;
Assert.NotNull(actual);
IccProfile values = input.Metadata.IccProfile;
Assert.Equal(values.Entries, actual.Entries);
}
[Theory] [Theory]
[WithFile(TestImages.Jpeg.Issues.ValidExifArgumentNullExceptionOnEncode, PixelTypes.Rgba32)] [WithFile(TestImages.Jpeg.Issues.ValidExifArgumentNullExceptionOnEncode, PixelTypes.Rgba32)]
public void Encode_WithValidExifProfile_DoesNotThrowException<TPixel>(TestImageProvider<TPixel> provider) public void Encode_WithValidExifProfile_DoesNotThrowException<TPixel>(TestImageProvider<TPixel> provider)
@ -29,5 +111,71 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Null(ex); Assert.Null(ex);
} }
[Theory]
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, JpegEncoder);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
}
[Theory]
[MemberData(nameof(QualityFiles))]
public void Encode_PreservesQuality(string imagePath, int quality)
{
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, JpegEncoder);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
JpegMetadata meta = output.Metadata.GetJpegMetadata();
Assert.Equal(quality, meta.Quality);
}
}
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgb24, JpegEncodingColor.Luminance)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio420)]
[WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgb24, JpegEncodingColor.Rgb)]
public void Encode_PreservesColorType<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor expectedColorType)
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
using Image<TPixel> input = provider.GetImage(JpegDecoder);
using var memoryStream = new MemoryStream();
// act
input.Save(memoryStream, JpegEncoder);
// assert
memoryStream.Position = 0;
using var output = Image.Load<Rgba32>(memoryStream);
JpegMetadata meta = output.Metadata.GetJpegMetadata();
Assert.Equal(expectedColorType, meta.ColorType);
}
} }
} }

457
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

@ -1,15 +1,10 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities;
@ -26,189 +21,149 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private static JpegDecoder JpegDecoder => new(); private static JpegDecoder JpegDecoder => new();
public static readonly TheoryData<string, int> QualityFiles = private static readonly TheoryData<int> TestQualities = new()
new()
{ {
{ TestImages.Jpeg.Baseline.Calliphora, 80 }, 40,
{ TestImages.Jpeg.Progressive.Fb, 75 } 80,
100,
}; };
public static readonly TheoryData<JpegColorType, int> BitsPerPixel_Quality = public static readonly TheoryData<JpegEncodingColor, int, float> NonSubsampledEncodingSetups = new()
new()
{ {
{ JpegColorType.YCbCrRatio420, 40 }, { JpegEncodingColor.Rgb, 100, 0.0238f / 100 },
{ JpegColorType.YCbCrRatio420, 60 }, { JpegEncodingColor.Rgb, 80, 1.3044f / 100 },
{ JpegColorType.YCbCrRatio420, 100 }, { JpegEncodingColor.Rgb, 40, 2.9879f / 100 },
{ JpegColorType.YCbCrRatio444, 40 },
{ JpegColorType.YCbCrRatio444, 60 },
{ JpegColorType.YCbCrRatio444, 100 },
{ JpegColorType.Rgb, 40 },
{ JpegColorType.Rgb, 60 },
{ JpegColorType.Rgb, 100 }
};
public static readonly TheoryData<int> Grayscale_Quality = { JpegEncodingColor.YCbCrRatio444, 100, 0.0780f / 100 },
new() { JpegEncodingColor.YCbCrRatio444, 80, 1.4585f / 100 },
{ { JpegEncodingColor.YCbCrRatio444, 40, 3.1413f / 100 },
{ 40 },
{ 60 },
{ 100 }
}; };
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles = public static readonly TheoryData<JpegEncodingColor, int, float> SubsampledEncodingSetups = new()
new()
{ {
{ TestImages.Jpeg.Baseline.Ratio1x1, 1, 1, PixelResolutionUnit.AspectRatio }, { JpegEncodingColor.YCbCrRatio422, 100, 0.4895f / 100 },
{ TestImages.Jpeg.Baseline.Snake, 300, 300, PixelResolutionUnit.PixelsPerInch }, { JpegEncodingColor.YCbCrRatio422, 80, 1.6043f / 100 },
{ TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch } { JpegEncodingColor.YCbCrRatio422, 40, 3.1996f / 100 },
{ JpegEncodingColor.YCbCrRatio420, 100, 0.5790f / 100 },
{ JpegEncodingColor.YCbCrRatio420, 80, 1.6692f / 100 },
{ JpegEncodingColor.YCbCrRatio420, 40, 3.2324f / 100 },
{ JpegEncodingColor.YCbCrRatio411, 100, 0.6868f / 100 },
{ JpegEncodingColor.YCbCrRatio411, 80, 1.7139f / 100 },
{ JpegEncodingColor.YCbCrRatio411, 40, 3.2634f / 100 },
{ JpegEncodingColor.YCbCrRatio410, 100, 0.7357f / 100 },
{ JpegEncodingColor.YCbCrRatio410, 80, 1.7495f / 100 },
{ JpegEncodingColor.YCbCrRatio410, 40, 3.2911f / 100 },
}; };
[Theory] public static readonly TheoryData<JpegEncodingColor, int, float> CmykEncodingSetups = new()
[WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32, JpegColorType.Luminance)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg420Small, PixelTypes.Rgba32, JpegColorType.YCbCrRatio420)]
[WithFile(TestImages.Jpeg.Baseline.JpegRgb, PixelTypes.Rgba32, JpegColorType.Rgb)]
public void Encode_PreservesColorType<TPixel>(TestImageProvider<TPixel> provider, JpegColorType expectedColorType)
where TPixel : unmanaged, IPixel<TPixel>
{ {
// arrange { JpegEncodingColor.Cmyk, 100, 0.0159f / 100 },
using Image<TPixel> input = provider.GetImage(JpegDecoder); { JpegEncodingColor.Cmyk, 80, 0.3922f / 100 },
using var memoryStream = new MemoryStream(); { JpegEncodingColor.Cmyk, 40, 0.6488f / 100 },
};
// act
input.Save(memoryStream, JpegEncoder);
// assert
memoryStream.Position = 0;
using var output = Image.Load<Rgba32>(memoryStream);
JpegMetadata meta = output.Metadata.GetJpegMetadata();
Assert.Equal(expectedColorType, meta.ColorType);
}
[Theory] public static readonly TheoryData<JpegEncodingColor, int, float> YcckEncodingSetups = new()
[WithFile(TestImages.Jpeg.Baseline.Cmyk, PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg410, PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg411, PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg422, PixelTypes.Rgba32)]
public void Encode_WithUnsupportedColorType_FromInputImage_DefaultsToYCbCr420<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{ {
// arrange { JpegEncodingColor.Ycck, 100, 0.0356f / 100 },
using Image<TPixel> input = provider.GetImage(JpegDecoder); { JpegEncodingColor.Ycck, 80, 0.1245f / 100 },
using var memoryStream = new MemoryStream(); { JpegEncodingColor.Ycck, 40, 0.2663f / 100 },
};
// act public static readonly TheoryData<JpegEncodingColor, int, float> LuminanceEncodingSetups = new()
input.Save(memoryStream, new JpegEncoder()
{ {
Quality = 75 { JpegEncodingColor.Luminance, 100, 0.0175f / 100 },
}); { JpegEncodingColor.Luminance, 80, 0.6730f / 100 },
{ JpegEncodingColor.Luminance, 40, 0.9941f / 100 },
// assert };
memoryStream.Position = 0;
using var output = Image.Load<Rgba32>(memoryStream);
JpegMetadata meta = output.Metadata.GetJpegMetadata();
Assert.Equal(JpegColorType.YCbCrRatio420, meta.ColorType);
}
[Theory] [Theory]
[InlineData(JpegColorType.Cmyk)] [WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
[InlineData(JpegColorType.YCbCrRatio410)] [WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
[InlineData(JpegColorType.YCbCrRatio411)] [WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
[InlineData(JpegColorType.YCbCrRatio422)] [WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
public void Encode_WithUnsupportedColorType_DefaultsToYCbCr420(JpegColorType colorType) [WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
{ public void EncodeBaseline_Interleaved<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
// arrange where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality, tolerance);
var jpegEncoder = new JpegEncoder() { ColorType = colorType };
using var input = new Image<Rgb24>(10, 10);
using var memoryStream = new MemoryStream();
// act
input.Save(memoryStream, jpegEncoder);
// assert
memoryStream.Position = 0;
using var output = Image.Load<Rgba32>(memoryStream);
JpegMetadata meta = output.Metadata.GetJpegMetadata();
Assert.Equal(JpegColorType.YCbCrRatio420, meta.ColorType);
}
[Theory] [Theory]
[MemberData(nameof(QualityFiles))] [WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
public void Encode_PreservesQuality(string imagePath, int quality) [WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
{ [WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
var testFile = TestFile.Create(imagePath); [WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
using (Image<Rgba32> input = testFile.CreateRgba32Image()) [WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
{ public void EncodeBaseline_NonInterleavedMode<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
using (var memStream = new MemoryStream()) where TPixel : unmanaged, IPixel<TPixel>
{ {
input.Save(memStream, JpegEncoder); using Image<TPixel> image = provider.GetImage();
memStream.Position = 0; var encoder = new JpegEncoder
using (var output = Image.Load<Rgba32>(memStream))
{ {
JpegMetadata meta = output.Metadata.GetJpegMetadata(); Quality = quality,
Assert.Equal(quality, meta.Quality); ColorType = colorType,
} Interleaved = false,
} };
} string info = $"{colorType}-Q{quality}";
}
[Theory] ImageComparer comparer = new TolerantImageComparer(tolerance);
[WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)]
public void EncodeBaseline_CalliphoraPartial<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality) // Does DebugSave & load reference CompareToReferenceInput():
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality); image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
}
[Theory] [Theory]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 600, 400, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 158, 24, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 158, 24, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 153, 21, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 153, 21, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 600, 400, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 143, 81, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 138, 24, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 138, 24, PixelTypes.Rgb24)]
public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality) public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality); where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality);
[Theory] [Theory]
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 100, 100, 100, 255, PixelTypes.L8)] [WithSolidFilledImages(nameof(NonSubsampledEncodingSetups), 1, 1, 100, 100, 100, 255, PixelTypes.L8)]
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] [WithSolidFilledImages(nameof(NonSubsampledEncodingSetups), 1, 1, 255, 100, 50, 255, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 143, 81, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 143, 81, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 7, 5, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 96, 48, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 96, 48, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 73, 71, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 73, 71, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 24, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 48, 24, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 46, 8, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 46, 8, PixelTypes.Rgb24)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 51, 7, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 51, 7, PixelTypes.Rgb24)]
public void EncodeBaseline_WithSmallImages_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality) public void EncodeBaseline_WithSmallImages_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.12f)); where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality, ImageComparer.Tolerant(0.12f));
[Theory] [Theory]
[WithFile(TestImages.Png.BikeGrayscale, nameof(Grayscale_Quality), PixelTypes.L8)] [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.Rgb24, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.Rgba32, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.L8, 100)] [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.L8, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.L16, 100)] [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.L16, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La16, 100)] [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La16, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La32, 100)] [WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La32, 100)]
public void EncodeBaseline_Grayscale<TPixel>(TestImageProvider<TPixel> provider, int quality) public void EncodeBaseline_Grayscale<TPixel>(TestImageProvider<TPixel> provider, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, JpegColorType.Luminance, quality); where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, JpegEncodingColor.Luminance, quality);
[Theory] [Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 96, 96, PixelTypes.Rgba32 | PixelTypes.Bgra32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 96, 96, PixelTypes.Rgb24 | PixelTypes.Bgr24)]
public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality) [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 48, 48, PixelTypes.Rgb24 | PixelTypes.Bgr24)]
public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality); where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality);
[Theory] [Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)] [WithTestPatternImages(nameof(NonSubsampledEncodingSetups), 48, 48, PixelTypes.Rgb24 | PixelTypes.Bgr24)]
public void EncodeBaseline_WithSmallImages_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality) public void EncodeBaseline_WithSmallImages_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.06f)); where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, colorType, quality, comparer: ImageComparer.Tolerant(0.06f));
[Theory] [Theory]
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)] [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)]
[WithTestPatternImages(587, 821, PixelTypes.Rgba32, JpegColorType.YCbCrRatio444)] [WithTestPatternImages(587, 821, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)]
[WithTestPatternImages(677, 683, PixelTypes.Bgra32, JpegColorType.YCbCrRatio420)] [WithTestPatternImages(677, 683, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio420)]
[WithSolidFilledImages(400, 400, "Red", PixelTypes.Bgr24, JpegColorType.YCbCrRatio420)] [WithSolidFilledImages(400, 400, nameof(Color.Red), PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio420)]
public void EncodeBaseline_WorksWithDiscontiguousBuffers<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType) public void EncodeBaseline_WorksWithDiscontiguousBuffers<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageComparer comparer = colorType == JpegColorType.YCbCrRatio444 ImageComparer comparer = colorType == JpegEncodingColor.YCbCrRatio444
? ImageComparer.TolerantPercentage(0.1f) ? ImageComparer.TolerantPercentage(0.1f)
: ImageComparer.TolerantPercentage(5f); : ImageComparer.TolerantPercentage(5f);
@ -216,10 +171,40 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestJpegEncoderCore(provider, colorType, 100, comparer); TestJpegEncoderCore(provider, colorType, 100, comparer);
} }
[Theory]
[InlineData(JpegEncodingColor.YCbCrRatio420)]
[InlineData(JpegEncodingColor.YCbCrRatio444)]
public async Task Encode_IsCancellable(JpegEncodingColor colorType)
{
var cts = new CancellationTokenSource();
using var pausedStream = new PausedStream(new MemoryStream());
pausedStream.OnWaiting(s =>
{
// after some writing
if (s.Position >= 500)
{
cts.Cancel();
pausedStream.Release();
}
else
{
// allows this/next wait to unblock
pausedStream.Next();
}
});
using var image = new Image<Rgba32>(5000, 5000);
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var encoder = new JpegEncoder() { ColorType = colorType };
await image.SaveAsync(pausedStream, encoder, cts.Token);
});
}
/// <summary> /// <summary>
/// Anton's SUPER-SCIENTIFIC tolerance threshold calculation /// Anton's SUPER-SCIENTIFIC tolerance threshold calculation
/// </summary> /// </summary>
private static ImageComparer GetComparer(int quality, JpegColorType? colorType) private static ImageComparer GetComparer(int quality, JpegEncodingColor? colorType)
{ {
float tolerance = 0.015f; // ~1.5% float tolerance = 0.015f; // ~1.5%
@ -227,10 +212,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ {
tolerance *= 4.5f; tolerance *= 4.5f;
} }
else if (quality < 75 || colorType == JpegColorType.YCbCrRatio420) else if (quality < 75 || colorType == JpegEncodingColor.YCbCrRatio420)
{ {
tolerance *= 2.0f; tolerance *= 2.0f;
if (colorType == JpegColorType.YCbCrRatio420) if (colorType == JpegEncodingColor.YCbCrRatio420)
{ {
tolerance *= 2.0f; tolerance *= 2.0f;
} }
@ -239,18 +224,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
return ImageComparer.Tolerant(tolerance); return ImageComparer.Tolerant(tolerance);
} }
private static void TestJpegEncoderCore<TPixel>( private static void TestJpegEncoderCore<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality)
TestImageProvider<TPixel> provider, where TPixel : unmanaged, IPixel<TPixel>
JpegColorType colorType = JpegColorType.YCbCrRatio420, => TestJpegEncoderCore(provider, colorType, quality, GetComparer(quality, colorType));
int quality = 100,
ImageComparer comparer = null) private static void TestJpegEncoderCore<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel>
=> TestJpegEncoderCore(provider, colorType, quality, new TolerantImageComparer(tolerance));
private static void TestJpegEncoderCore<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, ImageComparer comparer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
// There is no alpha in Jpeg!
image.Mutate(c => c.MakeOpaque());
var encoder = new JpegEncoder var encoder = new JpegEncoder
{ {
Quality = quality, Quality = quality,
@ -258,171 +244,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}; };
string info = $"{colorType}-Q{quality}"; string info = $"{colorType}-Q{quality}";
comparer ??= GetComparer(quality, colorType);
// Does DebugSave & load reference CompareToReferenceInput(): // Does DebugSave & load reference CompareToReferenceInput():
image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "png"); image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "png");
} }
[Fact]
public void Quality_0_And_1_Are_Identical()
{
var options = new JpegEncoder
{
Quality = 0
};
var testFile = TestFile.Create(TestImages.Jpeg.Baseline.Calliphora);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
using (var memStream0 = new MemoryStream())
using (var memStream1 = new MemoryStream())
{
input.SaveAsJpeg(memStream0, options);
options.Quality = 1;
input.SaveAsJpeg(memStream1, options);
Assert.Equal(memStream0.ToArray(), memStream1.ToArray());
}
}
[Fact]
public void Quality_0_And_100_Are_Not_Identical()
{
var options = new JpegEncoder
{
Quality = 0
};
var testFile = TestFile.Create(TestImages.Jpeg.Baseline.Calliphora);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
using (var memStream0 = new MemoryStream())
using (var memStream1 = new MemoryStream())
{
input.SaveAsJpeg(memStream0, options);
options.Quality = 100;
input.SaveAsJpeg(memStream1, options);
Assert.NotEqual(memStream0.ToArray(), memStream1.ToArray());
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, JpegEncoder);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
}
[Fact]
public void Encode_PreservesIptcProfile()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
input.Metadata.IptcProfile = new IptcProfile();
input.Metadata.IptcProfile.SetValue(IptcTag.Byline, "unit_test");
// act
using var memStream = new MemoryStream();
input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
IptcProfile actual = output.Metadata.IptcProfile;
Assert.NotNull(actual);
IEnumerable<IptcValue> values = input.Metadata.IptcProfile.Values;
Assert.Equal(values, actual.Values);
}
[Fact]
public void Encode_PreservesExifProfile()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
input.Metadata.ExifProfile = new ExifProfile();
input.Metadata.ExifProfile.SetValue(ExifTag.Software, "unit_test");
// act
using var memStream = new MemoryStream();
input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
ExifProfile actual = output.Metadata.ExifProfile;
Assert.NotNull(actual);
IReadOnlyList<IExifValue> values = input.Metadata.ExifProfile.Values;
Assert.Equal(values, actual.Values);
}
[Fact]
public void Encode_PreservesIccProfile()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
input.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.Profile_Random_Array);
// act
using var memStream = new MemoryStream();
input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
IccProfile actual = output.Metadata.IccProfile;
Assert.NotNull(actual);
IccProfile values = input.Metadata.IccProfile;
Assert.Equal(values.Entries, actual.Entries);
}
[Theory]
[InlineData(JpegColorType.YCbCrRatio420)]
[InlineData(JpegColorType.YCbCrRatio444)]
public async Task Encode_IsCancellable(JpegColorType colorType)
{
var cts = new CancellationTokenSource();
using var pausedStream = new PausedStream(new MemoryStream());
pausedStream.OnWaiting(s =>
{
// after some writing
if (s.Position >= 500)
{
cts.Cancel();
pausedStream.Release();
}
else
{
// allows this/next wait to unblock
pausedStream.Next();
}
});
using var image = new Image<Rgba32>(5000, 5000);
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var encoder = new JpegEncoder() { ColorType = colorType };
await image.SaveAsync(pausedStream, encoder, cts.Token);
});
}
} }
} }

6
tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs

@ -12,13 +12,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Fact] [Fact]
public void CloneIsDeep() public void CloneIsDeep()
{ {
var meta = new JpegMetadata { Quality = 50, ColorType = JpegColorType.Luminance }; var meta = new JpegMetadata { ColorType = JpegEncodingColor.Luminance };
var clone = (JpegMetadata)meta.DeepClone(); var clone = (JpegMetadata)meta.DeepClone();
clone.Quality = 99; clone.ColorType = JpegEncodingColor.YCbCrRatio420;
clone.ColorType = JpegColorType.YCbCrRatio420;
Assert.False(meta.Quality.Equals(clone.Quality));
Assert.False(meta.ColorType.Equals(clone.ColorType)); Assert.False(meta.ColorType.Equals(clone.ColorType));
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save